Skip to content

ci: fleet review lanes show up as commit statuses on the PR head - #52

Merged
askalf merged 11 commits into
mainfrom
ci/fleet-status
Sep 25, 2026
Merged

askalf merged 11 commits into
mainfrom
ci/fleet-status

Conversation

@askalf

@askalf askalf commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

The fleet's review lanes (verification, Redline's gating review, the Second Read) run as tickets on the fleet's box. On GitHub, a PR waiting on one of them looked the same as a PR nobody had picked up. This posts one commit status per lane on the PR head, next to the other checks.

status pending green red
fleet/verify waiting on required CI (or the Breaker) at the head verified at the head, or not required (docs, assets, .github config, bot branch) a required check failed at the head
fleet/review waiting on Redline at the head, or on verification Redline approved the head Redline requested changes at the head
fleet/second-read waiting on the Second Read at the head, or on verification READY at the head, or not gating (non-code PR) NOT READY at the head, with its reason

Here the base branch requires test (20, 22, 24), analyze (javascript-typescript). The script reads that list from the branch rules at run time, so nothing here names a check. workflow_run re-runs it when any pull_request workflow finishes, and a test fails if one is missing from that list, so a required check added later from any workflow still refreshes the lanes. pull_request_target workflows (PR triage) are left out: they run against the base branch's commit, so their checks never land on the PR head.

The three fleet/* contexts are never counted as CI themselves, so they can become required checks without fleet/verify waiting on itself.

The rules are the fleet dispatcher's: a verdict counts only at the head; on code, Redline's deterministic low-risk approval is not a verdict and the Second Read gates too.

  • The status job runs the default branch's copy of scripts/fleet-status.mjs, never the PR's code. It is skipped until this merges, so this PR's own statuses are not the first live check; the next PR's are.
  • It runs on a GitHub-hosted runner with the workflow's own token (checks: read, statuses: write, reads only otherwise). It does not use the fleet's GitHub quota.
  • Fork PRs are skipped; the fleet does not review them.
  • No concurrency group: a cancelled run would roll up as a failed check. Instead each run posts only what differs from the head's newest statuses, then re-reads the PR and corrects what differs (up to three passes), so the run that acts last leaves statuses matching data at least as new as anything posted.
  • The statuses are informational. None is added to the ruleset's required checks.
  • self-test runs the script's 110 unit tests on the PR's code, read-only.
  • backfill (manual, workflow_dispatch) posts the lanes on every open same-repo PR, paging past 100 and failing loudly at 1000.

Next step, after this merges: add fleet/verify, fleet/review and fleet/second-read to the default branch's required status checks, then run backfill, so PRs opened earlier report too. With them required, GitHub refuses a merge (by hand or by native auto-merge) until every lane agrees at the head; without them, branch protection knows only CI plus one approval, so a merge by hand or by auto-merge can skip the Second Read.

How to test

  • node scripts/fleet-status.test.mjs: 110 pass, 0 fail.
  • The script and tests are plain ASCII. Tests build the Second Read's own separator character at run time with String.fromCharCode, so its real verdict line is covered with no dash in the source.

@github-actions github-actions Bot added github_actions Pull requests that update GitHub Actions code tests Test suite and CI size/L 200-799 hand-written lines labels Sep 25, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Deploying plumbline with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3c1c4d7
Status: ✅  Deploy successful!
Preview URL: https://867d9116.plumbline-9rm.pages.dev
Branch Preview URL: https://ci-fleet-status.plumbline-9rm.pages.dev

View logs

…in the bot rule

A deleted `## Verification at` comment or an edited review's SECOND READ line
changes the lanes, but neither event re-ran the workflow, so a green status could
outlive what it stood for. issue_comment now includes `deleted` and
pull_request_review includes `edited`.

isBotPr is unchanged on purpose: it is the dispatcher's rule. review-dispatch.sh's
`gate` field and needsVerification() in platform's public-automerge-sweep.ts both
exempt a bot-shaped branch only when askalf or github-actions opened it, because
anyone can name a branch `release-x`. The doc comment now says so, and five tests
pin it (a person on bot/ or release/ is still verified).
The concurrency group cancelled an in-flight run whenever a review, comment or
CI completion landed close behind another event. GitHub rolls a cancelled check
run up as a failure, so the PR's checks read red with nothing wrong (cordon#82,
truecopy-action#32 and checkout-with-retry#20 showed it within minutes).

The group goes. Ordering moves into the script: a run notes GitHub's clock (the
Date header) when it reads the PR, and before posting each context skips it if a
status for that context was posted after that moment, since that run read
fresher data. postedSince() is pure and has five tests (83/83).
The Second Read on amnesia#83 and redstamp#162: once fleet/verify, fleet/review
and fleet/second-read are required checks (the step this PR plans next), the
branch rules list them, and requiredCiState counted them as CI the head waits
on. fleet/verify pending made requiredCi pending, which kept fleet/verify
pending, so every code PR would have stayed blocked for good.

requiredCiState drops the three contexts before it reads anything else. Five
tests pin it, including the Second Read's reproduction: three rounds of feeding
each run's statuses back in as the next run's checks now end all green (88/88).

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the GPT gating lane (gating review).

Request changes: large documentation-only pull requests are incorrectly held for verification. rule:none

Blocking — scripts/fleet-status.mjs:67

return facts.files.length >= 100 || facts.files.some(isCodePath);

A same-repository PR changing 100 Markdown files (for example, a documentation reorganization) has no code path, so it is verification-exempt under the fleet rule. This added size condition instead makes it a code PR. laneStatuses() then leaves fleet/verify, fleet/review, and fleet/second-read pending until CI/verification, even though none is required. Once these contexts are required, that PR cannot merge. The accompanying test at scripts/fleet-status.test.mjs:125-126 deliberately pins this incompatible behavior.

return facts.files.some(isCodePath);

Keep the existing file-count handling only if the dispatcher rule is changed consistently; it cannot override the documented exemption for a PR where every changed file is documentation, an asset, .github/ configuration, or .gitattributes.

What's good: the workflow scopes writes to same-repository PR events, uses least-privilege job permissions, and the required-CI and stale-status paths have focused unit coverage. I checked the full 643-line added workflow/script/test diff and the required CI results; all four required checks are green at the live head.

…0 files

Redline on browser-bridge#114, truecopy#212 and plumbline#52:

- A dismissed Second Read review is not a verdict. secondReadAtHead skips
  DISMISSED reviews, so a dismissed NOT READY no longer keeps the lane red.
- Ordering by the Date header and created_at cannot tell a same-second newer
  post from an older one. postedSince is gone. Each run now posts only what
  differs from the head's newest status per context, then re-reads everything
  and corrects what differs, up to three passes. The run that acts last
  re-reads after its own writes, so what stays on the head matches data at
  least as new as anything posted. latestByContext and statusesToPost are the
  pure parts, with tests for a stale overwrite being corrected.
- The file-count rule matches the live dispatcher: forge's readPrFacts reads
  the first 100 files and fails closed when there are more, so more than 100
  (not exactly 100) is code. A large docs-only PR still verifies once its
  required CI passes.

94/94.
Three test lines described where a case came from instead of what it checks: a
section title naming an old PR, and two comments referring to a review and to
the planned rollout step. They now describe the behavior only. The workflow
comment on ordering matches the post-then-verify loop. No logic change (94/94).

@sprayberry-secondread sprayberry-secondread left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the Claude second-opinion lane (independent second read; the gating review is posted separately).

Verdict: no blocking issues at fc78a64. The lane rules match the stated dispatcher rules, each reachable boundary has a test that fails if the rule changes, and the claims in the body hold.

Read at the live head fc78a64e8ea26d6bd66b5e73a421aacaa534e648. The review ticket named 200fb1a, but the branch moved after that, so this verdict is for fc78a64. I read the full diff (3 files, +727). I ran the head's scripts/fleet-status.mjs against the Second Read verdict-line inputs listed below. I did not run the suite locally; self-test is green at this head.

Boundaries, rebuilt from the diff

Guard Input What the code does Pinned by
needsVerify: files.length > 100 99 / 100 / 101 docs files not code / not code / code 99 docs files are not code, exactly 100 docs files is not code, more than 100 files is code whatever they are
isBotPr person on bot/, release/1.2; askalf on bot/, release-v, receipts-; dependabot on any branch not bot / bot / bot the bot PRs block and the five dispatcher-rule checks
isCodePath .github/workflows/*.yml, .github/actions/**, .github/workflows/helper.MJS, .txt inside and outside docs/ not code / code / code / not code, code the what counts as code block
verifiedAtHead label only; comment only; 7-char prefix; older comment after a head comment; other login; findings/blocked heading false / false / true / false / false / false the verifiedAtHead blocks
requiredCiState []; only fleet/* required; missing; running; failed + pending; rerun passed; skipped/neutral none / none / pending / pending / failed / passed / passed the required CI block and the own-lanes block
laneStatuses verify, ci === 'pending' with label and comment at head pending (CI wins over the Breaker rule) CI pending: an old Breaker label and comment do not count
redlineVerdict deterministic approval on code / on docs; DISMISSED; COMMENTED; CR after APPROVED at the same head skip / count / skip / skip / red the deterministic approvals block and the Redline ordering checks
secondReadAtHead older head; no line; READY, mostly; two lines in one body; later lineless review; DISMISSED; Redline carrying the line none / none / none / last wins / keeps verdict / skip / none the Second Read blocks
fit 140 / 141 characters kept / 137 + ... the 140-character edge
statusesToPost same state and description; new description; missing context skip / post / post the post-then-verify block

I ran secondReadAtHead myself on CRLF bodies (...NOT READY - x\r\n, ...READY\r\n, and CRLF with no trailing newline), on an em-dash separator, on NOT READYISH, on READY\t, and on an empty body. All seven gave the expected lane: CRLF NOT READY is red with reason x both with and without the trailing newline, CRLF READY is green, the em dash is stripped from the reason, NOT READYISH is pending, READY\t is green, and the empty body is pending. The CRLF cases have no test. They pass today, so this is not a finding.

For the tests, I read each assertion against the rule it names. Every one fails if its rule is removed or inverted. For example, own lanes required and pending, CI green: still passed goes red without the OWN_CONTEXTS filter, and the latest comment wins goes red if the first comment were used instead.

Body claims, checked

  • 94 checks: check( appears 94 times in the test file.
  • Plain ASCII: no byte above 0x7F anywhere in the diff.
  • workflow_run on ci, CodeQL: both workflow names exist in this repo.
  • Required checks: the main ruleset requires exactly test (20), test (22), test (24) and analyze (javascript-typescript), and the script reads that list at run time.
  • Same script as askalf/dario#1419: blob 8b338ee matches the dario PR head. I did not find the test file at the same path there, so I checked only the script half of that claim.
  • Workflow-only permissions and no PR code in status: correct. The job checks out the default branch sparsely, PR is validated with ^\d+$ before use, and no event text reaches a shell.
  • "The status job … is skipped until this merges": strictly, the job runs and its step is skipped by hashFiles(...). status shows pass and the head has no fleet/* statuses. Same effect, wording only.

Note for the next step (not a finding on this diff)

The body plans to make fleet/verify, fleet/review and fleet/second-read required checks. Commit statuses are posted by context name. So any workflow that runs on a same-repo PR's own pull_request event with statuses: write can post fleet/second-read: success, including a new workflow file added in that PR. Once these contexts are required, they are as strong as write access to the repo, not as strong as the lanes. This diff makes nothing required, so nothing blocks here. When the ruleset change is made, please weigh that limit, or pin the source if the ruleset supports it.

What's good

The post-then-re-read loop in scripts/fleet-status.mjs:401-418 is a sound replacement for a cancelling concurrency group. Each pass posts from data it just read, so a stale run's overwrite is corrected by that same run's next pass or by the next event's run. Unreadable rules or checks fail closed: an unreadable ruleset falls back to the label-and-comment rule, and unreadable checks count as pending. Neither can turn fleet/verify green.

SECOND READ: READY

…losed rules

- The two NOT READY test fixtures use an ASCII hyphen instead of a dash escape.
- The reason strip drops the one separator after NOT READY (hyphen, colon, or
  the Second Read's own dash) and keeps a leading backtick, quote or bracket,
  so a reason that names a symbol keeps its code span. Four tests.
- Unreadable branch rules count as pending, as the dispatcher waits on them,
  instead of falling back to the label-and-comment rule.
- The bot-rule test uses 101 files, so it fails if the bot rule is removed,
  and a counterpart pins that a person's 101-file docs PR is code.

99/99.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the GPT gating lane (gating review).

Verdict: APPROVE — no blocking issues found in the live-head re-review.

I reviewed the 727-line workflow/status implementation and its unit coverage. The status job reads the default-branch script, constrains its token to the required read scopes plus statuses: write, excludes fork events, and re-reads facts after posting to correct stale writes. The updated review handling excludes dismissed Second Read reviews, and the status comparison is covered for stale-status correction. Required CI is green at fc78a64e8ea26d6bd66b5e73a421aacaa534e648 (analyze and all three required test jobs).

What's good: the change has focused tests for the verification gates, verdict/head matching, bot and file-count boundaries, required-check handling, and the three-pass reconciliation behavior. I did not run the local suite, per review policy; CI is the test signal.

… that it does

The workflow_run list named only the workflows behind today's required checks,
so a required check added later from another workflow would leave fleet/verify
pending until the next PR event. The list now names every workflow in this
repository that runs on pull requests, and a test reads .github/workflows and
fails if one is missing from it, so adding a workflow without listing it fails
CI instead of stalling the lanes.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the GPT gating lane (gating review).

Verdict: APPROVE — no blocking issues found at live head 7496a1fe4eef45f518a54b2ec41648d1adfb5cb1.

This is a fresh verdict because the head moved after the prior submission. I reviewed the additional live-head changes as well as the original 727-line status workflow and unit coverage. The update correctly fails closed when branch-rule reads are unavailable and preserves punctuation used as the first character of a Second Read reason while removing only a separator. The accompanying tests cover those cases and the bot/file-count ordering boundary. Required CI is green at this live head (analyze and all three required test jobs).

What's good: the workflow uses the default-branch status script with constrained permissions, excludes forks, and its post-then-re-read reconciliation avoids leaving stale lane contexts. I did not run the local suite, per review policy; CI is the test signal.

…et cannot move a required check

A pull_request_target workflow runs against the base branch's latest commit, so
its checks land on that commit and never on the PR head, and the status job
drops its workflow_run events anyway. Listing one (PR triage) could never
refresh the lanes, and the list test passed regardless. The list now names only
pull_request workflows, and the test checks both directions: every
pull_request workflow is listed, and a pull_request_target-only workflow is not.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.
Reviewed by the GPT gating lane (gating review).
Verdict: changes requested — the public PR description contains generated-writing tells. rule:reads-as-generated

Blocking — PR description, opening paragraph: “This posts one commit status per lane on the PR head, next to the other checks. It is the same change as askalf/dario#1419.” The public description also uses em dashes repeatedly (for example, “The three fleet/* contexts are never counted as CI themselves, so they can become required checks without fleet/verify waiting on itself.”). First-party strict mode treats em dashes in public PR text as a generated-writing tell. Remove the em-dash constructions and keep the description in the repository’s ordinary concise style before this is submitted.

Rewrite the affected sentences with plain ASCII punctuation, or remove the patch narration where it is not needed to describe the behavior.

I reviewed the three added workflow/script/test files and the live-head required CI signal. The required checks (analyze (javascript-typescript) and test on Node 20, 22, and 24) are green at 0ad9784. I did not run the local test suite. The workflow’s default-branch checkout, same-repository event guards, bounded post/readback loop, required-context exclusion, and tests for the status classification logic otherwise look coherent.

What's good: the workflow has minimal permissions, keeps fork events out of the write path, and exercises the status computation separately from the event runner.

… own separator

- backfill read one page of 100 open PRs, so a larger repository would leave
  some PRs without lanes. It now lists up to 1000 (gh pages past 100) and fails
  at that cap instead of skipping PRs. Two tests pin the limit and the guard.
- The Second Read writes its verdict line with its own separator character, and
  after the ASCII fixtures nothing tested that form. The tests now build every
  separator form at run time with String.fromCharCode, the Second Read's
  included, so the real line is pinned with no dash in the source.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the GPT gating lane (gating review).

Verdict: APPROVE — no blocking issues found.

What I checked: the complete 799-line workflow/status implementation and its unit tests, with particular attention to the new paginated backfill limit and the Second Read separator parsing. Required CI is green at 3c1c4d7. The current-head changes correctly retain all same-repository PRs returned through the 1000-item cap, fail rather than silently omit work at that cap, and exercise the real U+2014 verdict separator without placing it in source text. I did not run the local suite, per review policy.

@askalf askalf added the author-fixes The PR's author answers its reviews; no fleet fix ticket label Sep 25, 2026

@sprayberry-secondread sprayberry-secondread left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated review from the Sprayberry Labs fleet code reviewer.

Reviewed by the Claude second-opinion lane (independent second read; the gating review is posted separately).

Verdict: no blocking issues at 3c1c4d7. The ticket named 0ad9784, but the head has since moved to 3c1c4d7 (which adds backfill paging and the separator test). This read covers the live head.

What I read

The full diff (.github/workflows/fleet-status.yml, scripts/fleet-status.mjs, scripts/fleet-status.test.mjs, +799/-0), the on: blocks of all nine existing workflows on main, the main ruleset's required contexts (test (20), test (22), test (24), analyze (javascript-typescript)), the pinned checkout-with-retry inputs, and gh pr checks. self-test passed at this head: 110 pass, 0 fail. I did not run anything locally.

Boundaries, rebuilt from the diff

predicate input behaviour pinned by
needsVerify files.length > 100 99 / 100 / 101 docs files not code / not code / code 99 docs files are not code, exactly 100 docs files is not code, more than 100 files is code
needsVerify bot rule first bot branch + 101 files exempt a bot PR with more than 100 files is still not code
isBotPr person on bot//release/; askalf/github-actions on bot branch; dependabot on any branch no / yes / yes the bot PRs block and the dispatcher-rule block
isCodePath .md/images, docs/*.txt, .github config, .github/actions, .github/**/*.MJS, .txt outside docs matches the header comment isCodePath blocks
verifiedAtHead label only, comment only, old sha, 7-char prefix, later old comment, other login, findings/blocked heading each case covered verifiedAtHead blocks
requiredCiState [], own contexts only, unreported, running, failed plus pending, rerun passed, skipped/neutral none, none, pending, pending, failed, passed, passed required CI and own-lanes blocks
requiredCiState own-context filter fleet/* required and pending passed. The three-round loop fails without the filter because it stays pending own lanes required, three rounds
redlineVerdict deterministic approval on code vs docs; DISMISSED/COMMENTED at head; CR after APPROVED at the same head; CR at an unverified head skip vs count; older verdict stands; red; pending deterministic approvals, Redline reviews that are not verdicts
secondReadAtHead no line, READY, mostly, two lines in one body, later lineless review, older-head verdict, Redline carrying the line, dismissed NOT READY, bare NOT READY none, none, last wins, keeps verdict, none, none, pending, red with no trailing colon the Second Read, review by review and the dismissed block
reason strip -, :, U+2013, U+2014, code span, [ with no separator separator dropped, first char kept separator loop and reasonOf blocks
fit 140 / 141 chars kept / 137 plus ... the 140-character edge
statusesToPost identical, state differs, missing, same state with a new description, stale overwrite skip, post, post, post, corrected then left alone post-then-verify block

For every row, removing or inverting the guarded line changes at least one assertion's outcome. For example, dropping the OWN_CONTEXTS filter leaves the three-round loop pending, and dropping the DISMISSED skip turns dismissed NOT READY at head red. None of the new assertions holds whether or not the code under test is there.

PR body claims vs the diff

  • The status job runs the default branch's script: ref: ${{ github.event.repository.default_branch }} plus a sparse checkout of scripts/fleet-status.mjs (fleet-status.yml:49-54). Before merge, the hashFiles guard skips the step. Holds.
  • workflow_run lists every pull_request workflow and leaves out pull_request_target: on main, ci, CodeQL and labels are the three pull_request workflows, and PR triage is the only pull_request_target one. The file-scanning test pins this, and it would fail on a new unlisted workflow. Holds.
  • Fork PRs are skipped both in the job if: and in readFacts (fleet-status.mjs p.head?.repo?.full_name !== repo). PR code runs only in self-test, with contents: read. The PR env is checked against ^\d+$ before use. I found no injectable interpolation. Holds.
  • Unreadable rules or checks leave requiredCi at pending, so fleet/verify cannot turn green by accident. Holds.
  • Backfill: --limit 1000 with -ge 1000 fails loudly, and the test pins both. Holds.
  • The diff's added lines are all ASCII (checked mechanically). Holds.

Non-blocking observation

  • fleet-status.mjs:382 reads check-runs?per_page=100 without paging, unlike statuses, files, reviews and comments, which go through ghAll. With more than 100 check runs on a head, a required check on page 2 reads as unreported, so fleet/verify stays pending. That fails closed, and heads here carry about 13 runs, so this is informational only.

What's good

The lane rules are pure functions over a facts object, and every branch has a named case. The post-then-verify loop is a sound alternative to a concurrency group, which would roll up as a cancelled check. The workflow-list test turns "a new required check silently never refreshes the lanes" into a failing test.

SECOND READ: READY

@askalf
askalf merged commit bddc238 into main Sep 25, 2026
19 checks passed
@askalf
askalf deleted the ci/fleet-status branch September 25, 2026 05:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author-fixes The PR's author answers its reviews; no fleet fix ticket github_actions Pull requests that update GitHub Actions code size/L 200-799 hand-written lines tests Test suite and CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants