fix(github): allow deployments:read so PR cards can show preview URLs - #551
Merged
Conversation
Studio's PR card resolves `previewUrl` from four sources; for a VTEX FastStore WebOps repo the preview is published ONLY as a GitHub Deployment, so the only source that finds it is `GET_PREVIEW_DEPLOYMENT` against the Deployments API. That call needs `deployments:read`, which `ALLOWED_PERMISSIONS` did not permit — so `capPermissions` hard-rejected it, Studio's mint ladder shed it, and every repo-scoped token 403'd on the endpoint: Not authorized to read deployments for <owner>/<repo> (403). The token may lack deployments:read. Across production, 0 of 207 repo-scoped connections carry `deployments` (92 carry `checks`, minted as recently as today) — the allowlist, not any single org's installation, is what has been gating it. - Add `deployments` to ALLOWED_PERMISSIONS. - Add READ_ONLY_PERMISSIONS (metadata, checks, deployments): capped to `read` whatever is asked. `checks:write` would let a token post a green check run, and Studio gates PR merges on check status; `deployments:write` would let it write the `environment_url` the PR panel renders as a preview link. - Generalize the refresh path's checks-only widening into an ordered ladder (OPTIONAL_READ_UPGRADES + buildUpgradeLadder). GitHub 422s the WHOLE mint when any requested permission exceeds the installation's grant, so asking for both optionals at once would have 422'd every grant that already had `checks` — and the old code, which only retried when it had just added `checks`, would have gone straight to handleMintFailure and REVOKED those still-valid grants. The ladder sheds one optional at a time and ends at the grant's stored set. - Derive the tool's permissions description from the allowlist; the hand-written list had already gone stale when `checks` was added. - README: document the repository permissions the GitHub App must declare and that each installation must accept the request before it takes effect. Existing grants self-heal on their next ~1h `/repo-grant/token` refresh once their installation approves the permission — no re-import, no re-install. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Unrelated to this branch's github change — `check:registry` has been failing on `main` since #550: `google-analytics-sa/app.json` carries a trailing space in `short_description`, but the `registry.json` committed alongside it does not, so the generator's output has never matched the checked-in artifact. Fixed at the source rather than by regenerating, so the stray space isn't baked into the published registry: with this, `bun run check:registry` is clean and `registry.json` needs no change at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
A Studio task-board card whose PR has a deploy preview shows no preview link. Studio resolves
previewUrlfrom four sources in order — a Workers Builds check-run, a commit-statustarget_url, the deploy bot's PR comment, and finallyGET_PREVIEW_DEPLOYMENT— and for a VTEX FastStore WebOps repo the preview is published only as a GitHub Deployment, so that last source is the only one that can find it.It has been 403ing for every such repo. From production (
deco-studio, all pods, repeating hourly):Root cause is here, in
ALLOWED_PERMISSIONS.deploymentswas not on the mint allowlist, socapPermissionshard-rejected it, Studio's mint ladder (studio#6333) shed it, and every repo-scoped token was minted without it. The affected repo's stored grant is{contents:write, metadata:read, pull_requests:write, issues:write, checks:read}— nodeployments.This is not one org's misconfiguration. Across the whole production DB: 0 of 207 repo-scoped
mcp-githubconnections carrydeployments, while 92 carrychecks(most recently minted the same day).checkspasses the gate anddeploymentsnever does — the allowlist is what has been blocking it.The live tool schema said as much: "Allowed keys: contents, metadata, pull_requests, issues" — already stale, since
checksis in fact accepted.Changes
1.
deploymentsjoinsALLOWED_PERMISSIONS. The one-line fix the rest supports.2. New
READ_ONLY_PERMISSIONS(metadata,checks,deployments) — capped toreadwhatever the caller asks. Neither write is ever needed and both have teeth:checks:writelets a token POST a green check run, and Studio gates PR merges on check status — a forged ship signal.deployments:writelets it write theenvironment_urlthe PR panel then renders as a trusted preview link. Capped rather than rejected, matchingcapPermissions' documented contract and howmetadatahas always been handled: stored grants are re-capped on every refresh, so throwing would turn a legacy over-broad grant into a hard refresh failure instead of quietly narrowing it.3. The refresh path's checks-only widening becomes an ordered ladder (
OPTIONAL_READ_UPGRADES+buildUpgradeLadder).This is the part that needed care. GitHub
422s the whole mint when any requested permission exceeds the installation's grant — it does not partially fulfil. So simply addingdeploymentsto the widened set would have 422'd the refresh for every grant that already hadchecks; and the old code only retried when it had just addedchecks(addedChecks), so those grants would have fallen straight through tohandleMintFailure, which maps 422 →invalid_grant→ revoke. That would have broken working connections, not just failed to fix them.The ladder sheds one optional at a time, newest first, and ends at the grant's stored set:
Rungs a grant already satisfies are deduped away, so a fully-upgraded grant still costs exactly one call. Only a
422advances the ladder — a 5xx/429 outage or a 401/403 from our own App credentials says nothing about the permission set, so it stops immediately rather than multiplying GitHub calls during an outage.Cost: until an installation approves a newer permission, each refresh burns one extra 422'd mint per un-approved optional (~1/hour per connection). That is the deliberate price of picking the permission up automatically the moment an org approves it, instead of requiring every connection to be re-imported.
4. The tool's
permissionsdescription is derived from the allowlist, so it cannot go stale again the way it did whencheckswas added.5. README documents the repository permissions the GitHub App must declare, and that adding one is not self-applying — each installation must accept the request.
Safety
Two guards on
buildUpgradeLadder, since the stored grant is KV data written by past versions of the code:permissions: {}reads as omitted to GitHub (minting every permission the installation holds), andcapPermissions({})returns the defaultcontents:writeset. The caller then reports a transient failure and keeps the grant. Not reachable today (GitHub always echoesmetadataback at issue time)./repo-grant/tokenas a 500 — it would bypass the transient-vs-permanent mapping the whole refresh path is built around. The widened rungs are skipped and the verbatim rung still re-mints the grant.Rollout — this PR alone does not fix anything yet
The mint is gated in three places, and all three must line up:
Deployments: Readin its repository permissions. Until it does, GitHub 422s the request and the ladder shrugs it off.Settings → Applications → <App> → Review request.Until 2 and 3 happen, behaviour is unchanged: the optional is shed and everything keeps working. Once an installation accepts, its existing grants pick
deploymentsup on their next ~1h/repo-grant/tokenrefresh — no re-import, no re-install, no user action. That self-heal is the reason for the ladder rather than a "delete and re-import the connection" runbook.No change is needed on the Studio side: it already requests
deployments:readand already sheds it gracefully.Testing
bun testingithub/— 124 pass, 0 fail.bunx oxlint github/serverclean;tscreports nothing inserver/(the remaining errors are pre-existing@decocms/runtimetype noise innode_modules, present repo-wide and untouched here).New/changed coverage:
capPermissionsacceptsdeployments:read; thetest.eachthat asserteddeploymentswas hard-rejected is inverted (environmentsand the rest stay rejected).checks/deploymentsrequested aswriteare capped toread.OPTIONAL_READ_UPGRADESentry is itself inALLOWED_PERMISSIONS— otherwise rung 0 would throw instead of 422.buildUpgradeLadder: ordering, dedup, the fully-upgraded single-rung case, verbatim-stored-set-as-last-rung, and both safety guards.checksbut notdeploymentskeepschecks(the regression above); a non-422 stops after one attempt and stays 503 without revoking; a 422 on the last rung does revoke.🤖 Generated with Claude Code
Summary by cubic
Adds
deployments:readto the repo-token allowlist so Studio PR cards show deploy-preview URLs for repos that publish previews only as GitHub Deployments (VTEX FastStore WebOps). Previously every repo-scoped token 403'd on the Deployments API because the allowlist rejecteddeployments.Upgrade ladder
checks:readanddeployments:read, shedding un-approved optionals one at a time — GitHub 422s the whole mint when any requested permission exceeds the installation's grant.checksanddeploymentsare capped to read-only, and the tool's permission description is now derived from the allowlist so it cannot go stale again.Rollout
Deployments: Readin repository permissions, and each installation's owner/admin must accept the permission request./repo-grant/tokenrefresh — no re-import, re-install, or user action.Also drops a trailing space in
google-analytics-sa/app.jsonthat was failingcheck:registryon main.Written for commit acd90e7. Summary will update on new commits.