Fix #2393: push slice integration branch by SHA, not parent ref name - #2395
Conversation
The orchestrator's per-pipeline worktree is checked out on `<branch>/work` and has no local ref matching `<parent_branch>` — only `refs/remotes/origin/<parent_branch>` after a fetch. The old refspec `<parent_branch>:refs/heads/<integration_branch>` resolved the source side against the worktree's local refs and failed every slice push with `src refspec X does not match any` (the fourth latent regression in the slice-DAG creation chain after #2369, #2370, #2372). Fix: fetch the parent into the local odb, resolve to a SHA on origin via `git ls-remote`, then push `<sha>:refs/heads/<integration_branch>`. Pushing an explicit SHA bypasses local ref-name resolution entirely and surfaces "parent missing on origin" as a clear failure instead of git's confusing src-refspec error.
There was a problem hiding this comment.
Review
I traced the diff against the linked issue (#2393), the call site in orchestrator/routes/pipelines.py:11118, the helper functions (fetch_branch, get_remote_branch_sha), and the gateway-side push path (/api/v1/git/push + _SLICE_INTEGRATION_BRANCH_RE exemption + extract_branch_from_refspec). I also ran the new test file locally — all 8 tests pass.
The fix is correct, minimal, and well-targeted. Approving.
Why the fix works
The root cause from #2393 is sharp: the orchestrator's per-pipeline worktree is on <branch>/work, and parent_branch exists only as refs/remotes/origin/<parent_branch> after a fetch — there is no local ref by the bare name. git push <sha>:refs/heads/<integration> resolves the source side as a SHA and bypasses local-ref-name resolution entirely, which is the right mechanical fix.
The pipeline is now: best-effort fetch (so the SHA is in the local odb) → ls-remote to resolve parent to a SHA on origin → push the SHA. Failing fast on missing-parent-on-origin gives a clear False return instead of git's confusing "src refspec X does not match any". The gateway's extract_branch_from_refspec (gateway/policy.py:1043) splits on : and takes the destination, so the SHA-prefixed refspec passes the existing branch-ownership and _SLICE_INTEGRATION_BRANCH_RE checks unchanged. The is_infrastructure_push short-circuit (gateway/gateway.py:1342) keeps role/phase file restrictions out of the picture.
Non-blocking observations
1. Small TOCTOU window between fetch and ls-remote (gateway_client.py:1614–1644). The fetch updates refs/remotes/origin/<parent> to the SHA-at-fetch-time; ls-remote separately re-queries origin and may return a newer SHA if origin advanced in between. If that happens, the push of <new_sha>:refs/heads/<integration> will fail because the new SHA isn't in the local odb (only the older fetched SHA is). The failure is graceful (caught by the bare except, returns False, slice marked failed), so this isn't a correctness blocker, but the issue's "Option 1" — pushing refs/remotes/origin/<parent>:refs/heads/<integration> after the fetch — would have closed the race entirely without needing the extra ls-remote round-trip. Worth considering as a follow-up if slice creation ever becomes flaky in a hot pipeline.
2. test_fetch_failure_is_non_fatal (test_create_slice_integration_branch.py:181–211) over-promises slightly. The test stubs _make_request and a valid parent_sha, so it only verifies that create_slice_integration_branch doesn't bail out when fetch_branch returns False. In production, fetch failure is only non-fatal when the SHA happens to already be in the local odb from a prior step — if the worktree was just created and the fetch failed transiently, the push will fail with "fatal: bad object". The test name implies a stronger guarantee than the test actually demonstrates. Renaming to something like test_fetch_returning_false_does_not_short_circuit would be more precise.
3. Three round-trips per slice with three separate synthetic sessions. Each of fetch_branch, get_remote_branch_sha, and the push registers + deletes its own synthetic session (different temp_container_ids, all flowing through register_session + delete_session). For a 15-slice pipeline that's 45 gateway calls and 45 session lifecycles. Not blocking, but if slice spawn latency becomes a concern, register one synthetic session up front and pass bearer_token through to all three calls — the slice-branch session already has the right synthetic=True + branch=integration_branch shape for the push. (Relatedly: the helper signatures don't currently accept a pre-registered token, so this would be a small refactor, not a one-liner.)
4. Logging on success now includes parent_sha (good) but the failure log only includes it when the failure happens after SHA resolution. The early-return at gateway_client.py:1644 (parent not on origin) logs parent_branch but not parent_sha (which is None); the later except always has a real SHA. That's actually correct — just noting it because the asymmetry might trip up someone grepping logs for parent_sha. Not worth changing.
Tests
The 8-test suite is well-shaped: it pins the SHA refspec format, the call ordering (fetch → ls-remote → push), fail-fast on missing parent, session cleanup on push failure, and the two short-circuits (empty branch, integration == parent). The session-metadata test confirms the synthetic flag + integration-branch + role still propagate to register_session, which is the contract that drives the gateway-side _SLICE_INTEGRATION_BRANCH_RE exemption. Good regression coverage.
The PR author's own test plan correctly notes the still-open gap: there's no end-to-end multi-slice integration test that would have caught the original #2393 (or #2369/#2370/#2372 before it). That's the right thing to track separately — adding it here would balloon scope.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The reviewer on #2395 noted the original name over-promised: it implies fetch failure is recoverable in production, but the test only verifies that create_slice_integration_branch doesn't short-circuit when fetch_branch returns False — the SHA must still happen to be in the local odb for the subsequent push to succeed. Rename to test_fetch_returning_false_does_not_short_circuit and clarify the docstring with the production-failure note.
|
Thanks for the careful trace and the four observations. Per-item disposition: 1. TOCTOU window between fetch and ls-remote — 2. 3. Three round-trips per slice with three separate synthetic sessions — 4. Logging asymmetry on — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-review
The only change since 7ea48a8 is a single test rename + docstring expansion in orchestrator/tests/test_create_slice_integration_branch.py:181-211 — directly addressing observation #2 from my previous review. No production code touched.
Verification
git fetch origin main+gh api .../compare/...confirms one file changed, +12 / -4 lines, no other deltas.test_fetch_failure_is_non_fatal→test_fetch_returning_false_does_not_short_circuit. The new name is precise: it pins the control-flow contract (don't bail whenfetch_branchreturnsFalse) without overclaiming production recoverability.- The added docstring note is accurate: a transient fetch failure on a fresh worktree does leave the SHA absent from the local odb, and
git push <sha>:refs/heads/...will fail withfatal: bad objectin that case. Calling that out in the test itself is the right place — anyone reading the test no longer mistakes it for a stronger guarantee. - Other observations dispositioned correctly:
No new issues
I re-read the test with the renamed identity to make sure no other test or doc still references the old name. grep -n test_fetch_failure_is_non_fatal returns nothing — the rename is complete and self-contained.
Approving.
— Authored by egg
|
egg review completed. View run logs 3 previous review(s) hidden. |
Summary
orchestrator.gateway_client.create_slice_integration_branchwas building its push refspec as<parent_branch>:refs/heads/<integration_branch>. The source side resolved against the orchestrator's per-pipeline worktree, which is checked out on<branch>/workand has no local ref matching<parent_branch>— onlyrefs/remotes/origin/<parent_branch>after a fetch. Every slice push failed withsrc refspec X does not match any(the fourth latent regression in the slice-DAG creation chain after Fix #2367: pass pipeline_branch into _sync_worktree_with_remote #2369, Fix #2368: synthetic-session exemption for slice integration-branch pushes #2370, Slice integration-branch push blocked by role-based path allowlist (false positive on branch-creation-only pushes) #2372).git ls-remote(existingget_remote_branch_shahelper), then push<sha>:refs/heads/<integration_branch>. Pushing an explicit SHA bypasses local ref-name resolution entirely and is robust to any local-ref-naming weirdness in the worktree. Also surfaces "parent missing on origin" as a clearFalsereturn instead of git's confusing src-refspec error.orchestrator/tests/test_create_slice_integration_branch.pypin: SHA-shaped refspec, fetch → ls-remote → push ordering, fail-fast on missing parent, fetch failure non-fatal, push-failure session cleanup, and the no-op short-circuits.Closes #2393.
Test plan
pytest orchestrator/tests/test_create_slice_integration_branch.py— 8 new tests passpytest orchestrator/tests/test_slice_run_loop_integration.py— existing slice tests still green (32 pass)make lintissue-2261-v5(or any multi-slice implement-phase pipeline) end-to-end and confirm slice integration branches are created on origin and agents spawn — the end-to-end multi-slice test gap flagged in Slice integration-branch push blocked by role-based path allowlist (false positive on branch-creation-only pushes) #2372 is still open and tracked separately