State the Checks API gap instead of prescribing an impossible tick

Fine-grained tokens have no Checks permission. GitHub's permission reference
lists no Checks section and no check-run endpoint, and checks is absent from
the token form's pre-fill parameters, so the earlier instruction to tick
Checks: Read asked for a box that does not exist. A token created with every
listed permission still could not read check runs, which is what surfaced this.

The prompt now states the consequence rather than offering a remedy: gh pr
checks reports commit statuses only and gh run view returns no annotations.
Both degrade to empty output rather than a permission error, so without the
note they read as a broken CI integration. Job logs are unaffected; they fall
under Actions, which is granted.

secret_scanning_alerts and vulnerability_alerts move into the pre-filled URL,
leaving nothing for the operator to tick beyond repository selection.
This commit is contained in:
2026-07-31 08:48:26 -05:00
parent b8bf9f9eff
commit 5e167d3a0d
3 changed files with 47 additions and 26 deletions
+23 -8
View File
@@ -186,15 +186,30 @@ workflows=write actions=write statuses=read security_events=write
secret_scanning_alerts=read vulnerability_alerts=read secret_scanning_alerts=read vulnerability_alerts=read
``` ```
Two things the form cannot pre-fill, so the task prints them as a checklist: One thing the form cannot pre-fill: **Repository access → Only select repositories
→ `api-portal`.** GitHub has no query parameter for repository selection.
1. **Repository access → Only select repositories → `api-portal`.** GitHub has no ### The Checks API is out of reach
query parameter for repository selection.
2. **Checks: Read.** The only permission the agent needs that is absent from Fine-grained tokens cannot read check runs. This is not a permission you forgot to
GitHub's pre-fill parameter list. Without it `gh pr checks` reports no status grant — GitHub's permission reference has no Checks section and lists no check-run
rollup and `gh run view` returns no annotations — both fail as empty results endpoint, so there is no box to tick. Inside the sandbox:
rather than as permission errors, so a missed tick is easy to misread as a
broken CI integration. | Command | Behaviour |
| --- | --- |
| `gh pr checks` | shows commit statuses only, not check runs |
| `gh run view` | returns no annotations |
| `gh run view --log` | works — job logs fall under Actions |
Both degrade to empty output rather than a permission error, so they read as a broken
CI integration unless you know why. A 403 names what it wanted in the
`X-Accepted-GitHub-Permissions` response header.
Only an installation token from a GitHub App can reach the Checks API. That was
evaluated and rejected for this workflow: minting one requires the App private key on
every developer's machine, and device-flow user tokens — the alternative that needs no
key — were measured and do **not** honour `repository_id`, so they reach every
repository in the installation.
Generate the token and paste it at the prompt. It is read with the terminal echo off Generate the token and paste it at the prompt. It is read with the terminal echo off
and piped straight into the `sbx` secret store, so it never reaches your shell history. and piped straight into the `sbx` secret store, so it never reaches your shell history.
+17 -14
View File
@@ -40,13 +40,13 @@ TOKEN_URL_PERMISSIONS=(
vulnerability_alerts=read vulnerability_alerts=read
) )
# "checks" is the one permission the agent needs that GitHub omits from the # Fine-grained tokens cannot reach the Checks API at all: GitHub's permission
# pre-fill parameters, so it has to be ticked by hand. Each entry names what # reference has no Checks section and lists no check-run endpoint, so there is
# breaks without it, because an unticked box fails later as an empty result or # no box to tick. Both calls below degrade to empty output rather than a
# a 403 rather than as a permission error. # permission error, which reads as broken CI unless it is called out.
TOKEN_MANUAL_PERMISSIONS=( TOKEN_LIMITATIONS=(
"Checks: Read - without it 'gh pr checks' reports no status rollup and" "gh pr checks shows only commit statuses, not check runs"
" 'gh run view' returns no annotations" "gh run view returns no annotations"
) )
usage() { usage() {
@@ -205,17 +205,20 @@ Create a fine-grained token for $REPOSITORY.
Everything except the repository is pre-filled. On the page: Everything except the repository is pre-filled. On the page:
1. Repository access -> Only select repositories -> ${REPOSITORY#*/} 1. Repository access -> Only select repositories -> ${REPOSITORY#*/}
2. Under Permissions -> Repository permissions, tick the one the form 2. Generate token, then paste it below.
cannot pre-fill:
Every permission is pre-filled. Fine-grained tokens cannot read check runs,
so inside the sandbox:
EOF EOF
local permission local limitation
for permission in "${TOKEN_MANUAL_PERMISSIONS[@]}"; do for limitation in "${TOKEN_LIMITATIONS[@]}"; do
printf ' %s\n' "$permission" >&2 printf ' %s\n' "$limitation" >&2
done done
cat >&2 <<EOF cat >&2 <<'EOF'
3. Generate token, then paste it below.
A 403 will name what it wanted in the X-Accepted-GitHub-Permissions header.
EOF EOF
+7 -4
View File
@@ -69,11 +69,14 @@ for expected in secret_scanning_alerts=read vulnerability_alerts=read statuses=r
fail "permission dropped out of the pre-filled URL: $expected" fail "permission dropped out of the pre-filled URL: $expected"
done done
((${#TOKEN_MANUAL_PERMISSIONS[@]})) || ((${#TOKEN_LIMITATIONS[@]})) ||
fail "the manual checklist is empty; checks is not pre-fillable and must be listed" fail "the limitations list is empty; the Checks gap must be stated"
printf '%s\n' "${TOKEN_MANUAL_PERMISSIONS[@]}" | grep -q 'Checks' || printf '%s\n' "${TOKEN_LIMITATIONS[@]}" | grep -q 'gh pr checks' ||
fail "the manual checklist must name Checks" fail "the limitations must name gh pr checks"
printf '%s\n' "${TOKEN_LIMITATIONS[@]}" | grep -qi 'tick\|check the box' &&
fail "the limitations must not imply Checks can be granted"
if ((failures)); then if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2 printf '%d assertion(s) failed\n' "$failures" >&2