feat(app): a GitHub App that reviews pull requests, forks included - #209
feat(app): a GitHub App that reviews pull requests, forks included#209anirudhkumar-nanonets wants to merge 5 commits into
Conversation
A workflow on a fork's PR gets a read-only token and cannot comment; an App's installation token belongs to the base repo, so a fork is ordinary work. It also serves the viewer page itself behind a signed expiring link, which is what a private repo needs and GitHub Pages cannot give. node:http and node:crypto only — this process holds a private key, a webhook secret and other people's source, so every dependency is one more thing to trust. The PR's code is never executed: no install, no build, no hooks, just tree-sitter reading source text.
🌱 graft blast radius1 area changed → 2 areas can be affected. 5 dependent symbols, depth 2. flowchart TB
A0(("Code Graph Loading<br/>3 symbols"))
A1(("Telemetry Flushing<br/>2 symbols"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1 reached;
All 5 dependent symbols, grouped by areaCode Graph Loading — 3 symbols in 2 files
Telemetry Flushing — 2 symbols in 2 files
Test signal per changed area — 1 ✓Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
2 test suites also reference this code2 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
package.json runs scripts/postinstall.mjs on install, and copying only the manifests left node unable to find it — it exits 1 before the script's own "never fail an install" guard runs. --ignore-scripts is not the way out: tree-sitter builds its native bindings in those same hooks.
npm ci runs this package's prepare script, which is the build, so the sources have to be there before the install — not after. And the runtime stage cannot reinstall: --omit=dev reruns prepare without tsc, --ignore-scripts skips the native builds tree-sitter needs. It carries the compiled node_modules over and prunes in place instead.
21 of our 30 open PRs come from forks, and not one of them can get a blast-radius comment: a
pull_requestjob on a fork gets a read-only token, andpull_request_targetcannot check out fork code without opting into running it. No amount of workflow YAML fixes that. Every competitor in this space — CodeRabbit, Qodo, Greptile, Graphite — ships as a GitHub App for exactly this reason.An App's installation token belongs to the base repository, so a fork PR is ordinary work. Two things come free with it: the App serves the viewer page itself, so a private repo never needs a public
gh-pages(Pages on a private repo is Enterprise-only), and installing is one click instead of a workflow file per repo.What's here
src/app/— around 600 lines, no new dependencies.node:httpandnode:cryptoonly: this process holds a private key, a webhook secret and other people's source code, so every dependency is one more thing to trust.ready_for_review; a bot commenting on every push to a draft is the fastest way to be uninstalled.refs/pull/<n>/mergeand the base, shallow.buildGraph→blastRadiusIn→markdownReport→ upsert the comment by marker, all in-process against the same functions the CLI calls, so the App andgraft blaston a laptop cannot drift./p/<id>?t=<hmac>.The security rule that shapes it
We clone attacker-authored code on every fork PR while holding a token for the base repo. So: nothing from the repo is executed — no install, no build, no hooks; the graph is tree-sitter reading source text. Git is told the same (
core.hooksPath=/dev/null,GIT_CONFIG_NOSYSTEM=1, no submodule recursion). The token is passed as a per-invocation auth header, never baked into a remote URL that.git/configand the reflog would keep — asserted in a test — and it is redacted out of error text before anything is logged. An unknown page and a bad token both return404, so the page endpoint cannot be used to enumerate pull requests.Tests
779 pass. The interesting ones need no network and no credentials: the JWT is verified against a generated public key, the checkout test stands up a local
--mirrorwith a realrefs/pull/7/mergeand asserts we land on the merge commit with the base available to diff, and the server tests drive real sockets with signed and forged deliveries.Not built yet (documented in
docs/github-app.md)Naming falls back to hub symbols — sending a private repo's source to a model should be an explicit per-installation opt-in, not a default. Pages are in-memory, so a deploy drops them and the next push rebuilds. The comment's evidence quotes need #180.
Installing still requires admin on the target repo — that is the one thing an App does not get around, so this does not help
assign.