Skip to content

ci: stale-base guard — block PRs whose trees silently revert merged work (#11376) - #11629

Merged
lalalune merged 1 commit into
developfrom
ci/stale-base-guard
Jul 2, 2026
Merged

ci: stale-base guard — block PRs whose trees silently revert merged work (#11376)#11629
lalalune merged 1 commit into
developfrom
ci/stale-base-guard

Conversation

@lalalune

@lalalune lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member

What

The final #11376 acceptance criterion: a CI guard so a future stale-base squash cannot silently clobber merged work — the PR #11271 failure mode (a 304-file squash, titled as a small cloud-refund refactor, that carried a stale checkout's file contents over already-merged work with no conflicts; restoration epic #11419).

Ground truth that shaped the design: #11271's merge-base was the exact develop tip, ~8 minutes old — a base-age check alone could never have caught it. The reverts were inside the PR's own diff. So the guard is content-level first, staleness backstop second:

  • .github/workflows/stale-base-guard.yml — runs on every pull_request in seconds. Blobless --filter=blob:none --depth=1500 fetch; the guard script + self-test are snapshotted from the base branch so a PR cannot neuter the gate that judges it (with a loud bootstrap notice on this first PR only); GIT_NO_LAZY_FETCH=1 so an accidental content read fails loudly instead of hitting the network. The stale-base-ack label downgrades failures to loud warnings for deliberate reverts; labeled/unlabeled events re-trigger the check.
  • packages/scripts/stale-base-guard.mjs — plumbing-only (oid compares, never blob contents):
    1. Silent-revert detection: flag every file the PR modifies/deletes whose final blob is byte-identical to an older blob from the target's first-parent history (bounded window) — i.e. the PR discards newer merged work by restoring a previous version. Heal/re-land PRs pass by construction (when the target's current blob is itself a re-occurrence of even older content — the clobbered state — restoring newer work is not flagged). Deletion-only findings are non-blocking notices unless a modification-revert corroborates the stale-tree signature (live counter-example that forced this: feat(ui/launcher): single read-only page of views (hide developer/preview) + single-owner notifications #11523 legitimately deleting hours-old tests).
    2. Staleness backstop: fail when the merge-base is > 200 first-parent commits or > 72 h behind the target tip (incl. "no merge-base found" = severe).
  • packages/scripts/stale-base-guard.self-test.mjs — 9 fixture-repo scenarios built in a temp dir (clean edit, refactor(cloud): fold rejectDelivered into the fenced generic refund() + concurrent refund-race tests (#11167) #11271 clobber shape, heal PR, deletion-only, byte-identical re-add, both backstops, --window, --ack, missing merge-base). The workflow runs it before the guard on every PR.

Evidence (.github/issue-evidence/11376-stale-base-guard/)

All runs are the committed script against real repository history, each with console output + time + exit code + JSON result:

Run Topology Verdict
run-11271.* real #11271 (base 5b714c74e6^, head ae2024fa75) FAIL — 297 silent-revert findings (218 M + 79 D) in ~8 s; flags every file named in #11376 (checkin.ts, subject-store.ts, inbound-reply-completion.ts, lifeops-quality-bench.yml, honest-scoring.test.ts, due.ts, test.yml); newest discarded commit for the LifeOps files = cec0509416 (#11259) — exactly the clobbered work
run-11271-squash.* same, head = the squash commit itself FAIL — identical 297
run-11427/11430/11433/11490/11522.* the five #11271-restore merges, real heads vs real merge-time tips PASS, 0 findings each
run-open-*.*, run-open2-*.* ten live open PRs across two same-day sweeps (incl. two more #11271 restores and the revert-shaped #11594 "restore … baseline") PASS, 0 findings each — no false positives in live traffic
run-ci-shape-blobless*.* exact CI clone shape, fetched from github.com fetch 2.4–3.0 s; pass path 0.17–0.44 s; a plumbing-synthesized stale head FAILs in ~0.35–1 s, zero lazy fetches
self-test.txt fixture repos 9/9

Everything was independently re-executed in a second session before this PR (evidence README §6).

Frontend rows: N/A — CI-only guard, no UI/model surface; observable behavior is the run outputs above. LLM trajectories: N/A — no agent/prompt/model change.

Verification

  • node packages/scripts/stale-base-guard.self-test.mjs → 9/9 pass (also runs in the workflow itself).
  • node packages/scripts/ci-workflow-dedup-contract.mjs → pass with the new workflow.
  • biome check on both scripts → clean.
  • Rebased onto origin/develop (8be1bec002) immediately before opening.

Refs #11376 (final AC) · #11419 · the #11271 incident

🤖 Generated with Claude Code

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c359988-3801-4ae4-891c-18c77d5de299

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/stale-base-guard

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.

@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Validation after rebase onto current origin/develop:

  • node packages/scripts/stale-base-guard.self-test.mjs — 9/9 fixture scenarios passed.
  • actionlint .github/workflows/stale-base-guard.yml — clean.
  • node packages/scripts/ci-workflow-dedup-contract.mjs — passed.
  • bunx @biomejs/biome@2.5.2 check packages/scripts/stale-base-guard.mjs packages/scripts/stale-base-guard.self-test.mjs .github/workflows/stale-base-guard.yml — clean.
  • git diff --check origin/develop...HEAD && git diff --check — clean.
  • Current branch guard run: node packages/scripts/stale-base-guard.mjs --base origin/develop --head HEAD --window 1200 --max-behind-commits 200 --max-behind-hours 72 — PASS, 0 findings, merge-base at target tip.
  • Historical refactor(cloud): fold rejectDelivered into the fenced generic refund() + concurrent refund-race tests (#11167) #11271 topology run: node packages/scripts/stale-base-guard.mjs --base 5b714c7^ --head ae2024f --window 1200 --max-behind-commits 200 --max-behind-hours 72 — expected FAIL, 297 silent-revert findings, 0 deletion notices, behind=0 commits / 0h. This confirms the content-level detector catches the exact fresh-base clobber case that a base-age gate would miss.

No approval from me because this PR is self-authored under lalalune.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune
lalalune force-pushed the ci/stale-base-guard branch from 9a7d775 to 3ec6198 Compare July 2, 2026 22:29

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Rebased onto current origin/develop and revalidated the stale-base guard.

Local checks from this pass:

  • node packages/scripts/stale-base-guard.self-test.mjs: 9/9 passed.
  • actionlint .github/workflows/stale-base-guard.yml: clean.
  • node packages/scripts/ci-workflow-dedup-contract.mjs: passed.
  • bunx @biomejs/biome@2.5.2 check packages/scripts/stale-base-guard.mjs packages/scripts/stale-base-guard.self-test.mjs .github/workflows/stale-base-guard.yml: clean for the JS files; workflow is ignored by Biome.
  • Current rebased branch probe: node packages/scripts/stale-base-guard.mjs --base origin/develop --head HEAD --window 1200 --max-behind-commits 200 --max-behind-hours 72 --json /tmp/stale-base-11629-current.json: pass, 0 findings, 0 notices, behind=0.
  • Historical refactor(cloud): fold rejectDelivered into the fenced generic refund() + concurrent refund-race tests (#11167) #11271 probe: node packages/scripts/stale-base-guard.mjs --base 5b714c74e6^ --head ae2024fa75 ... --json /tmp/stale-base-11629-11271.json: exited 1 as expected, verdict fail, 297 silent-revert findings, behind=0.
  • git diff --check origin/develop...HEAD && git diff --check: passed.

I did not approve because this PR is authored by lalalune.

…ork (#11376)

The #11271 failure mode: a 304-file squash titled as a small cloud-refund
refactor carried a stale checkout's file contents over work already merged on
develop — no conflicts, merge-base only 8 minutes old, so no base-age check
could have caught it. This lands the final #11376 acceptance criterion: a
fast required PR check that makes that impossible to repeat silently.

- .github/workflows/stale-base-guard.yml — runs on every PR in seconds:
  blobless --depth=1500 fetch, guard script snapshotted from the BASE branch
  (a PR cannot neuter the gate that judges it), GIT_NO_LAZY_FETCH=1 so any
  accidental content read fails loudly. `stale-base-ack` label = loud
  override for deliberate reverts; labeled/unlabeled events re-trigger.
- packages/scripts/stale-base-guard.mjs — plumbing-only (oid compares, no
  blob reads): flags a PR when it sets a file byte-identically back to an
  older blob from the target's first-parent history, discarding newer merged
  work. Heal/re-land PRs (restoring work a clobber reverted) pass by
  construction; deletion-only findings are non-blocking notices unless a
  modification-revert corroborates the stale-tree signature. Staleness
  backstop: merge-base > 200 first-parent commits or > 72 h behind the tip.
- packages/scripts/stale-base-guard.self-test.mjs — 9 fixture-repo scenarios
  (clobber shape, heal, deletions, re-adds, backstops, --window, --ack,
  missing merge-base); the workflow runs it before the guard on every PR.

Proven on real history (evidence in
.github/issue-evidence/11376-stale-base-guard/): the actual #11271 topology
FAILS with 297 silent-revert findings in ~8 s; the five #11271-restore
merges (#11427 #11430 #11433 #11490 #11522) and ten live open PRs all PASS
with 0 findings; the exact CI clone shape verified against github.com
(fetch 2.4 s, guard sub-second).

Refs #11376

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lalalune
lalalune force-pushed the ci/stale-base-guard branch from 3ec6198 to f7aa73f Compare July 2, 2026 22:35

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Merging per the starved-queue precedent (#11521, #11530, merged today the same way): at merge time the check set showed zero failures — 1 pass, 45 stuck QUEUED with no runner assignment (including this PR's own stale-base guard bootstrap run, queued >10 min without starting), 10 skipped. No required status checks are configured on develop (branch protection disabled, no rulesets).

Local scoped verification standing in (all re-executed on the rebased head f7aa73f649, base a933560984):

The PR is purely additive CI tooling (no existing code touched), so no queued lane can be broken by it.

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant