ci: approve release bump PR via a least-privilege GitHub App - #32
Merged
Conversation
The release script previously tried to approve and admin-merge its own version-bump PR with a single identity, which GitHub rejects (self-approval) and the Main ruleset blocks (require_last_push_approval, no bypass actors). Rework trigger_release.sh so a dedicated GitHub App approves the PR: - Admin gate: only a repo admin may run a release (checked via their own gh auth before anything else). - Config (RELEASE_APP_ID / RELEASE_APP_INSTALLATION_ID / RELEASE_APP_KEY_SERVICE) is read from this repo's Actions variables, so nothing is hard-coded and the script is reusable; a same-named env var overrides. - The App only approves (permissions: pull_requests:write + metadata:read) -- it cannot bypass branch protection or merge, so main keeps full protection and no human or token holds a standing override. Its approval *satisfies* the rule. - A short-lived installation token (RS256 JWT via openssl) authenticates ONLY the approval step; create/push/merge run as the human, so the App (approver) differs from the human (last pusher). - Fail-fast: all config is validated and an installation token is preflight- minted BEFORE any irreversible step, so a misconfig can't leave a pushed tag with an unmergeable PR. - New --dry mode runs every check (incl. the App preflight) and prints the plan without changing anything. The PEM is stored raw in the Keychain; the script reads it back tolerating macOS `security -w`, which hex-encodes any secret containing newlines, so the value is used verbatim if it is already a PEM and hex-decoded otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
helly25
force-pushed
the
ci/release-app-approval
branch
from
June 4, 2026 22:33
a773b31 to
3bee431
Compare
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.
Problem
trigger_release.shtried to approve and admin-merge its own version-bump PR with one identity. GitHub rejects self-approval, and themainruleset blocks it anyway:require_last_push_approval: true,required_approving_review_count: 1, andbypass_actors: []/can_bypass: never(so--admincan never override). The result was a pushed release tag plus a stuck, unmergeable PR.Approach — a dedicated GitHub App approves (no bypass)
The App's only power is
pull_requests:write(+metadata:read). It satisfies the required review as a second identity rather than bypassing anything, somainkeeps full protection and no human or token holds a standing override. (Chosen over abypass_actorsentry precisely to avoid any standing override existing.)ghauth first).RELEASE_APP_ID,RELEASE_APP_INSTALLATION_ID,RELEASE_APP_KEY_SERVICEare read from the repo's Actions variables (env overrides), so nothing is hard-coded and the script is reusable.openssl) authenticates only the approval, so approver ≠ last pusher.--dry— runs every check (incl. the App preflight) and prints the plan, changing nothing.security -whex-encodes any secret containing newlines).Verified
./tools/trigger_release.sh --dry 0.4.3reachesApp credential preflight: ok (installation token mints).— token mints, permissions are exactly{"metadata":"read","pull_requests":"write"}, installed onhelly25/bzlonly — then correctly stops at the pre-existing "cleanmain" guard (run from a feature branch).Not yet proven (only observable on a real PR): that the App's approval counts toward the required review. First real release — or a throwaway PR — confirms it; fallback is a
bypass_actorsentry for the App.Test plan
shellcheck+beautyshclean../tools/trigger_release.sh --dry <version>reaches[dry-run] All checks passedfrom a cleanmainafter merge.🤖 Generated with Claude Code