Chapter 10Lesson 02~180 minutes

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.

Hands-onRuleset stagingBlocked pushVerification

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.
Mandatory path: GitHub.com, GitHub Free, a personal public repository that you own, Git, and GitHub CLI. Repository ruleset creation requires repository admin access. No paid organization, secret, self-hosted runner, package, environment, or external service is required.
Disposable-resource warning: this lesson deliberately blocks pushes to 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

Shell portability: commands shown with trailing \, $(...), 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-gate check. 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

Controlled failure: the next push is expected to fail. The commit exists only in your disposable local clone. After preserving the rejection text, reset the local branch to the unchanged hosted 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 --default exposed the active requirements after activation.
  • The direct push failed and the remote main OID 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?

A direct push fails after authentication succeeds. What layer should you inspect first?

What should remain unchanged after the rejected direct push?

Why is Disabled a useful rollout state?

What command can predict rules for a branch name that does not yet exist?

Next lesson

Choose the right governance model

Lesson 03 compares classic protection with rulesets, repository policy with organization policy, strict versus loose checks, and normal versus emergency bypass design.

Authoritative references

 About rulesets

 Creating rulesets for a repository

 Available rules for rulesets

 About protected branches

 REST API endpoints for rules

 GitHub CLI: gh ruleset

 Troubleshooting required status checks

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.