Skip to content

perf(source-control): merge a stale base before push in push-branch.sh - #10568

Merged
kyle-sexton merged 9 commits into
mainfrom
perf/push-branch-sync-stale-base
Oct 28, 2026
Merged

kyle-sexton merged 9 commits into
mainfrom
perf/push-branch-sync-stale-base

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: push churn measured on #10491 (about 75 min, 3 stale-base refusals)

Summary

A push waits in the rate gate (gate.sh run KEY -- <push>) for minutes, sometimes over an hour while the token breaker is tripped. Meanwhile main moves, and the pre-push hook (scripts/check-affected.sh running scripts/check-stale-base-overlap.sh) refuses the push as "STALE BASE with overlapping paths". Each refusal costs a manual merge of main, a re-test and a re-queue: #10491 took about 75 minutes and 3 refusals to land one push, and #10498 was refused once the same way. gate.sh stays a pure rate gate, so the fix is in source-control's push helper, which runs inside the gate after admission, when main is current.

Fix

plugins/source-control/skills/pull-request/scripts/push-branch.sh gains a sync-stale-base step on the operator path, just before the push:

  1. Resolve the PR's base branch: --pr's existing gh pr view read, now with baseRefName; otherwise gh pr view <branch>; with no PR yet, the fetch remote's default branch. Then git fetch it from the fetch remote.
  2. If the branch is behind the base on overlapping paths, merge the base in. The overlap rule is the checkout's scripts/check-stale-base-overlap.sh --check <remote>/<base> when that file exists (exit 1 merges, 0 does not, anything else skips with a note). Otherwise it is the paths both sides changed since the merge-base, read with git.
  3. git merge --no-edit, never a rebase. Git signs it itself when commit.gpgsign is true; -S is added when HEAD is signed. Nothing is hard-coded.
  4. A conflict runs git merge --abort and exits 4, pushing nothing. gate.sh uses 0 to 3 itself, so 4 stays distinct through gate.sh run.
  5. Then the push runs as before. The pre-push hook tests the merged tree.

The step is skipped with a stderr note when the base cannot be read or fetched, the branch is checked out in no worktree, tracked files have uncommitted edits, or a merge is in progress. --check and lane pushes never sync.

Config: a new team key sync_stale_base (boolean) in docs/conventions/source-control.yaml, read from the checkout through lib/convention-key.sh, added to the schema and reference/config-resolution.md. It defaults to false because an unrequested merge commit in a consumer's branch is a behavior change, and without a refusing hook it buys little. This repository turns it on. --sync-stale-base and --no-sync-stale-base override it for one push.

The create (§2.4.1) and ready (2.5.4) references and babysit-prs' safety rules describe the merge and exit 4.

Verification

  • push-branch.test.sh: 170 passed, 0 failed. New cases cover: current base (fetch, no merge, push); stale base with no overlap (no merge); overlap (two-parent merge, first parent the old tip, second the base tip, pushed, unsigned in an unsigned repository); commit.gpgsign=true and a signed tip with gpgsign unset (merge signed); conflict (exit 4, no MERGE_HEAD, HEAD unchanged, nothing pushed); disabled by no config, by --no-sync-stale-base and by --check (no fetch); --sync-stale-base with no config; a stacked PR base; the repository overlap script winning over git; a dirty tree. Before the implementation, 17 of the new checks failed.
  • Used live: this branch's own push went through the new helper inside the gate. Main had moved 14 commits with an overlap on babysit-prs/reference/safety.md. The helper merged origin/main (merge commit 30d0e2e693, signed, first parent the old tip), and the pre-push hook passed on the merged tree.
  • Fixture signing: on a machine with a global commit.gpgsign, fixture commits went through the machine's signer. That signer failed intermittently when the pre-push check ran suites in parallel (failed to write commit object, exit 128; reproduced with 16 concurrent commits, 1 failed), and the push was blocked. push-branch.test.sh now gives its fixtures an empty global config. The two babysit-prs fixtures that failed set commit.gpgsign=false per repository, as test_prune_babysit_worktrees.py already does. check-changed-skills.sh then passed 5 runs out of 5.
  • scripts/check-fixture-git-isolation.sh: OK. gh-forge.test.sh: 34 passed. check-convention-yaml.sh validates, and its test passed 45. babysit-prs Python tests: 3255 passed. shellcheck and shfmt are clean.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits October 27, 2026 19:44
A push that waits in the rate gate while main moves is refused by the
pre-push stale-base overlap check, costing a manual merge, re-test and
re-queue each time. push-branch.sh now fetches the PR's base just before
the push and, when the branch is behind it on overlapping paths (the
repo's scripts/check-stale-base-overlap.sh when present, else git path
lists), merges it with --no-edit, signing per the repo. A conflict aborts
the merge and exits 4 without pushing. Off by default; on through
sync_stale_base in docs/conventions/source-control.yaml (on for this
repository) or --sync-stale-base, off through --no-sync-stale-base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Capture the overlap and signature reads instead of piping into grep -q,
which pipefail can misread; name the new HEAD after a sync merge; and say
in the babysit safety rules and ready 2.5.4 what a sync merge or exit 4
means for those callers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a machine with a global commit.gpgsign, fixture commits in
push-branch.test.sh and two babysit-prs tests signed through the
machine's signer, which failed intermittently when the pre-push check
ran the suites in parallel ("failed to write commit object", exit 128),
blocking the push. push-branch.test.sh now gives its fixtures an empty
global config, and the two babysit-prs fixtures set commit.gpgsign=false
repo-locally, as test_prune_babysit_worktrees.py already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 28, 2026 01:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T01:44:11.152236Z 30d0e2e Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions 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.

claude-security-review: 1 inline finding(s) on 30d0e2e693a7d96f5b11d90c1b16d6746af1296f; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed d0a420be6d235404fe521880e6e6a1b28a377f58: 0 finding(s), 0 as inline comments.

Incremental security review of d0a420b (since 4ccabcc): no security findings.

The change swaps the sync_stale_base setting read in push-branch.sh from a network default-branch lookup to local refs. It adds local_default and setting_at. It also adds a "pending" state for pushes made without --pr. The docs and tests are updated to match. No trust-control path in the list is touched.

What I checked:

  • Setting source stays off the pushed head. setting_at reads docs/conventions/source-control.yaml only from refs/remotes/<remote>/<default> (push-branch.sh:372-385). It never reads the working tree or the pushed branch. A head that turns the setting on still enables nothing.
  • A fork cannot enable the sync. Without --pr, any fetched remote turning the setting on only sets on=pending (push-branch.sh:400-403). The forge read then resolves the real base repository. Before any fetch or merge, setting_at "$remote" for the base repository's remote must be true (push-branch.sh:421-423). The tests cover "fork on, base off" (nothing fetched) and "base on, fork off" (merged).
  • Failure paths skip rather than sync. In the gh pr view failure paths, the step skips for lack of a base or remote, or checks the fetch remote's own setting. None of them proceeds on an unverified setting.
  • Local default-branch detection is not attacker-writable from the PR. The refs/remotes/<r>/HEAD symref and the main/master/trunk fallback depend only on what the operator's clone has fetched. They cannot be influenced by the pull request's head. The only residual is staleness of a local ref, which affects availability, not authorization.
  • Overlap script and merge path are unchanged. The overlap script still comes from git archive "$ref", the base ref and never the head. The merge and signing logic were not modified in this increment.
  • Test and doc changes. They are inert: a gh/ls-remote log shim and fixtures, plus the matching prose in config-resolution.md.

Manifest items: no plugin.json or marketplace.json change in this increment.

critical-findings: 0

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed d0a420be6d235404fe521880e6e6a1b28a377f58: 0 finding(s), 0 as inline comments.

Incremental review of config-resolution.md, push-branch.sh and push-branch.test.sh since 4ccabcc. I found nothing to block or flag. I read the diff and the surrounding script. I did not run the test suite, so I can't confirm the new cases pass.

What changed. The setting read in sync_stale_base no longer calls the network. The new local_default and setting_at helpers read sync_stale_base from each remote's locally fetched default branch. Without --pr, the step uses the forge (gh pr view) only when some fetched remote turns the setting on (on=pending). It then re-checks the setting on the PR's base remote.

Traced and correct:

  • A forge read of the PR's base repository returns the base remote. A fork that turns the setting on, where the base does not, then exits without fetching. A base that turns it on, where the fork does not, still syncs.
  • A failed forge read leaves base empty, so the existing "base branch could not be read" skip applies.
  • The pending path with no matching remote falls through to the existing "no remote fetches the base repository" skip.
  • The setting still comes only from refs fetched from base remotes, never from the pushed head, so the earlier guarantee that the head cannot switch on the step or choose the script holds.
  • The new tests are consistent with sync_run, which clears $LOG on every run. That means gh.log, ls-remote.log and fetch.log count only the run under test.
  • The docs and the script header match the new behavior.

Noted, not flagged. local_default falls back to the first of main, master or trunk when refs/remotes/<remote>/HEAD is unset. A remote whose default is another name, with no local HEAD symref, therefore reads as unset and the step quietly doesn't sync. Before this change, the code asked the forge for that default. The docs say "as the clone last fetched it", so this follows the documented behavior. It only affects the opt-in setting, and a wrong guess ends in a skipped sync, not a wrong push.

No spend beyond prepaid or subscription usage is involved, and nothing here calls for a hold.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30d0e2e693

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

kyle-sexton and others added 3 commits October 27, 2026 23:21
… base

The sync step read sync_stale_base from the pushed checkout and ran that
checkout's scripts/check-stale-base-overlap.sh, so a head checked out to
review or fix could switch the step on and run its own script. The
setting is now read from the base repository's default branch as the
clone last fetched it, and the overlap script (with the base's scripts/)
is extracted from the base ref with git archive; nothing in the head is
run. The base is fetched from the remote that is the PR's base
repository, not the fetch remote, so a head that fetches from a fork
never syncs against the fork's same-named branch; with no such remote
the step is skipped with a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144XNKz9sKFKYo4gQtVW78R
Lane-Activity: pr-unblock/resolve-conflicts

@github-actions github-actions 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.

claude-review: 1 inline finding(s) on 5cf449da56518eea084d5dd0d7081fe8467ee997; the summary is in this lane's pull request comment.

claude-lane: claude-review

…' into perf/push-branch-sync-stale-base

Integrates the lane's merge of main (5cf449d). config-resolution.md
keeps this branch's sync_stale_base text and main's new dialect key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions 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.

claude-review: 2 inline finding(s) on 4ccabccc0298ae39bbf7325b6e03a496a8b7572d; the summary is in this lane's pull request comment.

claude-lane: claude-review

Without --pr, sync_stale_base was read from the fetch remote's default
branch before the PR's base repository was known, so a fork's copy
decided the step. It is now read from the remote that is the base
repository: the forge is asked for it only when some fetched remote turns
the setting on, so with the setting unset everywhere the step makes no
network call. The default branch is taken from the clone's fetched refs
(the remote HEAD symref, else main, master or trunk), never ls-remote, on
that off path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 9638b55 Oct 28, 2026
42 checks passed
@kyle-sexton
kyle-sexton deleted the perf/push-branch-sync-stale-base branch October 28, 2026 05:00
@github-actions

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 6, survived 2, timeout 0, no coverage 0, invalid 0, not run 0.
Covered-code score 75%; mutation score 75%.

File Killed Survived Timeout No coverage Invalid Not run
plugins/source-control/skills/pull-request/scripts/push-branch.sh 6 2 0 0 0 0

Surviving mutants

Location Operator Disposition Cause
plugins/source-control/skills/pull-request/scripts/push-branch.sh:442 SBR productive input-gap
plugins/source-control/skills/pull-request/scripts/push-branch.sh:454 ROR productive input-gap

The run's summary has the full report.

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