Skip to content

fix(docker-release): export GH_TOKEN so stevedore can cut the GitHub Release - #64

Merged
blairham merged 1 commit into
mainfrom
fix/docker-release-export-gh-token
Aug 22, 2026
Merged

fix(docker-release): export GH_TOKEN so stevedore can cut the GitHub Release#64
blairham merged 1 commit into
mainfrom
fix/docker-release-export-gh-token

Conversation

@blairham

Copy link
Copy Markdown
Contributor

Symptom

docker-release.yml@main builds and pushes the image, then fails the job on the last thing it does:

#41 pushing manifest for .../understudy:0.0.140@sha256:c684954930... 1.4s done
#41 DONE 10.4s
==> advanced release marker refs/releases/image/understudy
==> changelog written to dist/CHANGELOG.md
+ gh release create v0.0.140 --title 'understudy 0.0.140' --notes-file dist/CHANGELOG.md
gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable.
stevedore: gh release create: gh: exit status 4

Failing run: understudy v0.0.140.

Why it's worse than a red check

The failure straddles a completed publish, so the two halves disagree:

  • the image is live — understudy:0.0.140 and :31434f0, pushed 2026-08-22T04:17:19Z, verified via ecr describe-images;
  • the marker ref already advanced, so change detection considers the release done;
  • dist/CHANGELOG.md is generated and then thrown away with the job;
  • the tag gets no GitHub Release, which is the artifact understudy#225 created the tag flow to produce.

Consumers resolving the highest semver tag pick the image up regardless, so "red release run" does not mean "nothing shipped" — the natural reading is wrong. Every v* tag from v0.0.135 through v0.0.140 has had its Release created by hand.

Cause

internal/publish/publish.go shells gh release create and passes no token flaggh authenticates purely from the environment:

args := []string{"release", "create", tag, "--title", title, "--notes-file", notesPath}
if err := r.Run("gh", args...); err != nil {
    return fmt.Errorf("gh release create: %w", err)
}

The stevedore action sets GH_TOKEN only on its Install stevedore step (to download its own binary). Its Run stevedore step declares just SD_COMMAND / SD_ARGS / SD_CONFIG, so nothing carries a token into the process that runs gh. exit status 4 is gh's "no credentials", not a permissions denial.

GH_PRIVATE_TOKEN is a different thing and cannot stand in: it's a minted App token with contents: read, wired to a BuildKit --secret for fetching private modules inside the build.

Fix

One env line on the stevedore step. No new permission and no new secret — the job already declares contents: write for the marker refs, which is exactly what creating a release needs:

GH_TOKEN: ${{ github.token }}

Env set on a uses: step reaches a composite action's own steps. That path isn't assumed — REGISTRY and GH_PRIVATE_TOKEN are set the same way and demonstrably arrive in the same run that failed (the image went to the right ECR host, and the private ppkit fetch succeeded).

Deliberately not fixed here

docker-release-split.yml has the same missing env on Stevedore merge, and a second problem underneath it: Merge() just calls Release(), and the publish guard is if !o.NoPush && !o.Snapshot && !o.SkipPublish — nothing excludes a split leg. The per-platform legs run plain release --split <platform> with no --skip-publish, so each leg reaches publishRelease and would race to create the same tag's Release (gh release create is not idempotent).

Handing the legs a token before fixing that would convert one deterministic failure into a flaky one, so split mode is untouched. It needs --skip-publish on the legs first, then the same env line on merge.

Verification

  • actionlint clean over the whole repo (local 1.7.x; the pinned CI version is 1.7.7).
  • The single-job path is what every current caller uses, so this repo's own actionlint + validate-reusable-inputs jobs cover the change; the input contract is unchanged (no with:/inputs: edit), so no caller can break at parse time.
  • End-to-end proof needs the next v* tag on a caller. Note the first run after merge may hit an already-existing Release for a hand-created tag; that's the non-idempotent gh release create, not a regression from this change.

Refs #63, pinpredict/understudy#225

…Release

The stevedore step passed REGISTRY, STEVEDORE_CACHE_* and GH_PRIVATE_TOKEN
but no GH_TOKEN. stevedore's publish path shells out to `gh release create`
(internal/publish/publish.go) and takes no token flag, so gh found no
credentials and exited 4 — after the image had already been built, pushed and
marker-ref'd.

The failure mode is worse than a red check, because it straddles a completed
publish: on understudy v0.0.140 the run reports failure while
understudy:0.0.140 and :31434f0 are both live in ECR, the generated
dist/CHANGELOG.md is discarded with the job, and the tag ends up with no
Release for a consumer to read. Every v* tag since v0.0.135 has had its
Release created by hand.

`contents: write` was already declared on the job for the marker refs, so
github.token can create the release as-is; no new permission or secret. Env
set on a `uses:` step reaches a composite action's own steps — REGISTRY and
GH_PRIVATE_TOKEN travel that same path today and demonstrably arrive.

Not fixed here: docker-release-split.yml has the same missing env on its
`Stevedore merge` step, and additionally its per-platform legs run plain
`release` (no --skip-publish), which reaches publishRelease too — so each leg
would race to create the same tag's Release. Handing the legs a token without
first skipping publish there would turn one clear failure into a flaky one, so
split mode is left alone and tracked separately.

Refs #63, understudy#225
@blairham
blairham merged commit 07fe434 into main Aug 22, 2026
2 checks passed
@blairham
blairham deleted the fix/docker-release-export-gh-token branch August 22, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant