Skip to content

ci(bump-callers): add preflight.sh — one tested staleness/decommission guard (BE-6475) - #136

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6475-bump-callers-preflight
Open

ci(bump-callers): add preflight.sh — one tested staleness/decommission guard (BE-6475)#136
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-6475-bump-callers-preflight

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

ELI-5

Every bump-*-callers.yml workflow in this repo has the same ~40 lines of "wait, is this run actually the newest one, and does the thing I'm about to pin still exist?" pasted into it before it calls the shared bump script. There are eight copies and they have quietly drifted apart, so different fleets behave differently on the same situation. This PR lifts the best copy out into one tested file, .github/bump-callers/preflight.sh, and adds a test suite that builds throwaway git repos to prove each branch. Nothing uses it yet — swapping the entrypoints over is a separate change — so this merge changes no fleet's behaviour and fires no bump run.

What this adds

  • .github/bump-callers/preflight.sh — the guard, extracted from bump-detect-unreviewed-merge-callers.yml (PR ci(bump-callers): add detect-unreviewed-merge caller fleet (BE-6294) #117, the most-hardened copy) and generalized. Inputs WATCHED (required), WATCHED_ASSETS (optional, for multi-path fleets), NEW_SHA, plus Actions' GITHUB_SHA / GITHUB_OUTPUT. Outputs proceed=true|false and new_sha=<sha> on every exit-0 path. Exits non-zero only for a lookup it could not perform.
  • .github/bump-callers/tests/test_preflight.sh — 42 assertions across 9 cases, no network: each case builds a local bare repo as origin plus a clone as the run workspace, drives the real script, and asserts exit code, both outputs, and the presence/absence of ::error:: / ::warning::.
  • Wiringtest-bump-callers.yml shellchecks both scripts and both suites and runs the new suite; .github/bump-callers/README.md gains a "Preflight" section (inputs, outputs, the two-step consumption pattern); AGENTS.md Commands/Layout updated to match.

Why the #117 semantics specifically

The five simple copies skip on a bare tip mismatch. That is wrong: the push trigger is path-filtered, so an unrelated commit landing between the trigger and the check starts no run of its own — skipping there discards the only run for the change and freezes every caller, which is the exact pin-drift this directory exists to prevent. bump-auto-label-callers.yml compares content but then pins the stale github.sha rather than the verified tip. The #117 copy gets both right, plus the exact-refname tip parse (a branch literally named foo/refs/heads/main matches the ls-remote pattern at component boundaries and must not be consumed), the FETCH_HEAD verification (an unresolvable FETCH_HEAD must not be read as deletion), and the $WATCHED-variable deletion test (two literals drift on a rename and turn the fleet into a permanent silent no-op).

The one generalization is WATCHED_ASSETS: #117's own comment says its single-blob comparison is coupled to the path filter and must be widened before the re-point is reused by a fleet whose paths: has more than one entry (cursor-review, groom, pr-size). Test case 6 is that case — a commit touching only the asset directory must read as stale, and the same fixture run without WATCHED_ASSETS proceeds, which is the under-verification the widening removes.

Test cases

Case Asserts
decoy ref a refs/heads/foo/refs/heads/main sorting first is not consumed; run proceeds at the real tip, no "main moved" log
failed ls-remote exit 1, ::error::, proceed not true
stale re-run, watched blob changed exit 0, proceed=false, stale log
main moved, content identical exit 0, proceed=true, new_sha == the new tip (the re-point)
watched file deleted at tip exit 0, proceed=false, ::warning:: decommission
asset dir changed only (multi-path) proceed=false — and the single-path config on the same fixture proceeds
absent at the run's OWN commit ::warning:: own-commit decommission, not reported as stale
current tip happy path proceed=true, new_sha == GITHUB_SHA, no fetch/compare
asset dir gone locally at the tip the final -d "$WATCHED_ASSETS" guard fires

Verification

shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/preflight.sh \
  .github/bump-callers/tests/test_bump_callers.sh .github/bump-callers/tests/test_preflight.sh   # clean
bash .github/bump-callers/tests/test_preflight.sh      # 42 passed, 0 failed
bash .github/bump-callers/tests/test_bump_callers.sh   # 175 passed, 0 failed (unchanged)
python3 .github/agents-md-integrity/check_agents_md.py --root .   # passed (2 pre-existing warnings)

git status confirms no bump-*-callers.yml file is touched.

Judgment calls worth a reviewer's attention

  1. bump-pr-risk-callers.yml has diverged past ci(bump-callers): add detect-unreviewed-merge caller fleet (BE-6294) #117 and this script does not implement its extra checks. Since ci(bump-callers): add detect-unreviewed-merge caller fleet (BE-6294) #117 landed, the pr-risk entrypoint grew a git rev-list --count HEAD..FETCH_HEAD -- "${WATCHED[@]}" test ("did a later commit touch a watched path", which survives a land-then-revert that nets to zero content diff), an --unshallow probe that test needs, path-filter exclusions (:(exclude)scripts/pr-risk/tests), and a git merge-base --is-ancestor refusal to pin an orphaned commit. The ticket is explicit that the extraction adopts the ci(bump-callers): add detect-unreviewed-merge caller fleet (BE-6294) #117 semantics, and this PR does exactly that — but that means the phase-2 swap must decide whether to fold pr-risk's checks in or leave that fleet on its own guard. Deleting them in the name of consolidation would be a regression. Flagged in the script header, the README, and here.
  2. Only main is supported as the default branch, hardcoded — same as all eight existing copies. Not generalized, because no fleet needs it and an unused input is a footgun.
  3. A multi-path fleet whose extra surface is not a single directory (pr-risk's exclusions) is not expressible with one WATCHED_ASSETS string. That is fine for cursor-review/groom/pr-size and is the scope the ticket specifies; see (1) for pr-risk.
  4. A deleted workflow file whose asset dir survives logs "stale" rather than "decommissioned" (proceed=false either way). That follows the specified condition — both objects must be gone for the decommission message — and is defensible: the deletion commit did touch the path filter, so it does have its own run.
  5. Negative-claim check. The diff adds skip/warn paths, so: the proceed path is empirically exercised green in three test cases (decoy, re-point, current tip) rather than only asserted absent, and no capability is denied — nothing consumes the script on this branch, so no fleet's behaviour can change from this merge.

…n guard (BE-6475)

Every bump-*-callers.yml entrypoint carries an inline copy of the
staleness/decommission guard that runs ahead of bump-callers.sh, and the
eight copies have drifted: five skip on a bare tip mismatch (throwing away
the only run for a change), one compares content but never re-points the pin
at the verified tip, one is the hardened #117 version, and pr-risk has grown
a different hardening again.

Extract the #117 guard as .github/bump-callers/preflight.sh, generalized with
an optional WATCHED_ASSETS input so a multi-path fleet compares the asset
directory too — which is exactly what the "COUPLED TO THE PATH FILTER" note
in #117 requires before its re-point can be reused. Emits proceed/new_sha as
step outputs (never $GITHUB_ENV, which a step-level env: NEW_SHA: would
silently override).

Phase 1 of 2: script + tests + docs only. No entrypoint is swapped over yet,
so no fleet behaviour changes and no bump fleet fires on this merge.
@mattmillerai mattmillerai added the agent-coded Authored by the agent-work loop label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 41 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: 7127a388-29c8-461a-8cbf-0e56ff169214

📥 Commits

Reviewing files that changed from the base of the PR and between 7f7c9bf and 13c540f.

📒 Files selected for processing (5)
  • .github/bump-callers/README.md
  • .github/bump-callers/preflight.sh
  • .github/bump-callers/tests/test_preflight.sh
  • .github/workflows/test-bump-callers.yml
  • AGENTS.md

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

@mattmillerai
mattmillerai marked this pull request as ready for review August 5, 2026 03:48
@mattmillerai mattmillerai added the cursor-review Multi-model cursor review label Aug 5, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 10 finding(s).

Severity Count
🟠 High 2
🟡 Medium 5
🟢 Low 3

Panel: 8/8 reviewers contributed findings.

Comment thread .github/bump-callers/preflight.sh Outdated
Comment thread .github/bump-callers/preflight.sh
Comment thread .github/bump-callers/preflight.sh
Comment thread .github/bump-callers/preflight.sh Outdated
Comment thread .github/bump-callers/preflight.sh Outdated
Comment thread .github/bump-callers/preflight.sh Outdated
Comment thread .github/bump-callers/preflight.sh
Comment thread .github/bump-callers/preflight.sh Outdated
Comment thread .github/bump-callers/tests/test_preflight.sh
Comment thread .github/bump-callers/preflight.sh
…sion verdict (BE-6475)

Cursor review panel findings on the phase-1 extraction. Behaviour-affecting
fixes, all with functional coverage in tests/test_preflight.sh:

- Decommission at the tip is now EITHER watched surface being gone, not both.
  Retirement is normally staged (delete the reusable, clean up its asset
  directory later), so the `&&` let the common case fall through to the
  "stale run/re-run" branch and exit green — suppressing the ::warning:: that
  is the fleet's only signal that live callers now hard-fail at startup. It
  also disagreed with the local -f/-d guards, which already used OR semantics.
  The message now names the surface that went away.
- NEW_SHA is validated as a full 40-hex lowercase SHA. It is the one value
  never derived from a lookup, yet it is emitted verbatim into $GITHUB_OUTPUT
  and handed to bump-callers.sh's pin rewrite: a newline in it injects extra
  output lines, and an injected `proceed=true` would win over the
  `proceed=false` this script wrote.
- HEAD is asserted equal to $GITHUB_SHA. Every "here" side is read from HEAD
  while every decision is keyed off GITHUB_SHA; a `ref:` override in the
  consuming checkout would have the script compare main against itself, so
  every comparison reads "unchanged" and every stale re-run proceeds.
- The tip is fetched as `refs/heads/main`, not the bare name. Refspec
  resolution consults refs/tags/<name> BEFORE refs/heads/<name>, and this repo
  routinely creates and force-moves major tags — a tag named `main` would
  silently become the FETCH_HEAD every comparison and the re-point run against.
  A tip that advances between the ls-remote and the fetch is now logged as the
  benign race it is.
- WATCHED / WATCHED_ASSETS are rejected when glob-shaped or slash-terminated.
  The guidance to widen them "to match the fleet's paths: filter" points
  straight at `.github/groom/**`, which resolves to nothing and would make
  every comparison verify nothing behind a green run.
- rev-parse lookups distinguish "absent from the tree" (exit 1) from "the
  lookup failed" (any other status); `|| true` collapsed both into a silent
  decommissioned/stale verdict.
- An asset tree already gone at the run's own commit is a decommission, not a
  "changed since", matching the existing here_blob guard.

Docs: the multi-path fleet list was wrong in both directions — pr-size IS
multi-path, and agents-md-integrity and pr-risk were missing. Script comment
and README now list all five and tell you to read the entrypoint's `paths:`
rather than trust the list.

Tests: 74 assertions (was 42), including staged-retirement in both orders, the
local -f decommission branch, the tag-shadowing fixture, and the three input
guards. Verified discriminating by mutation: reverting the OR semantics fails 4
assertions, reverting the branch refspec fails 3.

The direction/ancestry check (a fetched tip that is BEHIND HEAD) is not fixed
here: `--depth=1` leaves FETCH_HEAD parentless, so `merge-base --is-ancestor`
would reject the legitimate re-point. Doing it right needs pr-risk's unshallow
probe, which is exactly the phase-2 decision this PR defers by design.
Deferred to a follow-up.
@mattmillerai

mattmillerai commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review panel in 13c540f (pushed). 9 of the 10 findings fixed, 1 deferred with reasoning on its thread; every thread has a reply and is resolved.

Behaviour fixes (each with a discriminating functional case):

  • Decommission at the tip is now OR, not AND (the 🟠 one). Staged retirement — delete the reusable, clean up its assets later — fell through to the "stale run/re-run" branch and exited green, suppressing the ::warning:: that is the fleet's only signal that live callers hard-fail at startup. It also disagreed with the local -f/-d guards, which already used OR. The warning now names whichever surface went away.
  • NEW_SHA is shape-validated (^[0-9a-f]{40}$, applied to GITHUB_SHA too). It is the one value never derived from a lookup, yet it is emitted verbatim into $GITHUB_OUTPUT — a newline in it injected extra lines, and an injected proceed=true would have won over the proceed=false this script wrote.
  • HEAD is asserted equal to $GITHUB_SHA, so a ref: override in a consuming checkout can no longer have the script compare main against itself and read every comparison as "unchanged".
  • The tip is fetched as refs/heads/main, not the bare name — refspec resolution consults refs/tags/<name> first, and this repo force-moves major tags. A tip that advances between the ls-remote and the fetch is now logged as the benign race it is.
  • Glob-shaped / slash-terminated watched paths are rejected up front, instead of resolving to nothing and making the fleet a permanent silent no-op behind a green run.
  • rev-parse distinguishes "absent from the tree" from "the lookup failed"; || true collapsed both into a silent decommissioned/stale verdict, breaking the header's own promise.
  • An asset tree already gone at the run's own commit is a decommission, not a "changed since" — matching the existing here_blob guard.

Docs. The multi-path fleet list was wrong in both directions: pr-size is multi-path (that reviewer note was mistaken), but agents-md-integrity and pr-risk were both missing. Script comment and README now list all five with their actual asset paths, and say to read the entrypoint's paths: rather than trust a list — which is how this one went stale.

Tests. 42 → 74 assertions, 9 → 14 cases: staged retirement in both orders, the local -f decommission branch, a tag-shadowing fixture, and the three input guards. Verified they discriminate by mutation — restoring the && semantics fails 4 assertions, restoring the bare origin main refspec fails 3.

Deferred (1). The direction/ancestry check — no assertion that the fetched tip is ahead of GITHUB_SHA. The diagnosis is right, but the suggested merge-base --is-ancestor breaks the happy path here: --depth=1 leaves FETCH_HEAD parentless, so it rejects the legitimate re-point. Doing it properly needs bump-pr-risk-callers.yml's --unshallow probe, which is exactly the phase-2 decision this PR defers by design. Reasoning on that thread; a follow-up is queued.

No follow-up filed for the "frozen fleet after a failed newer run" note — every entrypoint already carries workflow_dispatch: {}, which runs at the current tip and proceeds unconditionally. That is the force-proceed, and it pins the right SHA rather than forcing a stale one through.

Verification (all clean, no bump-*-callers.yml touched):

shellcheck -x .github/bump-callers/{bump-callers.sh,preflight.sh,tests/test_bump_callers.sh,tests/test_preflight.sh}
bash .github/bump-callers/tests/test_preflight.sh      # 74 passed, 0 failed
bash .github/bump-callers/tests/test_bump_callers.sh   # 175 passed, 0 failed
python3 .github/agents-md-integrity/check_agents_md.py --root .   # passed (2 pre-existing warnings)

@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-6670 — Decide pr-risk's extra guards for preflight.sh: ancestry/direction check + "did a later commit touch" test — filed as agent-spike (premise unverified)

The following carry agent-spike instead of agent-ok because their reachability claim was not backed by evidence (BE-5378) — the claim is investigated before any code is written, and "the premise does not hold" is a valid, successful outcome:

  • Decide pr-risk's extra guards for preflight.sh: ancestry/direction check + "did a later commit touch" test — no reachability block in the proposal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants