From 91d106ed3062316ddbde15307a3aea5df7eed919 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 27 Jun 2026 18:30:36 -0700 Subject: [PATCH 1/3] Bundle release assets into LanguageTags.7z (v1.3-style) The GitHub release attached the raw .nupkg/.snupkg. Reinstate the v1.3 behavior: build the library and packages into one dir, archive them into a fixed-name LanguageTags.7z, and attach that single deterministic asset (releases/latest/download/LanguageTags.7z) instead of the raw files. NuGet publishing is unchanged - the .nupkg/.snupkg still push to NuGet.org; only the GitHub release asset changes. WORKFLOW.md updated to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-release-task.yml | 22 ++++++++++++++++------ WORKFLOW.md | 18 +++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index e68f3aa..076f9ab 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -112,13 +112,14 @@ jobs: # GeneratePackageOnBuild emits the .nupkg (and .snupkg) from the build, so build, not pack, is the # one-step producer (dotnet pack on top of it double-packs and fails). OutputPath and PackageOutputPath - # redirect the assembly and the packages out of the .artifacts layout into a build and a publish dir. + # both point at the publish dir, so the compiled library and the packages land together for the + # release archive; the push below still globs only *.nupkg. - name: Build library step run: | set -euo pipefail dotnet build ${{ env.PROJECT_FILE }} \ --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ - -property:OutputPath=${{ runner.temp }}/build/ \ + -property:OutputPath=${{ runner.temp }}/publish/ \ -property:PackageOutputPath=${{ runner.temp }}/publish/ \ -property:Version=${{ needs.get-version.outputs.SemVer2 }} \ -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ @@ -145,14 +146,22 @@ jobs: --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} \ --skip-duplicate - # Hand the package to the github-release job. Plain name (single target). Consumed within this run, + # Bundle the compiled library plus the .nupkg/.snupkg into one fixed-name LanguageTags.7z, so the + # release exposes a single deterministic asset (releases/latest/download/LanguageTags.7z). + - name: Compress release asset step + if: ${{ inputs.publish && !inputs.smoke }} + run: | + set -euo pipefail + 7z a -t7z ${{ runner.temp }}/LanguageTags.7z ${{ runner.temp }}/publish/* + + # Hand the archive to the github-release job. Plain name (single target). Consumed within this run, # so retention-days: 1 is the failure-path backstop. - name: Upload release asset step if: ${{ inputs.publish && !inputs.smoke }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-asset - path: ${{ runner.temp }}/publish/* + path: ${{ runner.temp }}/LanguageTags.7z retention-days: 1 github-release: @@ -191,7 +200,8 @@ jobs: fi # target_commitish must be the built commit, or GitHub tags the release on the default branch. The - # release is the tag plus the auto source zip, README, LICENSE, and the .nupkg/.snupkg. + # release is the tag plus the auto source zip, README, LICENSE, and LanguageTags.7z (the compiled + # library bundled with the .nupkg/.snupkg). - name: Create GitHub release step if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 @@ -204,7 +214,7 @@ jobs: files: | LICENSE README.md - ${{ runner.temp }}/publish/* + ${{ runner.temp }}/publish/LanguageTags.7z # Reclaim the transfer artifact now the release holds durable copies. Same gate as the create step # (exists == false || dispatch), so a no-op re-run keeps its artifact for the retention backstop. diff --git a/WORKFLOW.md b/WORKFLOW.md index 698f5cd..520a65c 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -212,9 +212,11 @@ release by dispatch. A merged dependency bump does not itself publish. See D8. The repo produces exactly one shipped artifact, the NuGet package. The leaf pushes the package, and where symbols are enabled its symbol package, to NuGet.org via OIDC trusted publishing (no long-lived API key, -D4.7), and attaches the `.nupkg`/`.snupkg` to the GitHub release. There is no generic multi-target -abstraction: no `enable_` flag selecting among leaves, no `expect_release_assets` toggle, no -`release-asset--*` glob. The single asset is attached directly by plain name. +D4.7), and bundles them with the compiled library into a single fixed-name `LanguageTags.7z` attached to +the GitHub release. There is no generic multi-target abstraction: no `enable_` flag selecting among +leaves, no `expect_release_assets` toggle, no `release-asset--*` glob. The single asset, +`LanguageTags.7z`, is attached by its fixed name, so `releases/latest/download/LanguageTags.7z` is a stable +download URL. ## 4. Behavioral contract - expected outcomes @@ -301,8 +303,9 @@ applicable guarantee is not operational (section 1). `GitCommitId`), never `github.sha` of a moving ref. *Prevents: the tag landing on a different commit than was built.* - **D4.4 Release contents and flag.** Output: every release is a tag on the built commit plus the auto - source zip, README, and LICENSE, with the `.nupkg` and (where `IncludeSymbols`) the `.snupkg` attached. - The GitHub-release `prerelease` boolean is set to `github.ref_name != 'main'`. *(GitHub computes the + source zip, README, and LICENSE, with a fixed-name `LanguageTags.7z` attached that bundles the compiled + library, the `.nupkg`, and (where `IncludeSymbols`) the `.snupkg`. The GitHub-release `prerelease` + boolean is set to `github.ref_name != 'main'`. *(GitHub computes the "Latest" badge from semver across non-prerelease releases, a consequence, not a workflow assertion.)* - **D4.5 No-op republish.** Input: a re-run whose version is unchanged. Output: the release-create step is skipped when the tag already exists (refreshed only on `workflow_dispatch`). The NuGet push runs and @@ -437,8 +440,9 @@ guarantee, each pass/fail/N-A with a `file:line` citation: `PUBLISH_ON_MERGE`; the dispatch path is guarded to `main`/`develop`; the publisher calls the same `validate-task` as a `validate` job and the publish job `needs:` it (D4.6); the run publishes only `github.ref_name`; `target_commitish` is the NBGV commit id; the GitHub-release `prerelease` boolean - `== (github.ref_name != 'main')`; the release body attaches the source zip, README, and LICENSE; the - leaf pushes `*.nupkg` and `*.snupkg` (symbols enabled) with `--skip-duplicate`; the publish job grants + `== (github.ref_name != 'main')`; the release body attaches the source zip, README, LICENSE, and a + fixed-name `LanguageTags.7z` bundle (compiled library + `.nupkg`/`.snupkg`); the leaf pushes `*.nupkg` + and `*.snupkg` (symbols enabled) with `--skip-duplicate`; the publish job grants `id-token: write` and pushes with a `NuGet/login@v1` short-lived key, not a `NUGET_API_KEY` secret (D4.7); release-create gated `exists == false || workflow_dispatch`. - **D5:** each cross-job transfer artifact has a delete gated to its consumer, `continue-on-error: true`, From ff38e91ffc94e41f81c07a349e2fdbd585cd00c4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 27 Jun 2026 18:41:19 -0700 Subject: [PATCH 2/3] Flat 7z archive, trim comments, stop auto-deleting protected branches - Compress step cd's into the output dir so LanguageTags.7z is flat (no absolute runner path), and trim the over-verbose build/compress/release comments to one line each. - delete_branch_on_merge is now off (it ignores rulesets and deleted develop on the promotion merge via the admin bypass). Feature and bot branches are deleted explicitly instead: the merge-bot now passes --delete-branch, and feature/fix merges already use it. main/develop survive promotions. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-release-task.yml | 15 +++++---------- .github/workflows/merge-bot-pull-request.yml | 4 ++-- repo-config/README.md | 3 ++- repo-config/settings.json | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 076f9ab..54dbf54 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -110,10 +110,7 @@ jobs: with: ref: ${{ needs.get-version.outputs.GitCommitId }} - # GeneratePackageOnBuild emits the .nupkg (and .snupkg) from the build, so build, not pack, is the - # one-step producer (dotnet pack on top of it double-packs and fails). OutputPath and PackageOutputPath - # both point at the publish dir, so the compiled library and the packages land together for the - # release archive; the push below still globs only *.nupkg. + # Build the library and its packages into publish/ (both output paths) so they archive together. - name: Build library step run: | set -euo pipefail @@ -146,13 +143,13 @@ jobs: --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} \ --skip-duplicate - # Bundle the compiled library plus the .nupkg/.snupkg into one fixed-name LanguageTags.7z, so the - # release exposes a single deterministic asset (releases/latest/download/LanguageTags.7z). + # Bundle the build output and packages into a flat LanguageTags.7z (cd in so the paths are relative). - name: Compress release asset step if: ${{ inputs.publish && !inputs.smoke }} run: | set -euo pipefail - 7z a -t7z ${{ runner.temp }}/LanguageTags.7z ${{ runner.temp }}/publish/* + cd ${{ runner.temp }}/publish + 7z a -t7z ${{ runner.temp }}/LanguageTags.7z ./* # Hand the archive to the github-release job. Plain name (single target). Consumed within this run, # so retention-days: 1 is the failure-path backstop. @@ -199,9 +196,7 @@ jobs: echo "exists=false" >> "$GITHUB_OUTPUT" fi - # target_commitish must be the built commit, or GitHub tags the release on the default branch. The - # release is the tag plus the auto source zip, README, LICENSE, and LanguageTags.7z (the compiled - # library bundled with the .nupkg/.snupkg). + # target_commitish must be the built commit, or GitHub tags the release on the default branch. - name: Create GitHub release step if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index a9d5420..1ec57af 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -53,7 +53,7 @@ jobs: main) method=--merge ;; *) echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"; exit 1 ;; esac - gh pr merge --auto "$method" "$PR_URL" + gh pr merge --auto --delete-branch "$method" "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} @@ -92,7 +92,7 @@ jobs: main) method=--merge ;; *) echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}"; exit 1 ;; esac - gh pr merge --auto "$method" "$PR_URL" + gh pr merge --auto --delete-branch "$method" "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/repo-config/README.md b/repo-config/README.md index 2489845..5e833a3 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -21,7 +21,8 @@ templates); repository administration config-as-code is the maintainer's, so it - [`ruleset-main.json`](./ruleset-main.json) - the `main` branch ruleset (merge-commit-only, signed commits, the same required check, strict **off**; no linear-history rule). - [`settings.json`](./settings.json) - repository settings (auto-merge on; squash **and** merge-commit - allowed; rebase off; delete branch on merge). + allowed; rebase off; auto-delete-on-merge **off**, so `main`/`develop` survive a promotion - feature and + bot branches are deleted explicitly via `--delete-branch`). ## What it does not store diff --git a/repo-config/settings.json b/repo-config/settings.json index 7c5d338..fc373ef 100644 --- a/repo-config/settings.json +++ b/repo-config/settings.json @@ -3,5 +3,5 @@ "allow_merge_commit": true, "allow_rebase_merge": false, "allow_auto_merge": true, - "delete_branch_on_merge": true + "delete_branch_on_merge": false } From d80664e09c7359fa95531d4e1b8587843d0ce137 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 27 Jun 2026 18:50:08 -0700 Subject: [PATCH 3/3] Clarify branch-cleanup wording in repo-config README Feature branches are not auto-deleted with delete_branch_on_merge off; the merge-bot --delete-branch handles bot PRs, and feature PRs are merged with --delete-branch. (GitHub's auto-delete-head-branches setting ignores deletion protection for non-default branches, which is why it deleted develop; the fix is to keep the setting off.) Co-Authored-By: Claude Opus 4.8 (1M context) --- repo-config/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo-config/README.md b/repo-config/README.md index 5e833a3..70af52e 100644 --- a/repo-config/README.md +++ b/repo-config/README.md @@ -21,8 +21,8 @@ templates); repository administration config-as-code is the maintainer's, so it - [`ruleset-main.json`](./ruleset-main.json) - the `main` branch ruleset (merge-commit-only, signed commits, the same required check, strict **off**; no linear-history rule). - [`settings.json`](./settings.json) - repository settings (auto-merge on; squash **and** merge-commit - allowed; rebase off; auto-delete-on-merge **off**, so `main`/`develop` survive a promotion - feature and - bot branches are deleted explicitly via `--delete-branch`). + allowed; rebase off; auto-delete-on-merge **off**, so `main`/`develop` survive a promotion - the + merge-bot passes `--delete-branch` for bot PRs, and feature PRs are merged with `--delete-branch`). ## What it does not store