Checkpoint Lab — Approvals, CODEOWNERS, Protected Branches and Tags, Push Rules, and Merge Governance
Design, test, verify, and remove a disposable merge-governance policy across branches and tags, with Free enforcement plus realistic Premium/Ultimate approval and push-rule fixtures.
Learning objectives
- Design a small merge-governance policy before touching GitLab and predict actor/ref outcomes.
- Implement Free-compatible branch and tag enforcement on disposable refs and capture at least one real denial.
- Prove policy behavior through independent Git and GitLab/API observations.
- Evaluate Premium/Ultimate approval, CODEOWNERS, and push-rule fixtures without claiming live Free enforcement.
- Restore the project to its pre-lab governance state and bridge to merge methods and integration strategy.
1. Checkpoint mission and preflight
Your task is to govern a tiny synthetic release flow without modifying the project’s real default-branch policy. You will create a disposable target branch, protect it against direct push, protect a synthetic release-tag namespace against creation, route one change through an MR, record the rejection and successful path, and then remove every lab rule/ref.
Required: GitLab Free-compatible project,
Maintainer/Owner role in that disposable project, Git, and
glab authentication from Chapter 02.
Not required: Premium/Ultimate, second human
account, CI compute, custom runner, secret, production registry,
cloud, Kubernetes, or administrator access.
2. Write the policy before implementing it
Before clicking a setting, write this policy record and fill in the actor you will use:
checkpoint_policy:
branch: governance-checkpoint
allowed_merge: Maintainer-or-Owner-lab-identity
allowed_direct_push: none
force_push: disabled
tag_pattern: checkpoint-v*
allowed_tag_creation: none
required_approvals_live: false # Free path
codeowners_live: false # paid feature fixture only
push_rules_live: false # paid feature fixture only
exception: remove only lab rule after evidence capture
Now predict outcomes:
| Action | Prediction before test | Control responsible |
|---|---|---|
Direct push to governance-checkpoint after
protection
|
Denied | Protected branch: push = No one |
| Push normal source branch | Allowed | Source branch is not the protected target |
| Merge MR into protected target as authorized lab Maintainer/Owner | Allowed if other merge checks permit | Allowed to merge |
Create checkpoint-v0.0.1 while wildcard allows
No one
|
Denied | Protected tag rule |
| Merge without required approval on Free | Not blocked by approval count alone | Free approvals are optional |
3. Capture before-state and create disposable refs
REPO="GROUP/governance-sandbox"
PROJECT_ID="123456"
BASE="main" # verify from project API
TARGET="governance-checkpoint"
SOURCE="governance-checkpoint-change"
TAG="checkpoint-v0.0.1"
mkdir -p ch08-checkpoint-evidence
glab api "projects/$PROJECT_ID" --jq '{path_with_namespace,default_branch,visibility}' | tee ch08-checkpoint-evidence/project.json
glab api "projects/$PROJECT_ID/protected_branches" --paginate | tee ch08-checkpoint-evidence/branches-before.json
glab api "projects/$PROJECT_ID/protected_tags" --paginate | tee ch08-checkpoint-evidence/tags-before.json
git fetch origin --prune --tags
git switch "$BASE"
git pull --ff-only origin "$BASE"
git switch -c "$TARGET"
printf 'checkpoint base
' > ch08-checkpoint.txt
git add ch08-checkpoint.txt
git commit -m "ch08: checkpoint governance target"
git push -u origin "$TARGET"
BASELINE_TARGET_SHA="$(git rev-parse HEAD)"
4. Implement the two Free enforcement rules
Use the current UI on the disposable project:
-
Branch rule: Settings → Repository → Branch rules
→ rule for
governance-checkpoint. Allow merge to the lab role; set Allowed to push and merge = No one; leave force push disabled. -
Protected tag: Settings → Repository → Protected
tags → wildcard
checkpoint-v*; set Allowed to create = No one.
Then export both policies:
glab api "projects/$PROJECT_ID/protected_branches/$TARGET" | tee ch08-checkpoint-evidence/branch-rule.json
glab api "projects/$PROJECT_ID/protected_tags" --paginate | tee ch08-checkpoint-evidence/tag-rules.json
5. Test one denied path and one allowed path
Create the source revision, then deliberately target the protected branch directly. Preserve the failure before using the allowed source-branch path.
git switch -c "$SOURCE" "origin/$TARGET"
printf 'checkpoint governed change
' >> ch08-checkpoint.txt
git add ch08-checkpoint.txt
git commit -m "ch08: checkpoint proposed change"
PROPOSED_SHA="$(git rev-parse HEAD)"
# Prediction 1: DENIED.
git push origin "HEAD:$TARGET" 2>&1 | tee ch08-checkpoint-evidence/direct-push-denied.txt
# Independent proof: remote target must still equal BASELINE_TARGET_SHA.
git ls-remote origin "refs/heads/$TARGET" | tee ch08-checkpoint-evidence/target-after-denial.txt
# Prediction 2: ALLOWED.
git push -u origin "$SOURCE"
6. Route the same source revision through the MR path
Create an MR whose target is the disposable protected branch. Inspect source and target before merging.
glab mr create -R "$REPO" --source-branch "$SOURCE" --target-branch "$TARGET" --title "ch08 checkpoint: governed merge" --description "Disposable governance checkpoint. No production resources." --yes
MR_IID="REPLACE_WITH_IID"
glab api "projects/$PROJECT_ID/merge_requests/$MR_IID" --jq '{iid,state,source_branch,target_branch,sha,diff_refs,detailed_merge_status}' | tee ch08-checkpoint-evidence/mr-before.json
Before selecting Merge, answer: Is the source SHA equal to
PROPOSED_SHA? Is the target exactly
governance-checkpoint? Is any approval requirement
shown, and if not, are you correctly treating Free approvals as
optional rather than “passed governance”?
Merge the synthetic MR only after those checks. Then:
git fetch origin "$TARGET"
AFTER_MERGE_SHA="$(git rev-parse "origin/$TARGET")"
printf 'baseline=%s
after_merge=%s
proposed=%s
' "$BASELINE_TARGET_SHA" "$AFTER_MERGE_SHA" "$PROPOSED_SHA" | tee ch08-checkpoint-evidence/ref-identities.txt
glab api "projects/$PROJECT_ID/merge_requests/$MR_IID" --jq '{iid,state,merged_at,merge_commit_sha,squash_commit_sha,sha}' | tee ch08-checkpoint-evidence/mr-after.json
Do not assume the post-merge target SHA must equal the source SHA; the exact result depends on the project’s merge method, which Chapter 09 studies next. The checkpoint only requires that the protected target changed through the permitted MR path, not through the denied direct push.
7. Test protected-tag behavior
git tag "$TAG" "origin/$TARGET"
# Prediction 3: DENIED by protected-tag wildcard.
git push origin "refs/tags/$TAG" 2>&1 | tee ch08-checkpoint-evidence/tag-create-denied.txt
git ls-remote --tags origin "refs/tags/$TAG" | tee ch08-checkpoint-evidence/tag-remote-proof.txt
The empty remote result plus non-zero push is independent evidence that the protected tag was not created. Do not create a real release merely to test tag governance.
8. Simulate the paid governance layer
Evaluate this Premium/Ultimate policy without enabling it:
premium_policy_fixture:
codeowners:
'/infrastructure/**': '@platform-lab/platform'
required_approval_rules:
- name: platform-owner
approvals_required: 1
target: protected branches
approval_freshness:
remove_code_owner_approvals_when_owned_files_change: true
push_rule:
commit_message_regex: '^PLAT-[0-9]+: '
For each line, write the enforcement point and expected failure evidence. CODEOWNERS determines path ownership; required approval rules block merge; approval-freshness policy changes approval state after source mutations; the push rule rejects at pre-receive. This exercise is complete without a paid subscription.
9. Failure drill: a rule appears stricter but is ineffective
Add this paper-only conflicting rule to your policy
worksheet: wildcard governance-* with Developers +
Maintainers allowed to push. Predict the effective behavior of
governance-checkpoint if both rules existed.
The safe answer is that the wildcard could make push/merge behavior more permissive because multiple matching protection rules compose according to GitLab’s documented rule behavior. The production response is to eliminate the permissive overlap, not rely on the exact-name rule to override it.
10. Verification, evidence manifest, and cleanup
Before cleanup, verify these invariants:
- The rejected direct push did not move the protected target.
- The permitted MR path did move the target.
- The protected tag was never created remotely.
- No required approval/CODEOWNERS/push-rule enforcement was falsely claimed on Free.
- Only chapter-specific disposable refs and rules were created.
python - <<'PY2'
from pathlib import Path
import hashlib, json, datetime
root=Path('ch08-checkpoint-evidence')
files=[]
for p in sorted(root.glob('*')):
if p.is_file():
files.append({'file':p.name,'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()})
out={'captured_at_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'files':files}
Path('ch08-checkpoint-manifest.json').write_text(json.dumps(out,indent=2)+'
')
PY2
cat ch08-checkpoint-manifest.json
checkpoint-v* protected-tag rule and
the governance-checkpoint Branch rule in the UI. Verify
those specific rules are gone before deleting their refs.
glab api "projects/$PROJECT_ID/protected_branches" --paginate > ch08-checkpoint-evidence/branches-after-rule-removal.json
glab api "projects/$PROJECT_ID/protected_tags" --paginate > ch08-checkpoint-evidence/tags-after-rule-removal.json
# Delete only the verified lab branches/tag, without hiding failures.
if git ls-remote --exit-code --heads origin "$SOURCE" >/dev/null 2>&1; then
git push origin --delete "$SOURCE"
else
printf 'remote source already absent: %s
' "$SOURCE"
fi
if git ls-remote --exit-code --heads origin "$TARGET" >/dev/null 2>&1; then
git push origin --delete "$TARGET"
else
printf 'remote target already absent: %s
' "$TARGET"
fi
if git show-ref --verify --quiet "refs/tags/$TAG"; then
git tag -d "$TAG"
else
printf 'local tag already absent: %s
' "$TAG"
fi
git switch "$BASE"
for branch in "$SOURCE" "$TARGET"; do
if git show-ref --verify --quiet "refs/heads/$branch"; then
git branch -D "$branch"
else
printf 'local branch already absent: %s
' "$branch"
fi
done
11. What Chapter 08 adds to a production GitLab operating model
You now have a governance model that distinguishes collaboration from enforcement. Normal changes can be forced through merge requests by branch protection; release tag names can be reserved; paid approval/CODEOWNERS/push validation can be layered when available; exception authority is treated as privileged configuration; and every control is validated through an allowed/denied behavior pair.
Chapter 09 picks up exactly where this leaves off. Once a change is authorized to merge, the team still must decide how GitLab integrates it—merge commit, semi-linear history, fast-forward, merge trains, conflict handling, and branch cleanup.
Knowledge check
The direct push was denied and the MR merge succeeded. What did the checkpoint prove?
That branch protection forced the tested change through the intended MR integration path for the disposable target; it did not prove paid approval or Code Owner enforcement.
Why is the post-merge target SHA not assumed to equal the source SHA?
The project merge method can create a merge commit, squash commit, or fast-forward. Chapter 09 studies those integration identities.
The local protected tag exists but the remote lookup is empty. What does that mean?
Local Git tag creation succeeded, but GitLab rejected remote tag creation under the protected-tag policy; local and hosted state are different.
What is the correct response if a broad wildcard protection makes the branch more permissive?
Tighten or remove the permissive overlapping rule and retest. Do not assume a more specific rule overrides it.
What three paid controls were intentionally simulated rather than required?
Required approval rules, Code Owners/CODEOWNERS enforcement, and push rules.
What is the next governance question after authorization is correct?
How authorized changes are integrated and ordered: merge methods, merge trains, mergeability, conflicts, and cleanup in Chapter 09.
Checkpoint summary
You designed policy before implementation, predicted actor/ref outcomes, proved a real direct-push denial and protected-tag denial, used the allowed MR path, kept Premium/Ultimate controls as explicit fixtures, hashed the evidence packet, and restored the disposable rules/refs. The chapter closes with governance separated cleanly from the merge mechanics that follow.
Official references
- GitLab Docs — Merge request approvals
- GitLab Docs — Merge request approval rules
- GitLab Docs — Merge request approval settings
- GitLab Docs — Code Owners
- GitLab Docs — Branch rules
- GitLab Docs — Protected branches
- GitLab Docs — Protection rules and permissions
- GitLab Docs — Protected tags
- GitLab Docs — Push rules
- GitLab Docs — Protect your repository
- GitLab Docs — Merge requests
- GitLab Docs — Protected branches API
- GitLab Docs — Protected tags API
- GitLab Docs — Project push rules API
- GitLab Docs — REST API pagination
- GitLab Docs — glab api
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.