Checkpoint Lab — Merge Methods, Auto-Merge, Merge Queue, Conflict Handling, and Branch Cleanup
Run a production-style integration checkpoint: compare two merge histories, install a required check, demonstrate blocked auto-merge and recovery, resolve a controlled conflict, inspect final graph evidence, and write a merge policy for a busy protected branch.
Learning objectives
- Create a disposable public repository and two PRs that deliberately demonstrate different merge policies, then prove final graph differences.
- Install a least-privilege required GitHub Actions check on the default branch and inspect the protection via versioned API evidence.
- Use auto-merge to demonstrate blocked integration, repair the exact failing condition, and verify the resulting merged SHA without bypass.
- Engineer a controlled competing-line conflict, resolve it locally, and prove the new PR head/check state before integration.
-
Simulate merge-queue ordering and
merge_groupevidence for a busy branch, with an optional live organization extension. - Write a merge-policy handoff covering history, gates, queue, conflicts, branch cleanup, rollback/debugging, and deployment separation.
\, command substitution such as $(...),
here-documents, printf, and rm -rf are Git
Bash/Bash/zsh syntax. PowerShell users can put the same Git/gh
arguments on one line, use the backtick for continuation, and use
Set-Content/Add-Content/Remove-Item -Recurse -Force
for file operations. GitHub merge and policy semantics are
shell-independent.
1. Checkpoint mission and evidence package
Build c09-merge-checkpoint. Capture: repository merge
settings; baseline/default-branch SHA; PR head/base OIDs;
merge-method choice; merged commit parents; ancestor tests;
required-check protection response; blocked and recovered auto-merge
state; conflict before/after head OID; remote branch cleanup
evidence; queue simulation; and a written merge policy. No admin
bypass, force push, production repository, secret, runner
registration, package deletion, transfer, or repository deletion is
required.
2. Preflight and collision gate
gh auth status --active --hostname github.com
OWNER=$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)
REPO="c09-merge-checkpoint"
gh repo view "$OWNER/$REPO" >/dev/null 2>&1 && {
echo "STOP: repository already exists" >&2
exit 2
}
3. Create repository, baseline, merge settings, and queue-aware check
gh repo create "$OWNER/$REPO" --public --add-readme --clone
cd "$REPO"
DEFAULT_BRANCH=$(git branch --show-current)
printf 'mode=baseline
' > app.conf
git add app.conf
git commit -m "chore: checkpoint baseline"
git push
BASE0=$(git rev-parse HEAD)
gh repo edit "$OWNER/$REPO" --enable-merge-commit --enable-squash-merge --enable-rebase-merge --enable-auto-merge --delete-branch-on-merge=false
mkdir -p .github/workflows
cat > .github/workflows/integration-gate.yml <<'EOF'
name: integration-gate
on:
pull_request:
types: [opened, synchronize, reopened, edited]
merge_group:
permissions:
contents: read
jobs:
integration-gate:
runs-on: ubuntu-latest
steps:
- name: Controlled block
if: github.event_name == 'pull_request' && contains(github.event.pull_request.title, '[block]')
run: exit 1
- name: Pass gate
if: ${{ !(github.event_name == 'pull_request' && contains(github.event.pull_request.title, '[block]')) }}
run: echo "integration gate passed"
EOF
git add .github/workflows/integration-gate.yml
git commit -m "ci: add checkpoint integration gate"
git push
contents: read, no checkout, no secrets, no context
dump, and does not interpolate the untrusted PR title into a shell
command. The merge_group trigger makes the required
check queue-ready.
4. Verify merge configuration before protection
gh repo view "$OWNER/$REPO" --json nameWithOwner,defaultBranchRef,mergeCommitAllowed,squashMergeAllowed,rebaseMergeAllowed,deleteBranchOnMerge
Prediction 1: enabling merge methods/auto-merge changes hosted
repository policy only; BASE0 and existing Git objects
do not move. Confirm git rev-parse HEAD is still the
expected setup commit after the workflow commit, and distinguish
that normal setup commit movement from policy toggles.
5. PR 1 — merge commit: preserve topic commits and add integration point
git switch -c checkpoint/merge "$DEFAULT_BRANCH"
printf 'merge-A
' >> app.conf
git add app.conf && git commit -m "feat: checkpoint merge A"
printf 'merge-B
' >> app.conf
git add app.conf && git commit -m "test: checkpoint merge B"
PR1_TIP=$(git rev-parse HEAD)
git push -u origin checkpoint/merge
PR1_URL=$(gh pr create --base "$DEFAULT_BRANCH" --head checkpoint/merge --title "checkpoint: preserve commit topology" --body "Merge-commit checkpoint PR.")
PR1=${PR1_URL##*/}
gh pr checks "$PR1" --watch
gh pr view "$PR1" --json baseRefOid,headRefOid,commits,mergeable,statusCheckRollup
gh pr merge "$PR1" --merge --delete-branch
git switch "$DEFAULT_BRANCH" && git pull --ff-only
PR1_MERGE=$(git rev-parse HEAD)
git show --no-patch --pretty='format:%H parents=%P subject=%s' "$PR1_MERGE"
git merge-base --is-ancestor "$PR1_TIP" HEAD && echo "PR1 topic tip preserved in base ancestry"
6. PR 2 — squash: one logical base commit
git switch -c checkpoint/squash "$DEFAULT_BRANCH"
printf 'squash-A
' >> app.conf
git add app.conf && git commit -m "feat: checkpoint squash A"
printf 'squash-B
' >> app.conf
git add app.conf && git commit -m "fix: checkpoint squash B"
PR2_TIP=$(git rev-parse HEAD)
git push -u origin checkpoint/squash
PR2_URL=$(gh pr create --base "$DEFAULT_BRANCH" --head checkpoint/squash --title "checkpoint: one logical change" --body "Squash checkpoint PR.")
PR2=${PR2_URL##*/}
gh pr checks "$PR2" --watch
gh pr merge "$PR2" --squash --delete-branch
git switch "$DEFAULT_BRANCH" && git pull --ff-only
PR2_SQUASH=$(git rev-parse HEAD)
git show --no-patch --pretty='format:%H parents=%P subject=%s' "$PR2_SQUASH"
if git merge-base --is-ancestor "$PR2_TIP" HEAD; then
echo "unexpected: original squash tip reachable"
else
echo "expected: original squash tip is not in base ancestry"
fi
Prediction 2: PR 1 adds a two-parent merge commit and keeps topic commits reachable; PR 2 adds one base commit and does not make the original topic tip an ancestor. The two predictions are verified with different evidence, not by looking only at PR badges.
7. Protect the default branch with the integration gate
enforce_admins: true intentionally prevents the
repository owner from casually bypassing the checkpoint gate.
printf 'protect=%s/%s:%s
' "$OWNER" "$REPO" "$DEFAULT_BRANCH"
gh api -X PUT -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2026-03-10" "repos/$OWNER/$REPO/branches/$DEFAULT_BRANCH/protection" --input - <<'JSON'
{
"required_status_checks": {"strict": true, "contexts": ["integration-gate"]},
"enforce_admins": true,
"required_pull_request_reviews": null,
"restrictions": null
}
JSON
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$OWNER/$REPO/branches/$DEFAULT_BRANCH/protection/required_status_checks" --jq '{strict,contexts}'
8. Demonstrate blocked auto-merge and safe recovery
git switch -c checkpoint/auto "$DEFAULT_BRANCH"
printf 'auto-gated
' >> app.conf
git add app.conf && git commit -m "test: checkpoint auto merge"
git push -u origin checkpoint/auto
AUTO_URL=$(gh pr create --base "$DEFAULT_BRANCH" --head checkpoint/auto --title "[block] checkpoint: required gate" --body "Auto-merge should wait for the deliberately failing required check.")
AUTO_PR=${AUTO_URL##*/}
gh pr checks "$AUTO_PR" || true
gh pr merge "$AUTO_PR" --auto --squash
gh pr view "$AUTO_PR" --json autoMergeRequest,mergeStateStatus,statusCheckRollup,headRefOid
Record the failing check and non-null auto-merge request. Then repair only the controlled cause:
gh pr edit "$AUTO_PR" --title "checkpoint: required gate repaired"
gh pr checks "$AUTO_PR" --watch
gh pr view "$AUTO_PR" --json state,mergedAt,mergeCommit,statusCheckRollup,autoMergeRequest
git switch "$DEFAULT_BRANCH" && git pull --ff-only
AUTO_MERGED=$(git rev-parse HEAD)
printf 'auto_merged_sha=%s
' "$AUTO_MERGED"
Do not claim deployment success. The evidence proves PR integration and required-check success only.
9. Controlled conflict under protection
Create the topic change from the current base, then advance the same line on the base through a separate passing PR. This avoids direct protected-branch pushes.
git switch "$DEFAULT_BRANCH" && git pull --ff-only
git switch -c checkpoint/conflict-topic
printf 'mode=topic
' > app.conf
git add app.conf && git commit -m "feat: topic sets mode"
git push -u origin checkpoint/conflict-topic
CONFLICT_URL=$(gh pr create --base "$DEFAULT_BRANCH" --head checkpoint/conflict-topic --title "checkpoint: resolve competing line" --body "Conflict target PR.")
CONFLICT_PR=${CONFLICT_URL##*/}
CONFLICT_HEAD1=$(git rev-parse HEAD)
git switch "$DEFAULT_BRANCH" && git pull --ff-only
git switch -c checkpoint/base-change
printf 'mode=base
' > app.conf
git add app.conf && git commit -m "fix: base sets competing mode"
git push -u origin checkpoint/base-change
BASE_URL=$(gh pr create --base "$DEFAULT_BRANCH" --head checkpoint/base-change --title "checkpoint: advance base" --body "Creates the competing base line.")
BASE_PR=${BASE_URL##*/}
gh pr checks "$BASE_PR" --watch
gh pr merge "$BASE_PR" --squash --delete-branch
# Return to the original topic and observe the conflict.
git switch checkpoint/conflict-topic
git fetch origin "$DEFAULT_BRANCH"
git merge "origin/$DEFAULT_BRANCH" || true
git status
Resolve locally to mode=resolved, commit, and push
normally:
printf 'mode=resolved
' > app.conf
git add app.conf
git commit -m "fix: resolve checkpoint merge conflict"
git push
CONFLICT_HEAD2=$(git rev-parse HEAD)
printf 'before=%s after=%s
' "$CONFLICT_HEAD1" "$CONFLICT_HEAD2"
gh pr view "$CONFLICT_PR" --json headRefOid,mergeable,mergeStateStatus,statusCheckRollup
gh pr checks "$CONFLICT_PR" --watch
gh pr merge "$CONFLICT_PR" --squash --delete-branch
The before/after OIDs prove conflict resolution changed the PR head. In a repository with review-freshness requirements, that change may invalidate earlier approval and must be handled according to Chapter 08 policy.
10. Queue simulation for a busy protected branch
Initial target: main@M100
Queued PRs: #31@H31, #32@H32
Group G31: main@M100 + #31@H31 -> integration-gate must pass on merge_group SHA G31
If G31 merges: main becomes M101
Group G32: main@M101 + #32@H32 -> integration-gate must pass again on new speculative SHA G32
If workflow only triggers pull_request: required integration-gate is missing for G31/G32 -> queue cannot safely advance.
Optional live extension: repeat with a disposable organization-owned public repository and a rule that requires the merge queue. Preserve the queue entry, merge-group SHA/ref, and required check evidence. The mandatory personal-repository checkpoint remains complete without this extension.
11. Verify branch cleanup and archive evidence
git switch "$DEFAULT_BRANCH" && git pull --ff-only
git ls-remote --heads origin checkpoint/merge checkpoint/squash checkpoint/auto checkpoint/conflict-topic
# Heads merged with --delete-branch should not be returned.
git log --graph --decorate --oneline --all --max-count=40
gh pr list --state merged --json number,title,mergedAt,mergeCommit,headRefName,baseRefName
If a branch unexpectedly remains, inspect whether another open PR depends on it or a rule prevented deletion. Do not force-delete until the dependency is understood.
12. Write the busy-branch merge policy
Target: main
History rule: squash ordinary PRs; merge commits only for explicitly documented integration branches
Allowed methods: configure repository to match the policy; do not leave unused methods enabled indefinitely
Required checks: integration-gate and named security/test checks; unique stable check names
Auto-merge: allowed when all encoded requirements pass
Merge queue: required for high-volume organization-owned main when available; CI listens to merge_group
Conflict policy: prefer local resolution; never hide conflict cause with admin bypass; new head requires freshness review
Branch cleanup: auto-delete short-lived heads after merge only after dependency inventory
Bypass: incident-only under separate, auditable governance
Evidence: record final merged SHA/checks/reviews; deployment state verified separately
13. Final verification checklist
- Two PRs demonstrably produced different histories: merge commit versus squash.
- Repository merge settings were inspected and all policy mutations were confined to the disposable repository.
- The integration-gate workflow has explicit least-privilege permissions and includes merge_group.
- Branch protection requires integration-gate and was verified through the versioned REST API.
- Auto-merge was observed blocked by the failing required check and later completed only after the condition was repaired.
- No admin bypass or force push was used.
- Conflict resolution changed the PR head through an ordinary commit and the new OID/check state was verified.
- Queue semantics are labeled simulation unless a qualifying organization repository was used.
- Merged head branches were checked through hosted ref inspection.
- Merged SHA is not treated as deployment proof.
14. Cleanup / rollback
Archive the disposable repository to preserve the checkpoint evidence while preventing accidental future mutation. If you enabled an optional organization merge queue, remove the disposable branch rule/queue requirement before archiving only if your organization cleanup policy requires it. Permanent deletion is intentionally not mandatory.
cd ..
printf 'archive-check=%s/%s
' "$OWNER" "$REPO"
gh repo archive "$OWNER/$REPO"
rm -rf "$REPO"
15. What Chapter 09 adds to the production GitHub operating model
The operating model now defines not only who reviews a change, but exactly how reviewed commits enter a protected branch, which checks are authoritative at the final integration state, how busy-branch races are controlled, how conflicts change review scope, and when temporary refs can be removed. Merge state is now evidence-driven and distinct from deployment state.
16. Bridge to Chapter 10: protected branches, rulesets, push rules, and bypass governance
Chapter 09 used a minimal required check to make integration behavior observable. Chapter 10 generalizes that into repository governance: protected branches, rulesets, push rules, required checks, bypass lists, layering/conflicts between rules, and how to design enforcement without locking out safe maintenance.
Knowledge check
What independent evidence distinguished the two checkpoint merge policies?
The merge-commit PR produced a two-parent merge commit with the topic tip reachable from base; the squash PR produced one new base commit with the original topic tip not an ancestor.
Why was the auto-merge repair valid?
It fixed the deliberately failing required check condition and let the same required check pass; it did not bypass or remove the control.
What must a queue-aware required Actions workflow include?
The merge_group trigger in addition to the relevant pull_request behavior.
Why does a conflict-resolution commit matter to review policy?
It changes the PR head OID and content/ancestry, so earlier review evidence may no longer cover the current head under freshness rules.
Why is branch cleanup verified with hosted ref inspection?
Local branch state and the PR UI are not sufficient proof that the remote head ref is gone.
What is the handoff to Chapter 10?
The merge mechanics are understood; the next chapter formalizes branch/ruleset enforcement, required checks, push restrictions, and bypass governance.
Authoritative references
About merge methods on GitHub
Automatically merging a pull request
Merging a pull request with a merge queue
Managing a merge queue
Events that trigger workflows: merge_group
Resolving a merge conflict using the command line
Deleting and restoring branches in a pull request
About protected branches
gh pr merge
gh repo edit
REST API endpoints for protected branches
REST API versions
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.