Skip to content

feat(ci): label pull requests by reading effort - #3949

Merged
Astro-Han merged 5 commits into
mainfrom
feat/pr-triage-state-labels
Aug 27, 2026
Merged

feat(ci): label pull requests by reading effort#3949
Astro-Han merged 5 commits into
mainfrom
feat/pr-triage-state-labels

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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:success answers "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/XS 10, effort/S 100, effort/M 500, effort/L 1000, effort/XL above. 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 through planLabels, the module's only export. Added to the CI test job 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, and node --test scripts/ci-test-plan.test.mjs passes with the workflow registered in the pull request trigger allowlist.
  • The same classifier run over all 142 open pull requests, writing nothing. That rehearsal is now the workflow's dry_run dispatch input rather than a separate script, so it exercises the code path that runs in production:
  effort/XS       5
  effort/S       26
  effort/M       41
  effort/L       21
  effort/XL      49

largest gaps between raw and readable lines
  #3370   raw   23422 ->    3443  effort/XL
  #3296   raw    3113 ->     296  effort/M     <- would otherwise read as XL
  #3736   raw     255 ->      82  effort/S
  #3809   raw     115 ->       0  effort/XS

Exclusion coverage was checked against the tree rather than assumed: git ls-files '*Cargo.lock' finds three (3524, 1442 and 207 lines), and the four THIRD_PARTY_* files split across both the NOTICES and LICENSES spellings. Both are covered by tests.

Not run: the workflow itself cannot execute until it is on main, since pull_request_target and schedule only 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 a pull_request_review trigger would have checked out refs/pull/N/merge — executing pull request code in a workflow that holds a writable token. The remaining checkout pins ref explicitly for that reason, even though pull_request_target and schedule both already resolve to the default branch. That pin is now a contract: scripts/ci-test-plan.test.mjs requires every checkout in a pull_request_target workflow to use ref: ${{ github.sha }}, so both pointing it at head.sha and dropping it fail CI. It covers copilot-auto-review.yml too, 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}/labels does 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 --accent token. 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.

image
Tier Readable lines Colour Open today
effort/XS ≤ 10 #D4EBFF 5
effort/S ≤ 100 #AFD5FE 26
effort/M ≤ 500 #82B9F2 41
effort/L ≤ 1000 #4D97DE 21
effort/XL > 1000 #0871C1 49

Descriptions 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:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

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

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above

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
@Astro-Han
Astro-Han marked this pull request as draft August 26, 2026 16:54
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
@Astro-Han Astro-Han changed the title feat(ci): label pull requests by review state and reading effort feat(ci): label pull requests by reading effort Aug 26, 2026
@Astro-Han
Astro-Han marked this pull request as ready for review August 26, 2026 17:55
@Astro-Han

Copy link
Copy Markdown
Contributor Author

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 (is:pr is:open draft:false review:required status:success returns 48 on the current open set), and keeping labels in sync needed three event triggers, one of which would have checked out refs/pull/N/merge in a job holding a writable token.

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 scripts/pr-effort.mjs, and whether one daily sweep plus opened/ready_for_review is the right trade against labelling on every push.

@Astro-Han
Astro-Han requested review from M4n5ter and jackwener August 26, 2026 17:55
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 jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 所要求的独立人类审查。有人类对本条评论负责,如有错误请直接指出。

Comment thread .github/workflows/pr-effort-label.yml

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 M4n5ter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。

Comment thread .github/workflows/pr-effort-label.yml
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
@Astro-Han
Astro-Han merged commit 4b8bafc into main Aug 27, 2026
1 check passed
@Astro-Han
Astro-Han deleted the feat/pr-triage-state-labels branch August 27, 2026 03:04
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.

3 participants