feat(ci): label pull requests by reading effort - #3949
Conversation
The pull request list carries no labels at all, so finding the work that actually needs a reviewer means opening pull requests one at a time. At the current rate that scan is repeated over more than a hundred open pull requests, and two facts it depends on are not visible from the list at all: which stalled runs are waiting on a maintainer rather than on the author, and which approvals have already been retired by a later push. Project each pull request onto two independent axes instead. Status answers whose move it is. `waiting-infra` covers a run held by the fork approval gate or never created, which the contributor cannot clear; the gate outranks a sibling failure because no amount of fixing produces a complete verdict while a run is held. `needs-author` covers a conclusive failure or a standing request for changes. `review-ready` is green and unapproved, and is the only queue a reviewer needs to read. `merge-ready` additionally carries a committer approval on the current head. Effort answers how much reading a pull request is, on tiers that map to the only two decisions a reviewer makes about size: whether it can be cleared between other work, and whether it is too large to read as one unit. Lockfiles, regenerated artifacts and binaries are excluded from the count, since a pull request that touches one would otherwise land several tiers above what it asks anyone to read. Test code is not discounted; it is reviewed too. Approvals and requests for changes are treated asymmetrically on purpose. An approval binds to the head it was given on, so a push retires it even where stale-review dismissal is off. A request for changes survives a push, or pushing anything at all would clear the block. The labels are a projection and nothing more. Branch protection remains the only merge authority, and no label here grants or withholds it. A draft is left unlabelled rather than having a check result overwrite the author's own statement that it is not ready. `scripts/pr-triage-dryrun.mjs` reports the same classification over live pull requests without writing anything, so a rule change is reviewed against real data before it reaches anyone's pull request. Generated-by: Claude Code
Reviewer attention is the binding constraint here, and nothing in the pull request list says how much reading each entry asks for. Sorting that out means opening pull requests one at a time, over a set that turns over roughly fifty times a day. Label the one axis a search cannot express. GitHub already indexes review, check and draft state, so `review:required status:success` answers "whose move is it" for free and stays correct without anything keeping a copy fresh. There is no size qualifier, so effort has to be computed and stored. Five tiers, on inclusive upper bounds of readable lines: XS 10, S 100, M 500, L 1000, XL above. Every tier is populated on the current open set (5/26/41/21/49), so none of them is decoration. Readable lines exclude what nobody reads line by line: lockfiles including Cargo.lock, both spellings the notice generator emits, regenerated sources, snapshots and binaries. Without that a Rust dependency bump reads as XL on the strength of 3524 rewritten lock lines. Test code is not discounted; it is reviewed too. Labelling happens when a pull request first asks for review, then once a day over the whole open set. A push can change the tier, but subscribing to every push would run this hundreds of times a day to correct a label nobody is misled by in the meantime; the daily sweep absorbs that drift and doubles as the recovery path for any missed event. The checkout ref is pinned to the trusted default-branch commit rather than left to the event default, since this job imports the checked-out script and runs with a writable token. `scripts/pr-effort-dryrun.mjs` reports the same tiers over live pull requests without writing anything, so a boundary or exclusion change is reviewed against real data first. Generated-by: Claude Code
|
This started out labelling review state as well, and that axis has been removed rather than fixed — the branch name predates the change. Native search already covers it ( What is left is the one axis a search cannot express: reading effort, five tiers on readable lines with lockfiles, generated files and binaries excluded. Diff is +420/-777. Review focus, if it helps: the tier boundaries and the exclusion list in |
The CI planner test requires every workflow carrying a pull request trigger to be listed explicitly, so adding pr-effort-label.yml without the entry failed the `test` job rather than the workflow itself. Generated-by: Claude Code
A second script existed only to print what the labelling sweep would do, duplicating the sweep to do it. The workflow now takes a dry_run dispatch input and logs the plan instead of writing it, so the rehearsal exercises the same code path that runs in production rather than a copy that can drift. planLabels becomes the module's only export for the same reason: the tests now bind to the contract the workflow calls, not to helpers that would exist only to be tested. Generated-by: Claude Code
jackwener
left a comment
There was a problem hiding this comment.
Review of exact head 43770a536c58b645980b0e60fa82c015671f88fc.
I am not approving this head yet. Required test is still queued. This is an Astro-Han feature pull request; I will not merge it.
The gap is real: pull request search can already answer review, check, and draft state, and it has no size qualifier. Labelling only reading effort, and dropping the earlier review-state axis, is the smallest thing that adds a signal GitHub does not already index.
The token story is the load-bearing part. pull_request_target plus pull-requests: write is safe here because the job checks out ${{ github.sha }} (the base-branch commit for this event), sets persist-credentials: false, and imports scripts/pr-effort.mjs from that checkout. It classifies the pull request through pulls.listFiles, so it never runs the pull request's tree. I ran the 13 classifier tests locally; they pass.
I did not find an open P0–P2. I will approve only if test is status=completed and conclusion=success on this same SHA. The five effort/* labels do not exist on the repository yet; they need to be created before the first live run or GitHub will mint them with default colours, as the pull request already says.
Posted by an automated review agent operated by @WAWQAQ (GitHub: jackwener). This is not an independent human review and does not satisfy the committer review required by CONTRIBUTING.md. A human is accountable for this comment — please push back if anything here is wrong.
简体中文
这个 head 我还不能 approve:必选 test 仍在排队。代码侧没有未关闭的 P0–P2。pull_request_target 只 checkout 默认分支的 github.sha,不会跑 PR 分支的代码。五个 effort/* 标签仓库里还不存在,合入前需要先建好。Astro-Han 的功能 PR,我不会合。
本条评论由 @WAWQAQ 运行的自动化审查程序发出(GitHub: jackwener)。它不构成 CONTRIBUTING.md 所要求的独立人类审查。有人类对本条评论负责,如有错误请直接指出。
jackwener
left a comment
There was a problem hiding this comment.
Required test is now completed/success on exact head 43770a536c58b645980b0e60fa82c015671f88fc. I found no open P0–P2, so I am approving it.
This is a feature. I will not merge it. Create the five effort/* labels before the first live run if you have not already.
Posted by an automated review agent operated by @WAWQAQ (GitHub: jackwener). This is not an independent human review and does not satisfy the committer review required by CONTRIBUTING.md. A human is accountable for this comment — please push back if anything here is wrong.
简体中文
必选 test 已在这个 SHA 上 completed/success,我 approve。这是功能 PR,我不会合。合入前请先建好五个 effort/* 标签。
本条评论由 @WAWQAQ 运行的自动化审查程序发出(GitHub: jackwener)。它不构成 CONTRIBUTING.md 所要求的独立人类审查。有人类对本条评论负责,如有错误请直接指出。
M4n5ter
left a comment
There was a problem hiding this comment.
Approved on exact head 43770a536c58b645980b0e60fa82c015671f88fc.
The final scope is appropriately narrow: it stores only reading effort and leaves review, check, draft, and merge state to GitHub's existing signals. The writable-token job checks out ${{ github.sha }}, never the pull request head, disables credential persistence, and classifies the pull request from API metadata. I found no path that executes fork-controlled code.
The earlier review-state label axis and the duplicate dry-run script are gone, so the change does not leave a second readiness authority or a second implementation path behind. The exact-head hosted test completed successfully, the 56 focused classifier and workflow-contract tests passed locally, and the current-main merge result is clean. The existing P3 suggestion to lock the trusted checkout ref in a contract test is sensible but does not change the current behavior.
I left one non-blocking P2 inline: the repository currently has none of the five effort/* labels, so they must be provisioned before the first live run.
Posted by an automated review agent operated by @M4n5ter. This is not an independent human review and does not satisfy the committer review required by CONTRIBUTING.md. A human is accountable for this comment — please push back if anything here is wrong.
简体中文
本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md 所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。
pull_request_target hands the job a writable token while the pull request is fork controlled, so the checked-out ref is what decides whether that token can reach author-supplied code. copilot-auto-review.yml already stated the rule in a comment and pr-effort-label.yml pins the ref, but nothing failed when either changed. The contract covers both files and fails on the two edits that would widen the token's reach: pointing ref at head.sha, and dropping ref so a merge-ref event supplies the pull request's own tree. Generated-by: Claude Code
Summary
Reviewer attention is the binding constraint on this repository, and nothing in the pull request list says how much reading each entry asks for. Sorting that out means opening pull requests one at a time, over a set that turns over roughly fifty times a day.
This labels the one axis a search cannot express, and deliberately labels nothing else.
Why only effort. GitHub already indexes review state, check state and draft state, so
is:pr is:open draft:false review:required status:successanswers "whose move is it" today — it returns 48 on the current open set, needs no workflow, and cannot go stale. Pull request search has no size qualifier, so effort is the one signal that has to be computed and stored somewhere.Tiers. Inclusive upper bounds on readable lines:
effort/XS10,effort/S100,effort/M500,effort/L1000,effort/XLabove. Measured over the 142 currently open pull requests: 5 / 26 / 41 / 21 / 49. No tier is decoration.Readable lines exclude what nobody reads line by line — lockfiles (including
Cargo.lock), both spellings the notice generator emits, regenerated sources, snapshots and binaries. Without that, a Rust dependency bump reads as XL on the strength of 3524 rewritten lock lines. Test code is not discounted; it is reviewed too.Cost. Labelling runs when a pull request first asks for review, plus one daily sweep over the open set — roughly 60 runs a day, each a few seconds of pure Node with no build. A push can change the tier, but subscribing to every push would mean hundreds of runs a day to correct a label nobody is misled by in the meantime. The daily sweep absorbs that drift and doubles as the recovery path for any missed event.
Verification
node --test --test-concurrency=1 scripts/pr-effort.test.mjs— 13 tests, all passing, exercised throughplanLabels, the module's only export. Added to the CItestjob as a pure-Node step beside the existing planner and epoch-guard tests, so it consumes no additional runner.npm run format:check— clean over 1688 files, andnode --test scripts/ci-test-plan.test.mjspasses with the workflow registered in the pull request trigger allowlist.dry_rundispatch input rather than a separate script, so it exercises the code path that runs in production:Exclusion coverage was checked against the tree rather than assumed:
git ls-files '*Cargo.lock'finds three (3524, 1442 and 207 lines), and the fourTHIRD_PARTY_*files split across both theNOTICESandLICENSESspellings. Both are covered by tests.Not run: the workflow itself cannot execute until it is on
main, sincepull_request_targetandscheduleonly trigger from the default branch. The classification it depends on is covered by the unit tests and the dry run above.Review focus
An earlier revision of this branch also labelled review state (
review-ready,merge-ready,needs-author,waiting-infra). That axis has been removed rather than fixed. It duplicated native search, it needed three event triggers to stay fresh, and apull_request_reviewtrigger would have checked outrefs/pull/N/merge— executing pull request code in a workflow that holds a writable token. The remaining checkout pinsrefexplicitly for that reason, even thoughpull_request_targetandscheduleboth already resolve to the default branch. That pin is now a contract:scripts/ci-test-plan.test.mjsrequires every checkout in apull_request_targetworkflow to useref: ${{ github.sha }}, so both pointing it athead.shaand dropping it fail CI. It coverscopilot-auto-review.ymltoo, which stated the same rule in a comment nothing enforced.Rollout
The five
effort/*labels must exist before the first non-dry run.POST /issues/{n}/labelsdoes not create a label it has never seen, so a missing tier is simply never written — an earlier revision of this description claimed the opposite, and @M4n5ter corrected it.Proposed colours: one hue, five lightness steps, hue 250 being the Maka
--accenttoken. The ramp encodes review cost, not risk — a large pull request is expensive to read, not bad — which is why it is a single-hue ramp rather than green-to-red.effort/XS#D4EBFFeffort/S#AFD5FEeffort/M#82B9F2effort/L#4D97DEeffort/XL#0871C1Descriptions match the bounds, e.g.
effort/M— "Under 500 readable lines". Please say so on this pull request if you would rather these were named or coloured differently; they are cheap to change before the first sweep and awkward afterwards.AI use
Select exactly one:
Tool(s) and scope: Claude Code wrote the classifier, its tests, the workflow and the dry-run script, produced the measurements quoted above, and ran three adversarial reviews of an earlier revision whose findings led to removing the review-state axis. The scope decisions — which axis survives, the tier boundaries, and keeping labels out of merge authority — were settled by the contributor of record, who reviewed the final diff.
Checklist
Does this PR entail a change in behavior?