Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 6 additions & 13 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
# Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
# the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
# Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
# for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
outputs:
# Output of the uploaded artifact id
artifact-id:
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]

steps:
Expand DownExpand Up@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*

# Branch-suffixed so the publisher's branch matrix can build both
# branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: nugetlibrary-build-${{ inputs.branch }}
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
# Intermediate artifact consumed by build-release-task in the same run.
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
5 changes: 3 additions & 2 deletions .github/workflows/build-release-task.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}

- name: Download library build artifacts step
- name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
pattern: release-asset-${{ inputs.branch }}-*
merge-multiple: true
path: ./Publish

# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}

# Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [setup, publish, date-badge]
if: ${{ always() && needs.setup.outputs.publish == 'true' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
27 changes: 27 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"

# Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
# `check-workflow-status`'s needs so housekeeping never gates the required merge check.
cleanup-artifacts:
name: Delete workflow artifacts job
needs: [smoke-build]
if: always()
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Delete workflow artifacts step
# Best-effort housekeeping must never fail the run.
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
--jq '.artifacts[].id'); then
echo "::warning::Could not list run artifacts; skipping cleanup."
ids=""
fi
for artifact_id in $ids; do
gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
|| echo "::warning::Failed to delete artifact $artifact_id; continuing."
done
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset-<branch>-<target>`; the verbatim `github-release` job collects every `release-asset-<branch>-*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).

## Project Structure
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
Expand Down
7 changes: 0 additions & 7 deletions LanguageTags/LanguageLookup.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,6 @@ public sealed class LanguageLookup

try
{
// Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);

// Make sure the culture was not custom created
Expand DownExpand Up@@ -173,15 +172,13 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

// Get ISO 639-2 record
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
// Return the Part 2B code
return iso6392.Part2B!;
}

Expand All@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
// Return the Part 2B code
return iso6393.Part2B!;
}

Expand DownExpand Up@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
// Exact match
return true;
}

Expand All@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
// Prefix match
return true;
}

Expand All@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
// Rematch
languageTag = subtag.TagValue;
continue;
}
Expand Down
Loading