Skip to content

Commit 574a0c0

Browse files
bloveclaude
andauthored
docs(contributing): document the merge gate and the never-arriving-check trap (#941)
* docs(contributing): document the merge gate and the never-arriving-check trap On 2026-09-01 Vercel consolidated its commit-status contexts from one per project (`Vercel – threadplane`, `Vercel – threadplane-minting-service`) to a single `Vercel`. Branch protection required the literal old string, so the required check stopped arriving and every PR became permanently unmergeable while showing all checks green — `MERGEABLE` + `BLOCKED`, indistinguishable from a slow queue. That cost a lot of confused debugging, and the diagnostic is one API call once you know to make it. Adds a "The merge gate" section covering: - what the single required check is (`CI — required`, the `required-pr-checks` job, app-pinned to 15368, strict), and that `approve` must never join it; - why the gate is a job we own rather than a vendor status string a third party can rename out from under us; - the diagnostic — diff the contexts posted on the head SHA against the contexts protection requires — including the check-runs/statuses API split that makes `CI — required` absent from `/status`; - the pre-2026-09-01 protection config as a rollback reference, since branch protection is not version-controlled and needs an admin token to read. Deliberately no CI guard asserting protection matches the file: reading branch protection needs `administration: read`, which the workflow `permissions:` key cannot grant, so it would require a long-lived admin PAT in Actions secrets. That credential is a worse risk than the drift it catches. Reasoning is recorded in the section so the next person doesn't re-derive it. Verified against the API while writing: f927124 carries the two old contexts (minting-service failing), a8a603f carries the single `Vercel`, and `CI — required` is a check run from app 15368. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(contributing): record why the Vercel status stays non-required Reviewed 2026-09-01. States the residual gap plainly — a Vercel-environment- specific failure is not caught pre-merge, only post-merge by `Deploy → Vercel` — and why requiring the status back was rejected: the consolidated `Vercel` context now covers every project, so it would let an unrelated project block a pure-website PR, reversing the #931 behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 407c995 commit 574a0c0

1 file changed

Lines changed: 109 additions & 0 deletions

File tree

CONTRIBUTING.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,115 @@ Then add the same public key as a **Signing Key** at
8686
<https://github.com/settings/ssh/new>. Commits merged through the GitHub UI and
8787
bot commits (Renovate, Dependabot) are signed automatically.
8888

89+
## The merge gate
90+
91+
`main` has exactly one required status check: **`CI — required`**. That is the
92+
`required-pr-checks` job in `.github/workflows/ci.yml`, app-pinned to the
93+
GitHub Actions app (id `15368`), with `strict: true` (a PR must be up to date
94+
with `main` before it merges).
95+
96+
Nothing else is required. In particular:
97+
98+
- **`Vercel` is not required**, and that is a deliberate decision, reviewed on
99+
2026-09-01. The `website` job's last step is `npx nx build website`,
100+
byte-identical to `vercel.json`'s `buildCommand`, so a broken build still
101+
fails `CI — required`. The residual gap is real but narrow: a failure
102+
specific to Vercel's environment — an env var set there and not in CI, an
103+
install difference — is not caught pre-merge. It is caught post-merge by
104+
`Deploy → Vercel`, before production.
105+
106+
Requiring the status back was considered and rejected. Vercel now posts a
107+
single consolidated `Vercel` context covering every project, where it used to
108+
post one per project. Gating on it would mean an unrelated project's failure
109+
blocks a pure-website PR — the opposite of the current behaviour, which is
110+
load-bearing: #931 merged on 2026-09-01 while `threadplane-minting-service`
111+
was failing on that same commit, deliberately not allowed to block website
112+
work. It would also re-introduce the vendor-string dependency described
113+
below.
114+
- **`approve` is not a quality gate and must never become one.** It comes from
115+
`.github/workflows/auto-approve.yml` and exists only so OSSF Scorecard's
116+
Code-Review check has a review to read (see [Code review](#code-review)).
117+
118+
### Why the gate is a job we own, not a vendor status string
119+
120+
On 2026-09-01 Vercel consolidated its GitHub commit-status contexts. It had
121+
been posting one per project — `Vercel – threadplane` and
122+
`Vercel – threadplane-minting-service` — and from ~17:19 UTC it posted a single
123+
`Vercel`. Branch protection required the literal string `Vercel – threadplane`,
124+
so the required check simply stopped arriving and **every PR became
125+
permanently unmergeable while showing all checks green**. Same publisher
126+
(`vercel[bot]`, app id `8329`), so it was a platform-side rename, not a
127+
misconfiguration on our end.
128+
129+
A required context is matched by name. Any vendor free to rename its context is
130+
free to strand every PR in the repo, silently and indefinitely. `CI — required`
131+
is a job in this repository, so its name can only change in a commit.
132+
133+
It is safe to gate on because it is not vacuous: it runs
134+
`if: always() && github.event_name == 'pull_request'` (so it posts on every PR,
135+
fork PRs included), it fails when any in-scope job is not `success`, and it
136+
also fails when an out-of-scope job reports `failure` or `cancelled`. Read the
137+
job before changing it.
138+
139+
### Diagnostic: a PR is green but will not merge
140+
141+
`mergeable: MERGEABLE` together with `mergeStateStatus: BLOCKED`, on a PR whose
142+
checks are all green, means a **required context never arrived**. This looks
143+
identical to a slow queue and it never resolves on its own. Do not wait it out,
144+
and do not reach for `--admin`.
145+
146+
Compare what was actually posted on the head SHA against what protection
147+
requires:
148+
149+
```bash
150+
gh api repos/cacheplane/angular-agent-framework/commits/<head-sha>/status --jq '[.statuses[].context]'
151+
```
152+
153+
```bash
154+
gh api repos/cacheplane/angular-agent-framework/branches/main/protection --jq '.required_status_checks.checks'
155+
```
156+
157+
Any required context missing from the posted list is the cause. Fix branch
158+
protection (or the workflow that should post it) — forcing the merge only hides
159+
the next one.
160+
161+
Note that check *runs* (GitHub Actions) and commit *statuses* (external apps
162+
like Vercel) are different APIs. `CI — required` is a check run, so it appears
163+
in `gh pr checks` but not in the `/status` output above; use
164+
`gh api repos/cacheplane/angular-agent-framework/commits/<head-sha>/check-runs`
165+
for those.
166+
167+
### Rollback reference
168+
169+
Branch protection is repo-wide, not version-controlled, and reachable only with
170+
an admin-scoped token. The configuration in force before the 2026-09-01 change,
171+
recorded here so it can be restored without guessing:
172+
173+
```jsonc
174+
// required_status_checks, as of 2026-09-01 before the fix
175+
{
176+
"strict": true,
177+
"contexts": ["Vercel – threadplane"],
178+
"checks": [{ "context": "Vercel – threadplane", "app_id": 8329 }]
179+
}
180+
```
181+
182+
Everything else was, and remains, unchanged: `required_signatures.enabled:
183+
true`, `enforce_admins.enabled: false`, `required_approving_review_count: 0`,
184+
`allow_force_pushes: false`, `allow_deletions: false`, no rulesets. Only
185+
`required_status_checks` was edited, to
186+
`{"strict": true, "checks": [{"context": "CI — required", "app_id": 15368}]}`.
187+
188+
There is deliberately **no CI assertion that protection matches this file**.
189+
Reading branch protection needs the `administration: read` scope, which the
190+
workflow `permissions:` key cannot grant — `GITHUB_TOKEN` has no such scope —
191+
so a guard would require a long-lived admin PAT in Actions secrets. The
192+
blast radius of that credential is worse than the drift it would catch, given
193+
the drift is loud once you know the diagnostic above. The repo uses classic
194+
branch protection and has no rulesets, so the unprivileged
195+
`GET /repos/{owner}/{repo}/rules/branches/main` endpoint returns `[]` and is
196+
not an alternative.
197+
89198
## Code review
90199

91200
Every PR gets a genuine advisory AI code review

0 commit comments

Comments
 (0)