From aff5e108876c70743c01cbca7a632f540e42f1d6 Mon Sep 17 00:00:00 2001 From: Chris Shuttlesworth Date: Tue, 4 Aug 2026 12:28:49 -0400 Subject: [PATCH] fix: tag the released ref, and record image provenance in the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.0 claimed the git-data API sidesteps GitHub's workflow-file protection. It does not. Creating a ref at a commit whose .github/workflows differ from the default branch fails the same way over the API as over git push, reported as "Resource not accessible by integration" — the constraint is the token, not the transport, and GITHUB_TOKEN cannot hold the `workflow` scope. That condition is routine, not exotic: any CI change after the last image build triggers it, which is exactly when a release tends to happen. VirtualWindow hit it on both attempts (its build commit predates a ggshield pin and the release workflow itself). So stop encoding provenance in where the tag points. The tag marks the release point on the ref; the tag message and the release body name the build commit of every image. "Which commit is this version?" is still answerable — from the release rather than from the ref — and no repo needs a long-lived PAT to cut one. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release-image.yml | 43 ++++++++++++++++++++--------- README.md | 17 +++++++++--- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 732445c..767a91c 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -261,6 +261,7 @@ jobs: fi echo " $SOURCE_TAG = $digest" + release_commit="" if [ "$VERIFY" = "true" ]; then # WHICH COMMIT IS THIS IMAGE? Not necessarily HEAD. Build workflows # carry paths filters, so a docs- or CI-only commit moves the branch @@ -329,7 +330,12 @@ jobs: echo " tagged $IMAGE_TAG" [ -n "$first_digest" ] || first_digest="$digest" - printf -- '- `%s:%s` — `%s`\n' "$img" "$IMAGE_TAG" "$digest" >> /tmp/released-images.md + if [ -n "$release_commit" ]; then + printf -- '- `%s:%s` — `%s`, built from `%s`\n' \ + "$img" "$IMAGE_TAG" "$digest" "$(printf '%s' "$release_commit" | cut -c1-7)" >> /tmp/released-images.md + else + printf -- '- `%s:%s` — `%s`\n' "$img" "$IMAGE_TAG" "$digest" >> /tmp/released-images.md + fi done [ -n "$RELEASE_COMMIT" ] || RELEASE_COMMIT="$SHA" @@ -342,15 +348,22 @@ jobs: echo 'RELEASE_IMAGES_EOF' } >> "$GITHUB_OUTPUT" - # Created through the git-data API, NOT `git push`. A token-authenticated - # push is rejected whenever the ref being pushed carries a workflow file - # that differs from the default branch's — "refusing to allow a GitHub App - # to create or update workflow ... without `workflows` permission` — and - # GITHUB_TOKEN cannot hold that scope at all. That happens routinely here: - # the tag lands on the commit the image came from, and any later CI change - # makes its .github/workflows differ from main. The API creates a tag - # object pointing at an existing commit, introducing no file changes, so - # the check does not apply. + # TAGS THE REF THIS RAN ON, not the commit the image was built from. + # + # Tagging the build commit reads better — a version naming code actually in + # the image — and it is what this workflow did until it met GitHub's + # workflow-file protection: a GITHUB_TOKEN cannot create a ref pointing at + # a commit whose .github/workflows differ from the default branch's. That + # is not a push-vs-API distinction (the API returns the same refusal as + # "Resource not accessible by integration"); it is the token. And the + # condition is routine — any CI change after the last image build triggers + # it, which is precisely when a release is most likely. + # + # The alternative was a long-lived PAT carrying the `workflow` scope in + # every releasing repo. Not worth it: provenance does not need to live in + # the ref. The build commit of every image is recorded in the tag message + # and in the release body, so "which commit is this version?" is still + # answerable — from the release, rather than from where the tag points. - name: create the annotated tag uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: @@ -360,7 +373,8 @@ jobs: with: script: | const tag = process.env.GIT_TAG; - const sha = process.env.RELEASE_COMMIT; + const sha = context.sha; // the ref being released + const builtFrom = process.env.RELEASE_COMMIT; // newest image build commit const behind = process.env.BEHIND; // Annotated, not lightweight: `git show ` should answer who cut // this and when without a round trip to the API. @@ -369,8 +383,11 @@ jobs: repo: context.repo.repo, tag, message: `${tag}\n\nReleased by ${context.actor} via the release-image workflow. ` - + `Promoted the image digest built from ${sha} — no rebuild. ` - + `(${behind} commit(s) on ${context.ref.replace('refs/heads/', '')} after it changed no image inputs.)`, + + `Promoted existing image digests — no rebuild. ` + + `Newest image built from ${builtFrom}` + + (Number(behind) > 0 + ? `, ${behind} commit(s) behind this ref (they changed no image inputs).` + : ` (this ref).`), object: sha, type: 'commit', }); diff --git a/README.md b/README.md index d8dceac..98fe951 100644 --- a/README.md +++ b/README.md @@ -214,10 +214,19 @@ jobs: the build minutes. `images` takes several images that version together (an app and its sidecar are one release, not two). - **Refuses to release a mismatched build.** Unless `verify-source-commit` is - false, the source tag's digest must also be reachable under a commit-sha tag - for the running ref, so a build that is still in flight — or that failed after - the merge — cannot be released by accident. Repos tag per-commit differently, - so ``, the 7-char short sha and `sha-` are all tried. + false, each image's digest must be reachable under a commit-sha tag somewhere + in the running ref's recent history, so a build still in flight — or one that + failed after the merge, or came from another branch — cannot be released by + accident. Repos tag per-commit differently, so ``, the 7-char short sha + and `sha-` are all tried. +- **The tag lands on the ref, and provenance is recorded rather than implied.** + Tagging each image's own build commit reads better, but a `GITHUB_TOKEN` + cannot create a ref pointing at a commit whose `.github/workflows` differ from + the default branch's — the API refuses it exactly as a push does. Any CI change + after the last build triggers that, which is when you are most likely to be + releasing. Rather than require a PAT with the `workflow` scope in every repo, + the tag marks the release point and the **tag message and release body name + the build commit of every image**. - **Refuses to reuse a tag**, checked against the remote rather than the local clone. Released tags are immutable; supersede with a patch instead. - **A dispatch button, because the version is a human decision** — a major means