Skip to content

fix(bump-callers): commit each repo's files atomically via the Git Data API (BE-3902)#46

Merged
mattmillerai merged 8 commits into
mainfrom
matt/be-3902-atomic-git-data-commit
Jul 21, 2026
Merged

fix(bump-callers): commit each repo's files atomically via the Git Data API (BE-3902)#46
mattmillerai merged 8 commits into
mainfrom
matt/be-3902-atomic-git-data-commit

Conversation

@mattmillerai

Copy link
Copy Markdown
Contributor

STACKED — merging lands on matt/be-3896-multi-file-per-repo (owned by @mattmillerai, PR #45), NOT main. This PR is based on PR #45's branch. Merge #45 first; GitHub then auto-retargets this to main. Do not "merge to main" directly.

ELI-5

When we bump a caller repo, it might pin our reusable workflow from more than one file. The current code commits those files one at a time (a separate API call per file). If the 3rd call fails after the 1st and 2nd already landed, the branch is left with a partial bump — and if an open PR points at that branch, someone could merge a half-done bump. This PR makes all of a repo's files land in one commit or none: we build the whole commit off to the side (blobs → tree → commit) and only then move the branch to it in a single step. A failure anywhere leaves the branch untouched.

What changed

bump_repo() Pass 2 no longer loops PUT repos/<repo>/contents/<file> per file. Instead, for each staged file it:

  1. POST git/blobs — create a blob for the new content.
  2. POST git/trees — build one tree with base_tree=MAIN_SHA (the resolved default-branch tip) carrying all staged blobs, so every other path in the repo is preserved and only the bumped files appear.
  3. POST git/commits — create one commit with that tree, parented on MAIN_SHA (clean "bump to @short" diff vs the default branch).
  4. POST/PATCH git/refs/heads/<branch> — point the stable bump branch at the finished commit (create if new, else force). The ref moves in one step from its old state to the complete commit; it never transiently sits at an empty/partial tree.

Because the commit is fully built before the ref moves, a failure at any step leaves only dangling (GC-able) blobs/tree/commit and the branch untouched — all-or-nothing. This closes the residual BE-3896 partial-bump window: a mid-sequence failure that previously left earlier files committed on the branch (and, if an open PR pointed at it, a partial-yet-mergeable bump).

Test changes

tests/test_bump_callers.sh — the gh stub now models the Git Data API calls instead of the per-file Contents PUT:

  • POST git/blobs → captures each blob's decoded content (put.$n.txt / put.last.txt; count = number of blobs) — same content assertions as before.
  • POST git/trees → records the tree's path list as branch_files (the atomic branch's final file set).
  • POST git/commits → returns a commit sha.
  • POST/PATCH git/refs → points the branch (no-op for file-set modeling).

The BE-3896 monorepo assertion is preserved: both files still land on the one branch (now via one tree carrying both blobs). All 69 assertions pass; shellcheck -x is clean.

Verification

  • shellcheck -x .github/bump-callers/bump-callers.sh .github/bump-callers/tests/test_bump_callers.sh → clean.
  • bash .github/bump-callers/tests/test_bump_callers.sh69 passed, 0 failed.

Judgment calls

  • Tree entry mode 100644. Git tree entries need an explicit file mode; the bumped files are .github/workflows/*.yml callers, which are never executable, so 100644 is correct. (The old Contents PUT preserved mode implicitly; preserving it here would require an extra read of each existing tree entry's mode for no real-world benefit.)
  • One commit instead of N. The branch now carries a single "bump to @short" commit rather than one commit per file. The PR diff vs the default branch is identical; reviewers just see one commit.
  • Commit author is the authenticated token identity (the Cloud Code Bot app token), exactly as the previous Contents PUT — no author/committer fields are set, so no attribution change.
  • Stacked on PR fix(bump-callers): commit all of a repo's files onto one bump branch (BE-3896) #45 because the code this modifies (bump_repo() Pass 2's per-file PUT loop) only exists on PR fix(bump-callers): commit all of a repo's files onto one bump branch (BE-3896) #45's branch, not on main.

mattmillerai and others added 5 commits July 20, 2026 20:15
…-3882)

The dispatcher embedded the new short-SHA in the branch name
(ci/bump-<tag>-<short>), so every bump minted a unique branch and thus a
brand-new PR in each caller repo, leaving the prior bump PRs open. The stack
made it unclear which pin was current.

Make the branch stable per (repo, TAG) — ci/bump-<tag> — and update it in
place: each run rebuilds the branch from the caller's current default-branch
tip (a clean single-commit "bump to @short" diff), then, if a bump PR is
already open for that branch, refreshes its title/body to the new SHA instead
of opening another; a fresh PR is opened only when none is open (first bump,
or the prior one merged/closed since the last run). Result: at most one open
bump PR per (repo, workflow) at any time. Applies to both the cursor-review
and agents-md fleets via the shared script.

Adds test coverage for the update-in-place path (open PR is edited, not
re-opened) and the create path (no edit when no PR is open).
…or (BE-3882)

Harden the stable-branch bump PR reuse from the review panel:

- Existing-PR lookup: use `.[0].number // empty` (a bare `.[0].number`
  prints the literal `null` on an empty list, so the first bump for a
  repo — and every bump after the prior PR merged/closed — ran
  `gh pr edit null` and failed the caller). Also exclude
  cross-repository PRs (`isCrossRepository == false`): `--head` matches
  by branch name across forks, so with the now-predictable branch name
  an attacker could pre-open a fork PR the bot would stamp instead of
  bumping the real caller.
- Create path: `return 1` on a genuine `gh pr create` failure. The
  update-in-place path already handles an open PR, so reaching create
  means none exists and a failure is a real miss — record it in FAILED
  instead of reporting success.
- Serialize each fleet with a `concurrency:` group (cancel-in-progress:
  false) so overlapping runs can't race the shared stable branch; the
  newest pending run wins so the latest SHA is committed.
- Tests: the gh stub now faithfully models `gh pr list --json --jq` by
  running the real jq over the JSON gh would return, so the no-open-PR
  case reproduces production instead of masking it; add a fork-PR decoy
  regression case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(BE-3896)

Group CALLERS entries by repo before the bump loop and reset the stable
branch ONCE per repo, then commit each of that repo's files onto it with
successive PUTs (each carrying that file's own blob SHA). The old per-entry
loop reset the branch before every file, so a second same-repo entry's reset
discarded the first entry's commit and the PR shipped only the last file while
every entry reported success — a silent partial bump. Only affects repos that
appear more than once in a caller list; single-file repos are unchanged.

The test stub now models the one bump branch's committed file set (a ref
reset truncates it, a PUT appends), and a new same-repo two-file case asserts
BOTH files land on the branch — it fails against the pre-fix script.
…bumps (BE-3896)

Address cursor-review panel findings on the multi-file-per-repo bumper:

- Distinguish a genuine 404 (per-file skip) from any other fetch error
  (auth/rate-limit/5xx/network); a transient error now fails the whole repo
  instead of silently shipping a PR that omits the un-fetched file.
- Resolve the default-branch tip (MAIN_SHA) up front and pin every Pass-1 blob
  fetch to that immutable SHA, closing a TOCTOU race with a moving default branch.
- De-duplicate staged files by path so a repo listed twice for the same file
  commits it once (a second PUT with a now-stale blob sha would 409 the repo).
- Skip an already-pinned file by comparing rewritten-vs-original content instead
  of grepping for NEW_SHA anywhere, which also repairs a half-bumped file.
- Anchor the 40-hex SHA rewrite to the github-workflows / workflows_ref pin
  contexts so a full-SHA pin of another action (actions/checkout@<sha>) is not
  clobbered.
- Collect a caller entry's label only once its file is confirmed staged, so a
  skipped entry's label never lands on the real bump PR.
- Exact-match label de-dup (GitHub label names may contain the `|` sentinel).

Adds functional tests for each: 404-skip vs transient-fail, same-file de-dup,
non-github-workflows pin preservation, half-bump repair, and already-pinned skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mattmillerai mattmillerai added agent-coded Authored by the agent-work loop cursor-review Multi-model cursor review labels Jul 21, 2026
@coderabbitai

coderabbitai Bot commented Jul 21, 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: 39 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: 8975a024-6909-4cdf-bbd4-1e1f91b51010

📥 Commits

Reviewing files that changed from the base of the PR and between be4fd67 and 17b8445.

📒 Files selected for processing (2)
  • .github/bump-callers/bump-callers.sh
  • .github/bump-callers/tests/test_bump_callers.sh
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3902-atomic-git-data-commit
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3902-atomic-git-data-commit

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

@mattmillerai
mattmillerai marked this pull request as ready for review July 21, 2026 07:47

@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 4 finding(s).

Severity Count
🔴 Critical 1
🟢 Low 2
⚪ Nit 1

Panel: 6/8 reviewers contributed findings.

Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)

Comment thread .github/bump-callers/bump-callers.sh Outdated
Comment thread .github/bump-callers/tests/test_bump_callers.sh Outdated
Comment thread .github/bump-callers/bump-callers.sh
Comment thread .github/bump-callers/bump-callers.sh Outdated
…3902)

The Git Data Create-a-tree call was passing $MAIN_SHA — the tip *commit*
SHA — as base_tree, but that API requires a *tree* SHA. A commit SHA either
422s the call (every bump fails) or yields a tree carrying ONLY the bumped
files, so merging the PR would delete every other path in the caller repo.
Resolve the tip commit's tree via GET git/commits and pass that as base_tree;
$MAIN_SHA stays correct as the new commit's parent.

Also send the blob create body on stdin (--input -) instead of --field on argv,
matching the adjacent tree/commit calls and keeping content off cmdline/ARG_MAX.
Test stub now models GET git/commits and asserts base_tree is the resolved tree
SHA, not the commit SHA, so the offline suite catches this class of bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from matt/be-3896-multi-file-per-repo to matt/be-3882-stable-bump-branch July 21, 2026 17:33
Base automatically changed from matt/be-3882-stable-bump-branch to main July 21, 2026 17:34
…it-data-commit

# Conflicts:
#	.github/bump-callers/bump-callers.sh
#	.github/bump-callers/tests/test_bump_callers.sh
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Self-review verdict — HEAD 5567aa2 (post-conflict-resolution against main).

Reviewed the full diff fresh: the atomic-commit implementation (blob → tree → commit → single ref move) genuinely delivers the all-or-nothing guarantee — every Git Data API call before the ref move is inert/unreferenced on failure, so a mid-sequence error leaves the branch untouched. base_tree correctly resolves to the tip's tree SHA (not commit SHA), matching the earlier Critical bot finding's fix. All 4 prior bot findings verified genuinely addressed in code, not just claimed. Repo-name masking traced end-to-end, intact. Shellcheck clean; test_bump_callers.sh 71/71 passing.

Merge conflict against main (from PR #45 landing first) resolved by keeping this PR's atomic approach wholesale, since it fully supersedes #45's now-merged sequential-PUT fix — confirmed the resolved files are byte-identical to the pre-merge PR tip, zero drift introduced.

Babysitter is done; your turn.

…it-data-commit

# Conflicts:
#	.github/bump-callers/bump-callers.sh
@mattmillerai

Copy link
Copy Markdown
Contributor Author

Merge-conflict resolution — HEAD 17b8445 (merged origin/main in).

main had moved further since this branch's last merge (BE-3874's rejection ledger, BE-1814's wire-bot-identity landing on bump-callers.sh, and the new pr-size fleet registration), producing one real conflict hunk in bump_repo()'s local-variable declarations.

Root cause: BE-1814's wire-bot feature was built on top of the old per-file Contents-API loop (PEND_FILE_ENC/PEND_BLOB/BLOB_SHA), which this PR's atomic Git Data API rewrite (BE-3902) replaced entirely — this branch's Pass 1/Pass 2 never needed a pre-fetched blob SHA since Pass 2 creates fresh blobs from the rewritten content. Verified PEND_FILE_ENC/PEND_BLOB/BLOB_SHA are unreferenced anywhere else in the file, so the resolution keeps this branch's PEND_FILE/PEND_CONTENT naming and Pass-2 structure, and folds in only the two new locals the wire-bot code actually needs (WIRE_BOT, WIRING_ADDED_ANY). Everything else in that region (the wire-bot staging block, the WIRING_ADDED_ANY → PR-body note) merged cleanly with no overlap.

Verified post-merge:

  • shellcheck -x clean
  • bash .github/bump-callers/tests/test_bump_callers.sh — 88/88 passing (was 71 before main's wire-bot tests merged in)
  • cursor-review / agents-md-integrity / groom Python suites — 40/18/37 passing
  • All 4 prior Cursor Review panel findings (1 critical: base_tree must be a tree SHA, not commit SHA) confirmed still fixed in code post-merge, not just marked resolved

gh pr view now reports mergeable: MERGEABLE. CodeRabbit's "fail" check is its own rate-limit banner (unrelated to this diff) — Socket Security and shellcheck+functional both pass.

@mattmillerai
mattmillerai merged commit 49018c5 into main Jul 21, 2026
3 of 4 checks passed
@mattmillerai
mattmillerai deleted the matt/be-3902-atomic-git-data-commit branch July 21, 2026 17:55
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.

1 participant