Scenario
Section titled “Scenario”A six-person team ships a web service several times a week. Reviews are inconsistent: some pull requests get two careful reviews, some get a rubber stamp, and one last month was merged with failing tests because the check “looked flaky”. Nobody agrees on squash versus merge, so the history is a mix.
You have been asked to set the rules. Not one rule — the whole workflow.
Objective
Section titled “Objective”Make five decisions that only work when made together — template, required checks, review policy, merge method, and the branch rule that enforces them — write down why, apply them, and prove the workflow by opening a pull request that tries to break each rule.
Prerequisites
Section titled “Prerequisites”- Pull requests explained
- Branch protection or rulesets
- Lab 11 if you want CODEOWNERS in the mix
Starting state
Section titled “Starting state”A repository with a trivially checkable CI workflow, so “required check” means something:
mkdir -p /tmp/lab-prflow && cd /tmp/lab-prflowgit init -q -b main .git config user.email "lab@example.com"git config user.name "Lab User"mkdir -p .github/workflows
cat > .github/workflows/ci.yml <<'EOF'name: CIon: pull_request: merge_group:
permissions: contents: read
jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Fail if any file contains the word BREAK run: | if grep -rIl --exclude-dir=.git --exclude-dir=.github 'BREAK' .; then echo "::error::Remove BREAK before merging" exit 1 fiEOF
echo "# Service" > README.mdgit add . && git commit -q -m "Initial commit with CI"The check fails on any file containing BREAK. That gives you a one-word way to make a pull
request fail CI on demand.
-
Decide, in writing, before touching GitHub. For the team above, answer each:
Decision Options Your choice and one-line reason Merge method merge / squash / rebase Required approvals 0 / 1 / 2 Dismiss stale approvals on push? yes / no Required check check/ noneRequire branch up to date before merge? yes / no Who may bypass? nobody / admins -
Write the pull request template. It should make the reviewer’s job faster, not the author’s longer. Keep it under fifteen lines:
Terminal window cat > .github/pull_request_template.md <<'EOF'## What and why<!-- One paragraph. Link the issue. -->## How to verify<!-- Commands or steps a reviewer can run. -->## Risk- [ ] No schema, config or dependency change- [ ] Or: change is described above and rollback is `git revert`EOFgit add . && git commit -q -m "Add pull request template" -
Push and apply the rules. Create a repository, push, then apply your decisions as a ruleset on
main(Settings → Rules → Rulesets → New branch ruleset), or via the CLI:Terminal window git remote add origin <your-repo-url>git push -u origin mainIn the ruleset, enable: restrict deletions, block force pushes, require a pull request with your chosen approval count, require the
checkstatus check, and set the merge method you chose under Settings → General → Pull Requests (allow only that one). -
Try to break each rule with one pull request:
Terminal window git switch -c try-to-breakecho "BREAK" >> README.mdgit commit -qam "Introduce a failing check"git push -u origin try-to-breakOpen the pull request. Observe: the template appeared; the check fails; the merge button is disabled and says why. Now fix it:
Terminal window sed -i 's/BREAK//' README.mdgit commit -qam "Fix"git pushObserve: the check re-runs on the new commit. If you required approvals, the button stays disabled until someone approves. If you enabled “dismiss stale approvals”, get an approval, push once more, and watch it disappear.
-
Try a force push to
main:Terminal window git switch maingit commit --allow-empty -qm "Rewrite"git push --forceIt is rejected. Read the message — it names the rule.
Validation
Section titled “Validation”Every rule you chose was visibly enforced in step 4 or 5: the failing check blocked the merge, the approval count blocked it, stale approvals were dismissed if enabled, and the force push was refused. If any rule did not bite, Troubleshooting covers why.
Step 1. There is no universally right answer, but for this team: squash merges give a
readable main for a fast-shipping service whose branches are short; one approval with dismissal
on push is enough for six people and prevents the “approved last Tuesday” merge; the check must be
required or the flaky-test merge happens again; up-to-date-before-merge is worth it at six people
and a merge queue is worth it at sixty; nobody bypasses, with a documented break-glass procedure.
Step 3. Rulesets layer and can be enforced for admins; classic branch protection applies only its single most specific rule. Prefer rulesets for anything new.
Step 4. A required check that never reports leaves the pull request stuck at “Expected”. If your workflow is skipped by a path filter, that is what you will see.
Solution
Section titled “Solution”One defensible set of choices for the stated team:
| Decision | Choice | Reason |
|---|---|---|
| Merge method | Squash only | Short branches, several merges a week; one commit per change makes main bisectable and revertable |
| Required approvals | 1 | Six people; two would serialise on the two most senior |
| Dismiss stale approvals | Yes | Otherwise a push after approval merges unreviewed code |
| Required check | check | The failing-test merge is the problem being solved |
| Up to date before merge | Yes | Cheap at this scale; prevents “green in isolation, broken together” |
| Bypass | Nobody | Break-glass is a temporary ruleset change with a written reason, not a permanent exemption |
Your answers may differ, and that is fine as long as each has a reason that references the team.
Explanation
Section titled “Explanation”These five decisions are one decision. A required check without a rule that enforces it is a suggestion. Two required approvals with permanent admin bypass is theatre. Squash merging with long-lived branches produces unreviewable commits. Each choice constrains the others, which is why the lab asks for all of them at once.
The template is for the reviewer. Long templates get skipped; short ones get filled in. “How to verify” is the section that changes review quality, because it turns “looks fine” into “I ran it”.
Rules are tested by trying to break them. A rule you have never seen refuse anything is a rule you are trusting rather than verifying. Step 4 and 5 are the test.
Troubleshooting
Section titled “Troubleshooting”The merge button was enabled despite the failing check. The check is not marked required in the ruleset, or the ruleset is in “evaluate” mode rather than “active”.
The force push succeeded. Block force pushes was not enabled, or you are an admin and the ruleset does not include admins in its bypass list exclusion — check the ruleset’s enforcement and bypass settings.
The template did not appear. It must be at .github/pull_request_template.md (or the
repository root, or docs/) on the base branch.
Clean up
Section titled “Clean up”Delete the scratch repository on GitHub, then:
cd /tmp && rm -rf lab-prflowRelated lessons
Section titled “Related lessons”Next lab
Section titled “Next lab”Build a Python CI pipeline — lint, test and cache, with the check that step 4 relied on made real.