Protected Branches, Repository Rulesets, Push Rules, Required Checks, and Bypass Governance: Guided Hands-On Workflow and Core Operations
Create a public disposable repository, produce a real status check, stage and activate a minimal ruleset, prove that a direct update is rejected, then complete the same change through the compliant PR path.
Learning objectives
- Inspect rules and repository state before changing policy.
- Create a safe GitHub Actions status check without secrets or elevated workflow permissions.
- Stage a branch ruleset Disabled, verify its target and requirements, then activate it.
- Interpret a rejected direct push as evidence of a specific rule rather than as a generic Git failure.
- Open a compliant PR, observe the required check, merge only after evidence passes, and verify the resulting ref.
- Remove or disable lab policy during cleanup and preserve a short evidence record.
main and later removes the policy. Use
only github-policy-lab or another throwaway repository.
Never test this sequence on an academy repository, employer
repository, production release branch, or valuable personal project.
1. Preflight: prove identity, repository, and absence of hidden policy
\, $(...), printf,
cat <<'EOF', or rm are Git
Bash/Bash/zsh syntax. In PowerShell, keep the same Git/gh
arguments but use the backtick for line continuation and
Set-Content/Add-Content/Remove-Item
for file operations. GitHub ruleset and branch-policy semantics do
not depend on the shell.
git --version
gh --version
gh auth status --active --hostname github.com
gh repo create github-policy-lab --public --add-readme --clone
cd github-policy-lab
gh repo view --json nameWithOwner,visibility,defaultBranchRef,viewerPermission
git remote -v
git status --short --branch
gh ruleset list --parents
gh ruleset check --default
Expected baseline: you are the repository administrator, the
repository is public, the default branch is normally
main, the worktree is clean, and no lab ruleset
applies. If parent policy appears because you created the repository
under an organization, stop and either understand that policy or
repeat with a personal disposable repository.
2. Create a real, harmless status check before requiring it
A policy should not require a check name that has never existed.
First create a tiny Actions workflow on the unprotected repository.
It reads no secrets and requests only contents: read.
# .github/workflows/policy-gate.yml
name: policy-gate
on:
pull_request:
permissions:
contents: read
jobs:
policy-gate:
runs-on: ubuntu-latest
steps:
- name: Validate policy fixture
run: echo "policy fixture accepted"
mkdir -p .github/workflows
# Create the file above with your editor.
git add .github/workflows/policy-gate.yml
git commit -m "ci: add policy gate"
git push origin main
git switch -c warmup-check
printf 'warmup
' > warmup.txt
git add warmup.txt
git commit -m "test: produce first policy check"
git push -u origin warmup-check
gh pr create --base main --head warmup-check --title "Warm up policy check" --body "Disposable pre-policy check run."
gh pr checks --watch
Record the exact check name shown by GitHub. It should correspond to
the job name policy-gate, but policy must use observed
state rather than an assumption. Merge this warm-up PR before
protection is enabled, then delete the branch.
gh pr merge --squash --delete-branch
git switch main
git pull --ff-only
3. Create the ruleset Disabled first
In the current GitHub repository settings, open the repository’s
Rules/Rulesets surface and create a
branch ruleset named
chapter10-main-policy. Use the default branch as the
target. Set enforcement to Disabled. Do not add
bypass actors yet.
Add two rules:
- Require a pull request before merging. For this one-account lab, keep required approvals at zero; review policy was covered in Chapter 08.
-
Require status checks to pass before merging. Add
the exact observed
policy-gatecheck. Leave “require branches to be up to date” off for this first lab so the lesson isolates policy enforcement from strict-update behavior.
Create/save the ruleset. Disabled means the policy object exists but does not enforce. Verify it through machine-readable interfaces:
gh ruleset list -R OWNER/github-policy-lab --parents
gh api -H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
repos/OWNER/github-policy-lab/rulesets --paginate \
--jq '.[] | {id,name,enforcement,target,source_type}'
# Active-rule evaluation should not treat the disabled ruleset as enforcement.
gh ruleset check --default -R OWNER/github-policy-lab
The REST list should expose chapter10-main-policy with
enforcement set to disabled. The active-rule check
should not claim those disabled requirements are currently blocking
main.
4. Activate policy, then inspect before attempting a forbidden change
Edit the lab ruleset and change enforcement to Active. Do not alter its target or rules at the same time. Separating “activate” from “redesign” makes the causal change obvious.
gh ruleset check --default -R OWNER/github-policy-lab
gh api -H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2026-03-10" \
repos/OWNER/github-policy-lab/rules/branches/main \
--jq '.[] | {type,source_type,source}'
You should now see active rules requiring the PR path and the configured status check. If you do not, inspect the ruleset target before testing anything else.
5. Intentionally break the rule: attempt one direct update to main
origin/main.
git switch main
git pull --ff-only
printf 'direct push should be blocked
' > blocked-direct-push.txt
git add blocked-direct-push.txt
git commit -m "test: attempt forbidden direct update"
# Expected to fail due to the active repository rule.
git push origin main
GitHub ruleset rejections commonly identify repository rule
violations (for example with a GH013-style message),
but preserve the actual text you receive because wording can change.
Distinguish this from authentication failure: the server recognized
the repository and then rejected the ref update because policy
requires a PR/check path.
# Preserve the rejected local commit ID before cleanup if you want evidence.
git rev-parse HEAD
# SECURITY-SENSITIVE LOCAL CLEANUP: intentionally discard only the rejected disposable commit.
git fetch origin
git reset --hard origin/main
git status --short --branch
6. Perform the same logical change through the compliant path
git switch -c compliant-change
printf 'same change, governed path
' > governed-change.txt
git add governed-change.txt
git commit -m "docs: add governed change"
git push -u origin compliant-change
gh pr create --base main --head compliant-change \
--title "Governed change" \
--body "Chapter 10 disposable policy test."
gh pr view --json number,baseRefName,headRefName,headRefOid,mergeStateStatus,statusCheckRollup
gh pr checks --watch
Prediction before merge: main must not move until the
PR path exists and policy-gate succeeds. Record the
base OID first, then merge and verify that the hosted default branch
changes only after the required evidence passes.
BEFORE=$(git rev-parse origin/main)
gh pr merge --squash --delete-branch
git switch main
git pull --ff-only
AFTER=$(git rev-parse origin/main)
printf 'before=%s
after=%s
' "$BEFORE" "$AFTER"
git log --oneline --decorate -5
7. Safe staging: Disabled → Active; optional Evaluate where your deployment offers it
The free mandatory path already tested policy lifecycle safely: configure while Disabled, inspect, activate, then test a denied action. If the current ruleset UI for your plan/deployment exposes Evaluate, you can insert it before Active and use Rule Insights to observe would-pass/would-fail events without enforcement. GitHub Enterprise Cloud explicitly documents general Evaluate mode. Do not write automation that assumes Evaluate exists everywhere.
8. Challenge: choose the right surface, not the first command you remember
You are told that a future branch named
release/2026.08 must require PRs even though it does
not exist yet. Which inspection tool best predicts whether current
rulesets would target it?
gh ruleset check release/2026.08 -R OWNER/github-policy-lab
The branch does not need to exist for this check. Explain why this
is stronger evidence than looking only at
git branch -r: Git can tell you which refs exist, not
which hosted policy would apply to a hypothetical future ref.
9. Cleanup and rollback
Before cleanup, capture the ruleset ID/name and the successful PR
number. Then delete the chapter10-main-policy ruleset
in repository Settings, or set it Disabled if you want to retain the
lab object temporarily for inspection. Verify that no rule remains
active:
gh ruleset list -R OWNER/github-policy-lab --parents
gh ruleset check --default -R OWNER/github-policy-lab
git status --short --branch
git branch -vv
Finally archive the disposable repository if you want a reversible cleanup:
gh repo archive OWNER/github-policy-lab
Archiving is repository lifecycle state, not a substitute for removing an accidentally dangerous rule from a repository you intend to keep using.
10. Verification checklist
- The ruleset existed Disabled before enforcement.
-
gh ruleset check --defaultexposed the active requirements after activation. -
The direct push failed and the remote
mainOID did not change. - The compliant PR produced the exact required check.
- The PR merged only after the requirement passed.
- The lab ruleset was removed/disabled and verified during cleanup.
Knowledge check
Why create the CI check before requiring its name?
It lets you observe the real check identity/source first and avoids locking the repository behind a misspelled or nonexistent requirement.
A direct push fails after authentication succeeds. What layer should you inspect first?
Inspect active branch protection/rulesets and the exact rejection evidence; do not troubleshoot credentials first when the server is explicitly reporting policy violation.
What should remain unchanged after the rejected direct push?
The hosted refs/heads/main OID. Only the disposable
local branch gained the rejected commit until you reset it.
Why is Disabled a useful rollout state?
It lets you configure and inspect the policy object before enforcement, then activate one known configuration change.
What command can predict rules for a branch name that does not yet exist?
gh ruleset check BRANCH --repo OWNER/REPO.
Authoritative references
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.