Skip to content

feat: push the formatter's commit from a GitHub App - #7

Merged
jasonappah merged 5 commits into
mainfrom
app-token-push
Sep 18, 2026
Merged

jasonappah merged 5 commits into
mainfrom
app-token-push

Conversation

@jasonappah

Copy link
Copy Markdown
Member

Follow-up to #5. Makes the auto-format complete without a human clicking Approve and run, and corrects the wording #5 shipped with.

What #6 taught us

The checkbox flow worked end to end on #6 (run 35367718975: write-perm check → resolve branch → checkout → format → commit aa41e97 → rewrite comment → reply; Acknowledge the request skipped, confirming it came from the checkbox rather than /fmt; the follow-up run 35368016195 correctly skipped as the loop guard).

But #5's README was wrong about what happens next. The bot's GITHUB_TOKEN push did create a pull_request run on the formatted commit — run 35367741574, created_at 16:19:01, run_started_at 16:20:12 — it just sat there until it was approved by hand. That's the documented third exception: a workflow that updates a pull request does create opened/synchronize/reopened runs, in an approval-required state. So "the push starts no workflow run, go push an empty commit or reopen the PR" was simply wrong, and e827349 replaces it.

What this adds

lint-fix.yml mints a GitHub App installation token and pushes with that. An app's push isn't held for approval, so the acls check re-runs on the formatted commit by itself.

It's optional and detected at runtime — job-level env reads the secrets, steps branch on it (both context-availability rules checked against the docs):

  • FMT_APP_ID + FMT_APP_PRIVATE_KEY set → app token pushes, run starts immediately, and that run's own report job rewrites the comment.
  • Not set → pushes with GITHUB_TOKEN as before, rewrites the comment itself, and the reply now says the run is waiting for approval and who can clear it.

The app needs contents:write and nothing else; comments and PR reads still go through GITHUB_TOKEN, which keeps the loop guard (sender != github-actions[bot]) correct since the app never comments.

The temporary commit

f9b7aa8 adds .github/workflows/tmp-app-smoke.yml, which just mints a token and prints which repos it can reach. It must come out before merge. It's here because the app's installation isn't confirmed: orgs/Comet-Robotics/installations lists 6 apps (cursor, draw-io-app, gitbook-com, sentry, trylle, vercel) and no formatter, which suggests the app was created but perhaps not installed on the org — and if there's no installation, create-github-app-token fails hard and takes the fmt job with it. The smoke test turns that into a clear yes/no before anything depends on it. It prints no secret.

Not done, deliberately

Auto-approving the held run instead of using an app is not viable: POST /actions/runs/{run_id}/approve is scoped to "a pull request from a public fork of a first time contributor," not to runs held because GITHUB_TOKEN pushed them.

🤖 Generated with Claude Code

jasonappah and others added 3 commits September 18, 2026 11:39
A commit pushed by GITHUB_TOKEN starts no workflow run, so the auto-format left
the acls check stale and asked the author to push again. A GitHub App's push
does start runs, so the fix becomes self-completing.

Configuration is optional and detected at runtime: job-level env reads the
secrets (which `secrets` is available in) and the steps branch on it (which
`env` is available in). With FMT_APP_ID and FMT_APP_PRIVATE_KEY set, the job
mints a 1h installation token and checks out with it; without them it pushes
with GITHUB_TOKEN exactly as before and keeps telling the author how to get a
fresh run. This can therefore merge before the app exists.

The app only needs contents:write -- comments and PR reads still go through
GITHUB_TOKEN, which also keeps the loop guard (sender != github-actions[bot])
correct, since the app never comments.

On the app path the "Rewrite the lint report" step is skipped: the push triggers
a real run whose own report job rewrites the comment, and doing it here too
would race that run.

README carries the setup steps and notes that the private key is the one durable
credential in the repo, scoped to contents:write on this repo alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Confirms the app can mint an installation token for this repo before
lint-fix.yml relies on it. Delete once confirmed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observed on #6: the bot's GITHUB_TOKEN push did create a pull_request run on the
formatted commit (35367741574), but it sat from 16:19:01 to 16:20:12 until it was
approved by hand. That is the documented third exception -- a workflow that
updates a pull request does create opened/synchronize/reopened runs, in an
approval-required state -- so the previous wording, that the push starts no run
at all and the author should push an empty commit or reopen the PR, was wrong.

The fallback reply now says the run is waiting for approval and who can clear
it. The note that re-running the old check doesn't help stays, since a re-run
still reuses the original unformatted commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jasonappah
jasonappah requested a review from a team as a code owner September 18, 2026 16:40
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

✅ Policy lint passed

policy.hujson parses and every group member is on its own line.

Lint output
policy.hujson: valid, formatting OK

98a5e54 · run log

The smoke test on this branch returned 404 from
get-a-repository-installation-for-the-authenticated-app: the app id and key are
valid (a bad key gives 401) but the app has no installation on this repo. With
the mint step required, that would have taken the whole fmt job down and broken
/fmt entirely -- worse than the approval click it was meant to remove.

The mint step is now continue-on-error, and the steps after it branch on the
token actually obtained rather than on the secrets being configured, so a
missing installation or a rotated-out key degrades to the GITHUB_TOKEN path
instead of failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jasonappah

Copy link
Copy Markdown
Member Author

The smoke test did its job — it failed, and usefully.

POST /app/installations/.../access_tokens -> 404 Not Found
documentation_url: .../apps#get-a-repository-installation-for-the-authenticated-app

A bad key or wrong app id returns 401; this is 404, which means the JWT was accepted and the app simply has no installation on this repository. So FMT_APP_ID and FMT_APP_PRIVATE_KEY are correct, and what's missing is step 6 of the README list — Install the app and grant it tailscale-acls. That matches orgs/Comet-Robotics/installations, which lists 6 apps and no formatter.

That failure also exposed a worse problem than the one this PR set out to fix, now addressed in 4a846e9: with the mint step required, a missing installation took the whole fmt job down, so /fmt would have broken completely — strictly worse than the approval click. The mint step is now continue-on-error, and the steps after it branch on the token actually obtained rather than on the secrets being present. A missing installation, or a key rotated out from under it, now degrades to the GITHUB_TOKEN path with the approval note, instead of failing.

Once the app is installed, the smoke test should print the repo it can reach. Then f9b7aa8 and .github/workflows/tmp-app-smoke.yml come out and this is ready to merge.

On approving the held run programmatically instead: not viable. POST /actions/runs/{run_id}/approve is documented as scoped to "a pull request from a public fork of a first time contributor," not to runs held because GITHUB_TOKEN pushed them. With the app installed there's nothing to approve anyway — its push isn't held.

Confirmed on rerun 35370080108 after the app was installed: installation
162802390, token scoped to Comet-Robotics/tailscale-acls alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jasonappah

Copy link
Copy Markdown
Member Author

App is installed and the token works. Rerun 35370080108:

installation-id: 162802390
{"repos":["Comet-Robotics/tailscale-acls"]}

create-github-app-token@v2 scopes the token to the current repository by default, so even though the installation itself is org-wide the token only reaches this repo. (The all-false permissions block in that log is expected — that field reports a user's permissions and an installation token isn't a user.)

Smoke test removed in 98a5e54. This PR is now just the two files, and acls + report are green on it. The CANCELLED pair above them is the superseded run from the previous commit, which is cancel-in-progress behaving.

Two hardening suggestions, neither blocking

tailscale-acl-checks (app_id 4992067) is installed more broadly than this needs:

  • repository_selection: all. The app can mint a token for any repo in the org. Narrowing it to Only select repositories → tailscale-acls means a leaked private key is confined to this one repo rather than all of them.
  • Permissions are actions:write, contents:write, pull_requests:write, metadata:read. The workflow only needs contents:write — comments and PR reads deliberately go through GITHUB_TOKEN, which is also what keeps the loop guard (sender != github-actions[bot]) correct. actions:write is worth dropping in particular: it would let a token minted from this key re-run and cancel workflows, including the acls check that gates the tailnet.

Both are changed in the app's settings; GitHub may ask you to accept the change on the installation. Worth doing since the private key is now the one durable credential in a repo that gates tailnet access, but the design works either way.

Still unverified

The smoke test proved the token can be minted. It didn't prove the app-token push produces a run that isn't held for approval — that's the whole point of the change, and it can only be tested once lint-fix.yml is on main. Happy to run that test right after merge: break the formatting on a throwaway PR, tick the box, and confirm acls goes green with no Approve and run click.

(Unrelated log noise: create-github-app-token@v2 targets Node 20 and the runner forces Node 24. Warning only, nothing to do.)

@jasonappah
jasonappah merged commit 10c1d85 into main Sep 18, 2026
2 of 4 checks passed
@jasonappah
jasonappah deleted the app-token-push branch September 18, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant