From 0bbea50b7c805d22c6ca2cc551dad5b181086c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sat, 19 Sep 2026 14:28:03 +0200 Subject: [PATCH 1/5] Fix docs snapshot workflow: direct push to main blocked by branch protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Snapshot versioned docs' job (triggered by the v2.0.0 tag) failed at its last step: stefanzweifel/git-auto-commit-action pushed an unsigned commit directly to main, which the quality-and-security-gating ruleset now rejects (2 required status checks expected + commits must be signed). - Replace the direct push with a PR + gh pr merge --auto flow using the pre-installed git/gh CLIs (no third-party action) — GitHub's own merge-via-API produces a Verified commit, and auto-merge waits for the required CI checks to pass before merging. - Removing that action also drops its Node 20 runtime deprecation warning. - Fix versions.json generation: the new major version entry is now tagged '(latest)' and that suffix is stripped from superseded entries (was previously left on the old entry, and never added to the new one). - Manually apply the v2 entry to docs/public/versions.json, since the earlier failed run never got to commit it. - Remove the now-unused git-auto-commit-action row from .github/skills/github-actions.md's action version table. --- .github/skills/github-actions.md | 1 - .github/workflows/docs.yaml | 65 +++++++++++++++++++++++++------- docs/public/versions.json | 11 +++++- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/.github/skills/github-actions.md b/.github/skills/github-actions.md index dcc221d..c58e188 100644 --- a/.github/skills/github-actions.md +++ b/.github/skills/github-actions.md @@ -69,7 +69,6 @@ a SHA corresponds to without running `git ls-remote` manually. | `actions/github-script` | v7 | `f28e40c7f34bde8b3046d885e986cb6290c5673b` | | `marocchino/sticky-pull-request-comment` | v2.9.4 | `773744901bac0e8cbb5a0dc842800d45e9b2b405` | | `romeovs/lcov-reporter-action` | v0.4.0 | `87a815f34ec27a5826abba44ce09bbc688da58fd` | -| `stefanzweifel/git-auto-commit-action` | v5.0.1 | `8621497c8c39c72f3e2a999a26b4ca1b5058a842` | | `bats-core/bats-action` | v4.0.0 | `77d6fb60505b4d0d1d73e48bd035b55074bbfb43` | When upgrading an action, update **every row in this table** and **every workflow file** diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f859211..5524857 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -113,8 +113,10 @@ jobs: # 2. Stores the output in the gh-pages branch under /v2/ using plain git # (no third-party action). The gh-pages branch is storage only — Pages # still points to GitHub Actions; the deploy job merges snapshots in. - # 3. Prepends the entry to versions.json on main WITHOUT [skip ci], which - # re-triggers the deploy job so the new snapshot is live immediately. + # 3. Opens a PR with the versions.json + blog stub changes against main and + # enables auto-merge — main requires signed commits and passing status + # checks, so a direct push is rejected. Once merged (no [skip ci]), the + # push re-triggers the deploy job so the new snapshot goes live. # # Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot. # Patch and minor releases update the main docs in-place via the deploy job. @@ -124,6 +126,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + pull-requests: write steps: - name: Checkout @@ -188,9 +191,13 @@ jobs: run: | MAJOR="${{ steps.ver.outputs.major }}" LINK="/${MAJOR}/" - # Idempotent — skip if the entry already exists. - jq --arg text "$MAJOR" --arg link "$LINK" \ - 'if any(.[]; .link == $link) then . else [{"text": $text, "link": $link}] + . end' \ + # Idempotent — skip if the entry already exists. Otherwise, prepend + # the new version tagged "(latest)" and strip that suffix from any + # previous entry, since it's now superseded. + jq --arg text "$MAJOR (latest)" --arg link "$LINK" \ + 'if any(.[]; .link == $link) then . + else [{"text": $text, "link": $link}] + (map(.text |= sub(" [(]latest[)]$"; ""))) + end' \ docs/public/versions.json > /tmp/versions_new.json mv /tmp/versions_new.json docs/public/versions.json @@ -256,12 +263,42 @@ jobs: PY fi - - name: Commit blog stub and versions.json to main - # Pin to exact commit SHA to prevent supply-chain attacks. - uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1 - with: - # No [skip ci] — the push to main matches paths: docs/** and re-triggers - # the deploy job, which merges the new snapshot into the Pages artifact. - commit_message: "docs: add ${{ steps.ver.outputs.major }} to versions.json and blog" - file_pattern: docs/public/versions.json docs/blog/ - branch: main + - name: Open PR with versions.json and blog stub, enable auto-merge + # main is protected by a ruleset requiring signed commits and passing + # status checks — a direct push (previously via + # stefanzweifel/git-auto-commit-action) is rejected. Open a PR instead: + # GitHub's own merge-via-API produces a Verified commit, and auto-merge + # waits for CI (required checks) to pass before merging — no third-party + # action needed, using the pre-installed git/gh CLIs. + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + MAJOR="${{ steps.ver.outputs.major }}" + BRANCH="docs/${MAJOR}-snapshot-metadata" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/public/versions.json docs/blog/ + if git diff --staged --quiet; then + echo "No changes to commit — skipping." + exit 0 + fi + + # Re-base the pending changes onto a fresh branch off origin/main — + # the tag checkout and main's tip are normally the same commit, but + # this is robust even if main advanced in between. + git stash + git fetch origin main + git checkout -B "$BRANCH" origin/main + git stash pop + git add docs/public/versions.json docs/blog/ + git commit -m "docs: add ${MAJOR} to versions.json and blog" + git push --force origin "$BRANCH" + + PR_URL=$(gh pr create --base main --head "$BRANCH" \ + --title "docs: add ${MAJOR} to versions.json and blog" \ + --body "Automated versioned-docs snapshot metadata for \`${GITHUB_REF_NAME}\`." \ + 2>&1) || PR_URL=$(gh pr view "$BRANCH" --json url -q .url) + echo "Opened $PR_URL" + gh pr merge "$BRANCH" --auto --squash diff --git a/docs/public/versions.json b/docs/public/versions.json index 80c42a0..d25f9ce 100644 --- a/docs/public/versions.json +++ b/docs/public/versions.json @@ -1 +1,10 @@ -[{ "text": "v1 (latest)", "link": "/" }] +[ + { + "text": "v2 (latest)", + "link": "/v2/" + }, + { + "text": "v1", + "link": "/" + } +] From cb224dc09281aeb0820ad1a5727c999e277ba272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sat, 19 Sep 2026 14:48:19 +0200 Subject: [PATCH 2/5] Fix docs snapshot workflow: dispatch CI explicitly before merging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review: gh pr merge --auto alone would wait indefinitely, since the PR is opened using the default GITHUB_TOKEN and GITHUB_TOKEN- authored events don't trigger new workflow runs — ci.yaml's pull_request trigger never fires for this PR, so its required checks stay 'expected' forever. - Add workflow_dispatch to ci.yaml (exempt from the no-cascade restriction, the documented way to get a fresh run from automation). - docs.yaml: explicitly dispatch ci.yaml for the snapshot-metadata branch, wait for it to complete (gh run watch --exit-status), then merge directly instead of relying on --auto. - Add the actions: write permission needed to dispatch workflows. --- .github/workflows/ci.yaml | 8 +++++++ .github/workflows/docs.yaml | 44 +++++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9126474..af78b13 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,6 +3,14 @@ name: CI on: pull_request: branches: ["main"] + # Allows other automation (e.g. docs.yaml's snapshot job) to explicitly + # trigger a CI run for a specific branch/SHA. Required because events + # authored by the default GITHUB_TOKEN (e.g. opening a PR from a workflow) + # do NOT cascade into new workflow runs — workflow_dispatch is exempted + # from that restriction, so it's the supported way to get a fresh run + # whose check results attach to that commit for required-status-check + # evaluation. + workflow_dispatch: jobs: test: diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 5524857..62ab2b2 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -113,10 +113,13 @@ jobs: # 2. Stores the output in the gh-pages branch under /v2/ using plain git # (no third-party action). The gh-pages branch is storage only — Pages # still points to GitHub Actions; the deploy job merges snapshots in. - # 3. Opens a PR with the versions.json + blog stub changes against main and - # enables auto-merge — main requires signed commits and passing status - # checks, so a direct push is rejected. Once merged (no [skip ci]), the - # push re-triggers the deploy job so the new snapshot goes live. + # 3. Opens a PR with the versions.json + blog stub changes against main — + # main requires signed commits and passing status checks, so a direct + # push is rejected. Since GITHUB_TOKEN-authored events don't trigger new + # workflow runs, ci.yaml is explicitly dispatched (workflow_dispatch is + # exempt from that restriction) for this branch/SHA and awaited before + # merging. Once merged (no [skip ci]), the push re-triggers the deploy + # job so the new snapshot goes live. # # Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot. # Patch and minor releases update the main docs in-place via the deploy job. @@ -127,6 +130,7 @@ jobs: permissions: contents: write pull-requests: write + actions: write steps: - name: Checkout @@ -263,13 +267,20 @@ jobs: PY fi - - name: Open PR with versions.json and blog stub, enable auto-merge + - name: Open PR with versions.json and blog stub, run CI, and merge # main is protected by a ruleset requiring signed commits and passing # status checks — a direct push (previously via # stefanzweifel/git-auto-commit-action) is rejected. Open a PR instead: - # GitHub's own merge-via-API produces a Verified commit, and auto-merge - # waits for CI (required checks) to pass before merging — no third-party - # action needed, using the pre-installed git/gh CLIs. + # GitHub's own merge-via-API produces a Verified commit. + # + # `gh pr merge --auto` alone is NOT enough here: this PR/branch is + # created using the default GITHUB_TOKEN, and events authored by that + # token don't cascade into new workflow runs — ci.yaml's `pull_request` + # trigger never fires, the required checks stay "expected" forever, + # and auto-merge would wait indefinitely. workflow_dispatch is + # exempted from that restriction, so we explicitly dispatch ci.yaml + # for this branch/SHA, wait for it, then merge directly once it's + # green — see review on PR #215. env: GH_TOKEN: ${{ github.token }} run: | @@ -301,4 +312,19 @@ jobs: --body "Automated versioned-docs snapshot metadata for \`${GITHUB_REF_NAME}\`." \ 2>&1) || PR_URL=$(gh pr view "$BRANCH" --json url -q .url) echo "Opened $PR_URL" - gh pr merge "$BRANCH" --auto --squash + + gh workflow run ci.yaml --ref "$BRANCH" + RUN_ID="" + for _ in $(seq 1 10); do + RUN_ID=$(gh run list --workflow=ci.yaml --branch="$BRANCH" \ + --event=workflow_dispatch --limit=1 --json databaseId --jq '.[0].databaseId // empty') + [ -n "$RUN_ID" ] && break + sleep 3 + done + if [ -z "$RUN_ID" ]; then + echo "::error::Could not find the dispatched CI run for $BRANCH" >&2 + exit 1 + fi + gh run watch "$RUN_ID" --exit-status + + gh pr merge "$BRANCH" --squash From c182ab4f1d5848e327667103ce67ea3114561c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sat, 19 Sep 2026 15:13:13 +0200 Subject: [PATCH 3/5] Scope the CI run lookup to the exact pushed SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review: the run lookup only filtered by branch name, so a retry (or a stale run left over from an earlier attempt reusing this branch) could match an older, already-green run whose SHA no longer corresponds to what was just pushed — gh run watch would then report success while the current commit's required checks stay unsatisfied. Capture the pushed SHA right after the force-push and filter the workflow_dispatch run list by headSha before watching it. --- .github/workflows/docs.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 62ab2b2..23a319b 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -306,6 +306,7 @@ jobs: git add docs/public/versions.json docs/blog/ git commit -m "docs: add ${MAJOR} to versions.json and blog" git push --force origin "$BRANCH" + SHA="$(git rev-parse HEAD)" PR_URL=$(gh pr create --base main --head "$BRANCH" \ --title "docs: add ${MAJOR} to versions.json and blog" \ @@ -316,13 +317,18 @@ jobs: gh workflow run ci.yaml --ref "$BRANCH" RUN_ID="" for _ in $(seq 1 10); do + # Filter by headSha, not just the newest run for this branch — a + # retry (or an earlier attempt reusing this branch name) can leave + # a stale, already-green run around whose SHA no longer matches + # what was just pushed. See review on PR #215. RUN_ID=$(gh run list --workflow=ci.yaml --branch="$BRANCH" \ - --event=workflow_dispatch --limit=1 --json databaseId --jq '.[0].databaseId // empty') + --event=workflow_dispatch --limit=10 --json databaseId,headSha \ + --jq --arg sha "$SHA" '[.[] | select(.headSha == $sha)] | .[0].databaseId // empty') [ -n "$RUN_ID" ] && break sleep 3 done if [ -z "$RUN_ID" ]; then - echo "::error::Could not find the dispatched CI run for $BRANCH" >&2 + echo "::error::Could not find the dispatched CI run for $BRANCH at $SHA" >&2 exit 1 fi gh run watch "$RUN_ID" --exit-status From be9d63f803d297ecfade2e417d0416da1a95e83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sat, 19 Sep 2026 15:33:24 +0200 Subject: [PATCH 4/5] Fix invalid gh run list --jq invocation (--arg unsupported) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review: gh CLI's --jq flag takes a single expression string and does not support jq's --arg the way the standalone jq binary does. As written, --jq consumed "--arg" as its expression and the remaining tokens were passed as unexpected extra arguments, erroring out every time — meaning every future major-release snapshot job would fail before reaching the merge step. Interpolate the validated SHA (git rev-parse HEAD output, not user input) directly into the jq expression string instead. --- .github/workflows/docs.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 23a319b..e5e85a2 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -321,9 +321,12 @@ jobs: # retry (or an earlier attempt reusing this branch name) can leave # a stale, already-green run around whose SHA no longer matches # what was just pushed. See review on PR #215. + # Note: gh's --jq takes a single expression string — it has no + # --arg support like the jq binary, so interpolate $SHA directly + # (safe: it's the output of `git rev-parse HEAD`, not user input). RUN_ID=$(gh run list --workflow=ci.yaml --branch="$BRANCH" \ --event=workflow_dispatch --limit=10 --json databaseId,headSha \ - --jq --arg sha "$SHA" '[.[] | select(.headSha == $sha)] | .[0].databaseId // empty') + --jq "[.[] | select(.headSha == \"${SHA}\")] | .[0].databaseId // empty") [ -n "$RUN_ID" ] && break sleep 3 done From 1673c85ac483ea84329bf1cfc34223eac7297854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20HOUZ=C3=89?= Date: Sat, 19 Sep 2026 15:48:31 +0200 Subject: [PATCH 5/5] Handle empty post-rebase diff, dispatch Docs deploy after main merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review findings: - Handle empty staged diff after rebasing before committing: origin/main may already carry the exact metadata being generated (e.g. a retry after it was applied manually, as happened for v2.0.0), so the post-rebase 'git add' can have nothing staged. Re-check and exit successfully instead of letting 'git commit' fail with nothing to commit. - Dispatch Docs workflow after the GITHUB_TOKEN-authored main merge: that merge is itself GITHUB_TOKEN-authored (via the API), so it does not trigger docs.yaml's own push trigger — the deploy job would never pick up the new snapshot automatically. Explicitly dispatch docs.yaml for main right after merging, and correct the comments that previously claimed the push re-triggers deployment. --- .github/workflows/docs.yaml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e5e85a2..6f42af1 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -22,8 +22,12 @@ on: # - each vX/ snapshot from the gh-pages branch merged in # → single artifact deployed via actions/deploy-pages # • Snapshot job stores the built snapshot in gh-pages branch via plain git, -# then pushes the updated versions.json to main (no [skip ci]) which -# re-triggers the deploy job to include the new snapshot immediately. +# then merges a PR updating versions.json on main. That merge is itself +# GITHUB_TOKEN-authored (via the API), so it does NOT cascade into a new +# `push`-triggered run — the snapshot job explicitly dispatches this +# workflow (workflow_dispatch) for main right after merging, so the new +# snapshot goes live immediately instead of waiting for an unrelated +# docs change. # Requires: Settings → Pages → Source: GitHub Actions. permissions: contents: write @@ -118,8 +122,9 @@ jobs: # push is rejected. Since GITHUB_TOKEN-authored events don't trigger new # workflow runs, ci.yaml is explicitly dispatched (workflow_dispatch is # exempt from that restriction) for this branch/SHA and awaited before - # merging. Once merged (no [skip ci]), the push re-triggers the deploy - # job so the new snapshot goes live. + # merging. The merge itself is also GITHUB_TOKEN-authored, so it won't + # trigger this workflow's own `push` trigger either — this workflow is + # explicitly dispatched for main right after merging instead. # # Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot. # Patch and minor releases update the main docs in-place via the deploy job. @@ -267,7 +272,7 @@ jobs: PY fi - - name: Open PR with versions.json and blog stub, run CI, and merge + - name: Open PR with versions.json and blog stub, run CI, merge, and deploy # main is protected by a ruleset requiring signed commits and passing # status checks — a direct push (previously via # stefanzweifel/git-auto-commit-action) is rejected. Open a PR instead: @@ -280,7 +285,9 @@ jobs: # and auto-merge would wait indefinitely. workflow_dispatch is # exempted from that restriction, so we explicitly dispatch ci.yaml # for this branch/SHA, wait for it, then merge directly once it's - # green — see review on PR #215. + # green — see review on PR #215. The merge push is ALSO + # GITHUB_TOKEN-authored, so this workflow's own `push` trigger won't + # fire either — dispatch it explicitly for main afterwards. env: GH_TOKEN: ${{ github.token }} run: | @@ -304,6 +311,13 @@ jobs: git checkout -B "$BRANCH" origin/main git stash pop git add docs/public/versions.json docs/blog/ + # The pre-rebase guard above doesn't cover this state: origin/main + # may already carry this exact metadata (e.g. a retry after it was + # applied manually), in which case there's nothing left to commit. + if git diff --staged --quiet; then + echo "No changes remaining after rebasing onto origin/main — already up to date, skipping." + exit 0 + fi git commit -m "docs: add ${MAJOR} to versions.json and blog" git push --force origin "$BRANCH" SHA="$(git rev-parse HEAD)" @@ -337,3 +351,9 @@ jobs: gh run watch "$RUN_ID" --exit-status gh pr merge "$BRANCH" --squash + + # This merge is itself GITHUB_TOKEN-authored, so the resulting push + # to main won't trigger docs.yaml's `push` trigger either — dispatch + # it explicitly so the deploy job picks up the new snapshot now, + # instead of waiting for the next unrelated docs change to main. + gh workflow run docs.yaml --ref main