Skip to content

ci: verify the port band above the first binder, not the last - #8

Merged
eilandert merged 2 commits into
mainfrom
ci/verify-port-band-before-prove
Aug 2, 2026
Merged

eilandert merged 2 commits into
mainfrom
ci/verify-port-band-before-prove

Conversation

@eilandert

@eilandert eilandert commented Aug 2, 2026 •

Copy link
Copy Markdown
Member

The one runnable row left in this repo's ledger, filed by CodeRabbit on #6 and deliberately left out of #6 and #7 because both were docs-only. Fixes the order and adds the check that would have caught it.

TL;DR

build-test.yml checks that its test port band is free — but it did the check after the first test suite had already used the band. So the check could only ever speak for the suite that ran second. Moving it up, and teaching the port linter that a check placed below a binder is not a check.

Precisely: Verify this job's port band is free moves above Run ci/t/, and lint-ci-ports.sh gains an ordering rule.

The mechanism

test-nginx declares TEST_BASE_PORT: '19200' and runs three things against it: prove -v ci/t/, then max-port.sh, then test_runtime.py. max-port.sh is the tool that names two specific failures — a leftover server still listening inside the band, or a band that reaches into the kernel ephemeral range where the allocator will race a listener. Neither is visible from the failure it produces downstream.

Sitting between the two suites, it guarded the runtime suite and nothing else. prove bound the band unverified, so both of those failures reached Test::Nginx as a bind error or a timeout with no cause attached — which is exactly the shape the tool exists to remove. The step's own comment claimed it "fails BEFORE the fixture starts", true of the runtime fixture and not of prove.

Every existing check reads the broken shape as correct: the band is declared, it is distinct from ci-deep.yml's 19400, and it is passed through to the driver. Presence was checked; position was not.

The linter rule

A verifier is a property of a position, not of a job, so check_ports now compares indices: a job that runs ci/tools/max-port.sh must run it before the first step that binds, where a binder is the runtime driver, prove, or coverage.sh. Non-run steps keep their slot so the comparison stays an ordering comparison.

Scoped to jobs that already carry the verifier. Whether a binding job must carry one at all is a wider question and a separate row — see Out of scope.

Testing

  • ci/linter/selftest.sh: all controls held, including the new verify-after-bind fixture going red (exit 1) and the clean fixture staying green. The fixture encodes the exact shipped shape — distinct band, passed through, verifier present, verifier below prove.
  • Mutation against the live tree, not just the fixture: git show HEAD~1:.github/workflows/build-test.yml into a scratch WORKFLOW_POLICY_ROOT reproduces the finding by name — build-test.yml:test-nginx runs ci/tools/max-port.sh at step 7, after step 6 has already bound the band. Post-fix tree is clean.
  • max-port.sh itself re-verified in both directions, since the reorder is only worth anything if the tool goes red: exit 1 with a squatted listener at 19205, exit 1 on a band at 60000 reaching the ephemeral floor (32768 here), exit 0 and 19263 on the clean band.
  • ci/linter/run-all.sh clean on the full tree.

The second commit fixes two false positives the check shipped with, both found by self-review before CI: a bare prove substring also matches improve/approve, and a single multi-line run: block that verifies then binds has one step index for both, which index comparison could only read as a violation. Both shapes are now cases in the clean fixture — the check must stay silent on them — and restoring the naive matching turns that fixture red naming exactly those two jobs.

Docs

The README drift list and ci/PROMPT-sync-module.md § 2a both told a derived module to work around this repo's step order. That instruction had a shelf life of this PR; both now state the rule — put the verifier above the first binder — and cite the old shape as the worked example of getting it wrong.

Out of scope

Two adjacent findings, both real, both filed rather than fixed here:

  • build-test.yml:asan and ci-deep.yml:build-flavors run prove with no TEST_NGINX_PORT, so they take Test::Nginx's hardcoded 1984 — three matrix cells in the latter. lint-ci-ports.sh does not catch it because "runtime-bearing" means test_runtime.py, and prove is not it.
  • ci-deep.yml:coverage declares 19400 and never verifies it.

Both want the linter's declaration check widened to cover prove, which turns the tree red until those jobs get bands — a different change with a different blast radius.

build-test.yml ran ci/tools/max-port.sh between 'Run ci/t/' and the runtime
suite. The step reads as a guard for the job, but a verifier can only speak for
the steps below it, so prove bound TEST_BASE_PORT unverified and the one failure
max-port.sh exists to name -- a leftover server on the band, or a band reaching
the kernel ephemeral range -- still arrived inside the suite as an unattributed
bind error or timeout.

Move the step above 'Run ci/t/', and add the ordering rule to lint-ci-ports.sh
so the position cannot regress: a job carrying the verifier must run it before
the first step that binds, where a binder is the runtime driver, prove, or
coverage.sh. Every presence check stayed green in the broken shape -- distinct
band, passed through, verifier present -- which is why nothing caught it.

Negative controls: new fixtures/policy/verify-after-bind goes red (selftest, all
controls held), and reverting the reorder in a scratch copy of the live tree
names the real job and both step numbers. max-port.sh itself was re-verified in
both directions: exit 1 on a squatted band, exit 1 on a band reaching the
ephemeral floor, exit 0 clean.

Docs that described the old order as a divergence to work around (README drift
list, PROMPT-sync-module 2a) now state the rule instead.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@eilandert, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b389e041-de4c-4125-b340-1efc6e308a9e

📥 Commits

Reviewing files that changed from the base of the PR and between 10c0670 and f91180e.

📒 Files selected for processing (10)
  • .github/workflows/build-test.yml
  • README.md
  • ci/PROMPT-sync-module.md
  • ci/linter/README.md
  • ci/linter/fixtures/policy/clean/.github/workflows/ci.yml
  • ci/linter/fixtures/policy/verify-after-bind/.github/workflows/ci.yml
  • ci/linter/fixtures/policy/verify-after-bind/README.md
  • ci/linter/lint-ci-ports.sh
  • ci/linter/selftest.sh
  • ci/linter/workflow_policy.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Self-review of the check added in the previous commit.

A bare "prove " substring also matches `improve `, `approve ` and `prover`,
so a run block that merely mentions improving something read as a binder sitting
above the verifier. Binder matching is now word-bounded.

And a single multi-line run block that verifies and then binds has one step
index for both, which the index comparison could only read as 'the binder is not
below the verifier' -- a finding on correct code. Same-index now falls back to
position within the step's text, and only reports when the bind genuinely comes
first.

Both shapes go into the clean fixture rather than a new one: they are cases the
check must stay SILENT on, so they belong in the positive control. Verified by
mutation -- restoring the substring match and dropping the same-step branch
turns the clean fixture red with exactly these two jobs named.
@eilandert
eilandert merged commit 72c28db into main Aug 2, 2026
14 checks passed
@eilandert
eilandert deleted the ci/verify-port-band-before-prove branch August 2, 2026 02:12
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