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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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