Skip to content

Commit 856685e

Browse files
Phlogistiqueclaude
andauthored
Push updated heads before retargeting PR bases in the fan-out path (#42)
The squash-merge fan-out retargeted every updated child PR onto the target branch and only afterwards pushed the new heads, batched into a single non-atomic push together with the merged-branch deletion. If the push failed (e.g. someone pushed to a child mid-run, rejecting the plain push) or a pr edit died partway through the loop, set -e aborted the run with PRs already retargeted but their heads stale - and unlike the conflict-resume path there is no label to re-trigger the action, so nothing ever repaired them. Apply the ordering the resume path already uses: push the updated heads first, then flip the bases, and delete the merged branch last (deleting a PR's base branch closes the PR, so every child must be off it first). A failed push now leaves the PRs untouched on their old base. The unit test captures the run transcript and asserts the push -> retarget -> delete order; it fails against the previous code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5329e52 commit 856685e

3 files changed

Lines changed: 31 additions & 12 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ This action tries to fix that in a transparent way. Install it, and hopefully th
1919
1. Triggers when a PR is squash merged
2020
2. Finds PRs that were based on the merged branch (direct children only)
2121
3. Creates a synthetic merge commit with three parents (child tip, deleted branch tip, squash commit) to preserve history without re-introducing code
22-
4. Updates the direct child PRs to base on trunk now that the bottom change has landed
23-
5. Pushes updated branches and deletes the merged branch
22+
4. Pushes the updated branches
23+
5. Updates the direct child PRs to base on trunk now that the bottom change has landed
24+
6. Deletes the merged branch
2425

2526
**Note:** Indirect descendants (grandchildren, etc.) are intentionally not modified. Their PR diffs remain correct because the merge-base calculation still works—the synthetic merge commit includes the original parent commit as an ancestor. When their direct parent is eventually merged, they become direct children and get updated at that point.
2627

tests/test_update_pr_stack.sh

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/bash
22

3-
set -e
3+
set -eo pipefail
44

55
# Get script directory (needed for static mock files)
66
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -71,6 +71,8 @@ echo "Simulated Squash commit (via cherry-pick): $SQUASH_COMMIT"
7171

7272
echo "Running update-pr-stack.sh..."
7373
# The update script sources command_utils.sh itself
74+
# Capture stdout+stderr interleaved so command ordering can be asserted.
75+
RUN_LOG="$TEST_REPO/update_run.log"
7476
run_update_pr_stack() {
7577
log_cmd \
7678
env \
@@ -79,10 +81,24 @@ run_update_pr_stack() {
7981
TARGET_BRANCH=main \
8082
GH="$SCRIPT_DIR/mock_gh.sh" \
8183
GIT="$SCRIPT_DIR/mock_git.sh" \
82-
$SCRIPT_DIR/../update-pr-stack.sh
84+
$SCRIPT_DIR/../update-pr-stack.sh 2>&1 | tee "$RUN_LOG"
8385
}
8486
run_update_pr_stack
8587

88+
# The head must be pushed before the PR is retargeted (a failed push must leave
89+
# the PR untouched on its old base), and the merged branch deleted only after
90+
# the retarget (deleting a PR's base branch closes the PR).
91+
push_line=$(grep -n "git push origin feature2" "$RUN_LOG" | head -1 | cut -d: -f1 || true)
92+
edit_line=$(grep -n "pr edit feature2 --base main" "$RUN_LOG" | head -1 | cut -d: -f1 || true)
93+
delete_line=$(grep -n "git push origin :feature1" "$RUN_LOG" | head -1 | cut -d: -f1 || true)
94+
if [[ -n "$push_line" && -n "$edit_line" && -n "$delete_line" \
95+
&& "$push_line" -lt "$edit_line" && "$edit_line" -lt "$delete_line" ]]; then
96+
echo "✅ Ordering: push head, then retarget base, then delete merged branch"
97+
else
98+
echo "❌ Wrong ordering (push=$push_line edit=$edit_line delete=$delete_line)"
99+
exit 1
100+
fi
101+
86102
# Verify the results
87103
cd "$TEST_REPO"
88104

update-pr-stack.sh

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,20 +311,22 @@ main() {
311311
fi
312312
done
313313

314-
# Only update base branches for successfully updated PRs
314+
# Push the heads before retargeting: a failed push then leaves each PR
315+
# intact on its old base, and the head already contains TARGET_BRANCH when
316+
# the base flips to it.
317+
if [[ "${#UPDATED_TARGETS[@]}" -gt 0 ]]; then
318+
log_cmd git push origin "${UPDATED_TARGETS[@]}"
319+
fi
320+
315321
for BRANCH in "${UPDATED_TARGETS[@]}"; do
316322
log_cmd gh pr edit "$BRANCH" --base "$TARGET_BRANCH"
317323
done
318324

319-
# Push updated branches; only delete merged branch if no conflicts
325+
# Deleting a PR's base branch closes the PR, so this must come after the
326+
# retargets. Keep the branch for reference while conflicted PRs remain.
320327
if [[ "${#CONFLICTED_TARGETS[@]}" -eq 0 ]]; then
321-
# No conflicts - safe to delete merged branch
322-
log_cmd git push origin ":$MERGED_BRANCH" "${UPDATED_TARGETS[@]}"
328+
log_cmd git push origin ":$MERGED_BRANCH"
323329
else
324-
# Some conflicts - keep merged branch for reference during manual resolution
325-
if [[ "${#UPDATED_TARGETS[@]}" -gt 0 ]]; then
326-
log_cmd git push origin "${UPDATED_TARGETS[@]}"
327-
fi
328330
echo "⚠️ Keeping branch '$MERGED_BRANCH' - still referenced by conflicted PRs: ${CONFLICTED_TARGETS[*]}"
329331
fi
330332
}

0 commit comments

Comments
 (0)