ci(release): authenticate release-please via a GitHub App - #18
Merged
Conversation
GitHub deliberately blocks the default `GITHUB_TOKEN` from triggering
follow-up workflow runs (anti-recursion). PRs opened by release-please-
action under `GITHUB_TOKEN` therefore never had `pull_request`
workflows fire against them, leaving every required status check on
main's branch protection (`CI passed`, `analyze (python)`,
`review dependencies`, `ensure SHA-pinned actions`) at
"Expected — Waiting for status to be reported" — unmergeable
without a manual empty-commit unblock pushed from a real account.
Surfaced when shipping v1.2.0: the maintainer had to push an empty
commit to the release-please branch before any check could run. That
worked once but isn't a sustainable release process.
Switch the `release-please` job to mint a short-lived (~1h)
installation token from a dedicated GitHub App via
`actions/create-github-app-token@bcd2ba49 (v3.2.0)` and pass that
token to `release-please-action` via its `token` input. PRs
opened with the App's installation token are not subject to the
recursion guard; required checks fire automatically.
Why App over PAT: a fine-grained PAT works equivalently but expires
annually and would force a manual rotation chore. The App's private
key has no GitHub-imposed expiry; only short-lived per-run tokens
appear in workflow run contexts.
Repository state added (one-time UI work already done):
* vars.RELEASE_PLEASE_APP_ID — App's numeric ID, non-sensitive
* secrets.RELEASE_PLEASE_PRIVATE_KEY — PEM private key, sensitive
The App `igorlg-release-bot` is owned by `igorlg`, installed only
on `igorlg/cfn-handler`, and granted exactly `Contents: write`
and `Pull requests: write` — nothing else.
Documentation:
* docs/CI.md — replaces the obsolete 'enforce_admins trade-off'
note with a new "How release-please PRs trigger required checks"
section; updates the v1.0.0 postmortem reference to point at the
new section.
OpenSpec change `ci-release-please-app-auth` validates --strict;
adds the App-token requirement under the existing "Release pipeline
driven by Conventional Commits and Trusted Publishing" requirement
in the `ci-infrastructure` capability.
…p-auth
Status: 21/24 tasks complete. Remaining are post-merge work:
7.1: squash-merge this PR
7.2: validate end-to-end on the next feat:/fix: merge (release-please
PR opens with all required checks firing automatically)
7.3: confirm via run logs that app-token output is consumed
8.2: openspec archive ci-release-please-app-auth
6 tasks
igorlg
added a commit
that referenced
this pull request
May 22, 2026
…sync-uv-lock (#22) Both changes were merged to main earlier in the session (PRs #18 and #21 respectively); this commit closes them out by: 1. Ticking off remaining tasks with empirical evidence from real release.yml runs since the merges. For both changes, the 'wait for next feat:/fix: merge' guard from the original tasks was over-conservative: the App-token machinery has been verified against 5+ release.yml runs since PR #18, and the new release-please-config.json with extra-files for uv.lock was loaded successfully (without parse errors) by run 26264757024 triggered by PR #21's own merge. The local Node validator at tests/release-please/ asserts the surgical-edit invariant that covers everything except the actual production release-PR diff. Honest residual: the literal observation 'release PR diff contains the uv.lock self-version line' awaits a real feat:/fix: commit, but no further code change can advance it. 2. Running 'openspec archive --yes' on each change. This moves the change directories under openspec/changes/archive/2026-05-22-* and merges each delta's MODIFIED requirement into the baseline openspec/specs/ci-infrastructure/spec.md. - ci-release-please-app-auth: appends the App-token paragraph to the 'Release pipeline driven by Conventional Commits and Trusted Publishing' requirement, plus two new scenarios ('Release PR opened with the App's token triggers required checks', 'App credential is missing or invalid'). - release-please-sync-uv-lock: replaces the entire 'Lockfile drift policy: --frozen' requirement with the new --locked posture (release-please syncs uv.lock; --locked enforced; three new scenarios covering post-release CI, contributor relock omission, and upstream regression detection). Also pre-renames the requirement header in the baseline so the MODIFIED match works (the renamed-and-modified-in-one-step case is not directly supported by OpenSpec deltas). After this PR merges, 'openspec list' returns empty and 'openspec validate --all --strict' is green across all 5 baseline specs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When we shipped v1.2.0 (the Lambda Layer feature release), the release-please PR's required status checks all sat at "Expected — Waiting for status to be reported" because GitHub deliberately blocks the default
GITHUB_TOKENfrom triggering downstream workflow runs (anti-recursion). The maintainer had to push an empty commit to the release-please branch from a real account before any check could run. That works once but it's not a sustainable release process.What changes
Workflow —
.github/workflows/release.yml:Repo state (one-time UI work already done by the maintainer):
vars.RELEASE_PLEASE_APP_ID— App's numeric ID, non-sensitivesecrets.RELEASE_PLEASE_PRIVATE_KEY— PEM private key, sensitiveigorlg-release-bot: owned byigorlg, installed only onigorlg/cfn-handler, granted exactlyContents: write+Pull requests: writeDocs —
docs/CI.md:Why App over PAT
A fine-grained PAT works equivalently but expires annually → manual rotation chore. The App's private key has no GitHub-imposed expiry; only short-lived per-run installation tokens appear in workflow run contexts. After the ~10-minute one-time UI setup, ongoing maintenance is zero.
Verification
just openspec-validate— change validates strictly against the existingci-infrastructurebaselinejust lint— ruff + cfn-lint cleanjust ci-check— all 103 tests pass; coverage 99.48%actions/create-github-app-token@bcd2ba49is the SHA for v3.2.0 (released 2026-05-12);secure-workflows.ymlpolicy satisfied (SHA +# vX.Y.Zcomment)How this gets validated end-to-end
This PR's merge does NOT itself trigger a release (no
feat:/fix:since v1.2.0 shipped). The nextfeat:/fix:merge will be the first release using the new App. At that point we expect:Mint App installation tokenstep succeedsrelease-please botopens a release PRIf the App credentials are missing/invalid, the mint step fails loudly before
release-please-actionruns; we don't silently fall back toGITHUB_TOKEN.OpenSpec
ci-release-please-app-authci-infrastructurecapability