From a0405db75e2f7ccc50f37cd8c54502dde21884ce Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 28 Aug 2026 18:33:16 +0200 Subject: [PATCH 1/5] * the release changelog is proposed as a pull request, not pushed to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main is about to take changes through pull requests only, and GitHub does not allow the Actions integration as a ruleset bypass actor on a personal-account repository, so this job cannot keep pushing. The release marker now tags the commit the packages were actually built and published from. It used to move to the changelog commit — a commit that contains none of the released code — which was wrong before this change and is simply no longer possible after it. CHANGELOG_PR_TOKEN is optional. Nothing done with GITHUB_TOKEN can trigger another workflow, so a pull request opened with it arrives with no checks and cannot satisfy a required-checks rule; with a personal access token carrying contents and pull-requests write, the pull request behaves like any other. --- .github/workflows/release.yml | 39 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 099fe996..7943eae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ on: permissions: contents: write + pull-requests: write # Force JavaScript-based actions to run on Node 24 instead of the deprecated Node 20 ahead of # the 2026-06-16 hard cutover, until we bump each action to a Node-24-native major version. @@ -78,16 +79,38 @@ jobs: node-version: '22' - name: Update changelog + generate release notes (bucketed by + - * # !) run: node ".github/workflows/scripts/update-changelog.mjs" --release "$REL" --notes RELEASE_NOTES.md - - name: Commit changelog (if it changed) + # main takes changes through pull requests only, so the changelog is proposed rather than + # pushed. The release is therefore tagged at the commit the packages were actually built and + # published from, which it should have been all along — it used to move to the changelog + # commit, a commit containing none of the released code. + # + # CHANGELOG_PR_TOKEN is optional and only affects whether CI runs on the proposed change. + # Anything done with GITHUB_TOKEN cannot trigger another workflow, so a pull request opened + # with it arrives with no checks and cannot satisfy a required-checks rule. + - name: Propose the changelog (if it changed) + env: + GH_TOKEN: ${{ secrets.CHANGELOG_PR_TOKEN || secrets.GITHUB_TOKEN }} run: | - if ! git diff --quiet -- CHANGELOG.md; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add CHANGELOG.md - git commit -m "* update changelog for $REL [skip ci]" - git push + rel_sha=$(git rev-parse HEAD) + echo "REL_SHA=$rel_sha" >> "$GITHUB_ENV" + if git diff --quiet -- CHANGELOG.md; then + echo "The changelog is already current." + exit 0 fi - echo "REL_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + branch="bot/changelog-$REL" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$branch" + git add CHANGELOG.md + git commit -m "* update changelog for $REL" + git push --force origin "$branch" + if [ -n "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then + echo "The open pull request for $branch now carries the current changelog." + exit 0 + fi + gh pr create --base main --head "$branch" \ + --title "* update changelog for $REL" \ + --body "The commit-message changelog for release $REL, cut from $rel_sha." - name: Create GitHub Release (dated marker; tags vyyyyMMdd at the changelog commit) uses: softprops/action-gh-release@v2 with: From cac3f56e6fbdeaee0a69ed6ce0754010b5bdb5f1 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 28 Aug 2026 22:24:15 +0200 Subject: [PATCH 2/5] * the CI/CD readme documents the changelog pull request and the tagged commit --- .github/workflows/README.md | 8 +++++++- .github/workflows/release.yml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 96f8920e..7370dee0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -51,6 +51,12 @@ in `scripts/`. - **`version.pl` is identical in every repo.** For a non-.NET repo (no csproj) it falls back to a root `VERSION` file — the only place that file is ever used. - **Changelogs are automatic:** nightlies and releases generate their notes from - commits; `release.yml` also refreshes and commits `CHANGELOG.md`. + commits; `release.yml` also refreshes `CHANGELOG.md` and proposes it as a pull + request on `bot/changelog-vyyyyMMdd`, because `main` takes changes through pull + requests only. The dated tag stays on the commit the packages were built from. +- **`secrets.CHANGELOG_PR_TOKEN`** is optional. Without it the changelog pull + request is opened with `GITHUB_TOKEN`, which cannot trigger another workflow, so + that pull request arrives with no checks and cannot satisfy a required-checks + rule. A PAT with contents + pull-requests write makes it behave like any other. - **Manual vs automatic:** stable releases (NuGet packaging + publish) are cut on demand via dispatch; nightlies and changelog notes happen automatically. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7943eae3..9c17cb29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: gh pr create --base main --head "$branch" \ --title "* update changelog for $REL" \ --body "The commit-message changelog for release $REL, cut from $rel_sha." - - name: Create GitHub Release (dated marker; tags vyyyyMMdd at the changelog commit) + - name: Create GitHub Release (dated marker; tags vyyyyMMdd at the released commit) uses: softprops/action-gh-release@v2 with: tag_name: ${{ env.REL }} From 2ceebc1e40dbe40d0d3c4c5442b6454b91f83cb4 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 28 Aug 2026 22:29:00 +0200 Subject: [PATCH 3/5] * the changelog pull request carries a signed commit, from the shared action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A commit made by git on the runner is unsigned, so the pull request this job opened would have satisfied the pull-request rule and then failed required_signatures forever. DriveBenderUtility hit exactly that and its coverage-matrix pull request had to be re-signed by hand before it could be merged. publish-generated-file@v1 commits through the contents API, which GitHub signs, and keeps one long-lived bot/changelog branch rather than one per release marker — a changelog is worth one open pull request, not one per release. --- .github/workflows/release.yml | 48 +++++++++++++---------------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c17cb29..7f019e00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,38 +79,24 @@ jobs: node-version: '22' - name: Update changelog + generate release notes (bucketed by + - * # !) run: node ".github/workflows/scripts/update-changelog.mjs" --release "$REL" --notes RELEASE_NOTES.md - # main takes changes through pull requests only, so the changelog is proposed rather than - # pushed. The release is therefore tagged at the commit the packages were actually built and - # published from, which it should have been all along — it used to move to the changelog - # commit, a commit containing none of the released code. - # - # CHANGELOG_PR_TOKEN is optional and only affects whether CI runs on the proposed change. - # Anything done with GITHUB_TOKEN cannot trigger another workflow, so a pull request opened - # with it arrives with no checks and cannot satisfy a required-checks rule. + # The release is tagged at the commit the packages were actually built and published from, + # which it should have been all along — it used to move to the changelog commit, a commit + # containing none of the released code. It cannot move there any more in any case, because + # the changelog is now proposed rather than pushed. + - name: Record the commit the release is cut from + run: echo "REL_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + + # main takes changes through pull requests only. CHANGELOG_PR_TOKEN is optional and only + # affects whether CI runs on the proposed change — see the action's token input. - name: Propose the changelog (if it changed) - env: - GH_TOKEN: ${{ secrets.CHANGELOG_PR_TOKEN || secrets.GITHUB_TOKEN }} - run: | - rel_sha=$(git rev-parse HEAD) - echo "REL_SHA=$rel_sha" >> "$GITHUB_ENV" - if git diff --quiet -- CHANGELOG.md; then - echo "The changelog is already current." - exit 0 - fi - branch="bot/changelog-$REL" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git checkout -B "$branch" - git add CHANGELOG.md - git commit -m "* update changelog for $REL" - git push --force origin "$branch" - if [ -n "$(gh pr list --head "$branch" --state open --json number --jq '.[].number')" ]; then - echo "The open pull request for $branch now carries the current changelog." - exit 0 - fi - gh pr create --base main --head "$branch" \ - --title "* update changelog for $REL" \ - --body "The commit-message changelog for release $REL, cut from $rel_sha." + uses: Hawkynt/RepositoryTemplate/publish-generated-file@v1 + with: + file: CHANGELOG.md + branch: bot/changelog + message: '* update changelog for ${{ env.REL }}' + title: '* update changelog' + body: The commit-message changelog, regenerated by the release job. + token: ${{ secrets.CHANGELOG_PR_TOKEN || github.token }} - name: Create GitHub Release (dated marker; tags vyyyyMMdd at the released commit) uses: softprops/action-gh-release@v2 with: From 0072c9a273d39ecff49ab20aaf16a50ded18cdb5 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 28 Aug 2026 22:30:10 +0200 Subject: [PATCH 4/5] * the CI/CD readme follows the changelog branch to bot/changelog One long-lived branch rather than one per release marker, and the commit is made through the contents API so it is signed. --- .github/workflows/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 7370dee0..c861fd54 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -52,8 +52,12 @@ in `scripts/`. falls back to a root `VERSION` file — the only place that file is ever used. - **Changelogs are automatic:** nightlies and releases generate their notes from commits; `release.yml` also refreshes `CHANGELOG.md` and proposes it as a pull - request on `bot/changelog-vyyyyMMdd`, because `main` takes changes through pull - requests only. The dated tag stays on the commit the packages were built from. + request on `bot/changelog`, because `main` takes changes through pull requests + only. One long-lived branch, so there is one open changelog pull request rather + than one per release. The dated tag stays on the commit the packages were built + from. The commit is made through the contents API by + `RepositoryTemplate/publish-generated-file`, because a commit made by `git` on a + runner is unsigned and would fail the signature rule forever. - **`secrets.CHANGELOG_PR_TOKEN`** is optional. Without it the changelog pull request is opened with `GITHUB_TOKEN`, which cannot trigger another workflow, so that pull request arrives with no checks and cannot satisfy a required-checks From f9e9d044bb43a73e25e3a695a98585cdc5ec5214 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Sat, 29 Aug 2026 14:38:41 +0200 Subject: [PATCH 5/5] * the release changelog is uploaded, not proposed as a pull request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A release-time changelog cannot be a pull-request check — nothing generates it until a release is cut — and a bot pull request carrying it would need a signed commit to be mergeable and a personal access token before its own checks would run, for a file the dated Release already states in full. It is uploaded as the changelog artifact instead, and the committed copy is refreshed when that is worth doing. The release job stops needing pull-requests: write. The release marker still tags the commit the packages were built and published from, which is the part of this branch that was right all along. --- .github/workflows/README.md | 16 +++++----------- .github/workflows/release.yml | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c861fd54..a9ee882a 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -51,16 +51,10 @@ in `scripts/`. - **`version.pl` is identical in every repo.** For a non-.NET repo (no csproj) it falls back to a root `VERSION` file — the only place that file is ever used. - **Changelogs are automatic:** nightlies and releases generate their notes from - commits; `release.yml` also refreshes `CHANGELOG.md` and proposes it as a pull - request on `bot/changelog`, because `main` takes changes through pull requests - only. One long-lived branch, so there is one open changelog pull request rather - than one per release. The dated tag stays on the commit the packages were built - from. The commit is made through the contents API by - `RepositoryTemplate/publish-generated-file`, because a commit made by `git` on a - runner is unsigned and would fail the signature rule forever. -- **`secrets.CHANGELOG_PR_TOKEN`** is optional. Without it the changelog pull - request is opened with `GITHUB_TOKEN`, which cannot trigger another workflow, so - that pull request arrives with no checks and cannot satisfy a required-checks - rule. A PAT with contents + pull-requests write makes it behave like any other. + commits. `release.yml` also refreshes `CHANGELOG.md` and uploads it as the `changelog` + artifact — the dated Release already states the notes in full, so the committed copy is + refreshed when you want it rather than by a bot. `main` takes changes through pull requests + only, and a bot's pull request cannot satisfy the signature and check rules without a + personal access token. - **Manual vs automatic:** stable releases (NuGet packaging + publish) are cut on demand via dispatch; nightlies and changelog notes happen automatically. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f019e00..feab17e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,6 @@ on: permissions: contents: write - pull-requests: write # Force JavaScript-based actions to run on Node 24 instead of the deprecated Node 20 ahead of # the 2026-06-16 hard cutover, until we bump each action to a Node-24-native major version. @@ -86,17 +85,18 @@ jobs: - name: Record the commit the release is cut from run: echo "REL_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" - # main takes changes through pull requests only. CHANGELOG_PR_TOKEN is optional and only - # affects whether CI runs on the proposed change — see the action's token input. - - name: Propose the changelog (if it changed) - uses: Hawkynt/RepositoryTemplate/publish-generated-file@v1 + # Neither pushed nor proposed. A release-time changelog cannot be a pull-request check — + # nothing generates it until a release is cut — and a bot pull request carrying it would need + # a signed commit to be mergeable and a personal access token before its own checks would run, + # for a file the GitHub Release already states in full. It is uploaded; commit it when you + # want the committed copy refreshed. + - uses: actions/upload-artifact@v4 + if: always() with: - file: CHANGELOG.md - branch: bot/changelog - message: '* update changelog for ${{ env.REL }}' - title: '* update changelog' - body: The commit-message changelog, regenerated by the release job. - token: ${{ secrets.CHANGELOG_PR_TOKEN || github.token }} + name: changelog + path: CHANGELOG.md + if-no-files-found: warn + - name: Create GitHub Release (dated marker; tags vyyyyMMdd at the released commit) uses: softprops/action-gh-release@v2 with: