Skip to content

Lab: Design a Pull Request Workflow

Lesson 2 of 2Intermediate5 min readHands-On Git & GitHub Labs · GitHub Labs
Time30 minutes
LevelIntermediate
You needA GitHub repository you administer (a private scratch repo is ideal)

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.

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.

A repository with a trivially checkable CI workflow, so “required check” means something:

Terminal window
mkdir -p /tmp/lab-prflow && cd /tmp/lab-prflow
git 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: CI
on:
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
fi
EOF
echo "# Service" > README.md
git 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.

  1. Decide, in writing, before touching GitHub. For the team above, answer each:

    DecisionOptionsYour choice and one-line reason
    Merge methodmerge / squash / rebase
    Required approvals0 / 1 / 2
    Dismiss stale approvals on push?yes / no
    Required checkcheck / none
    Require branch up to date before merge?yes / no
    Who may bypass?nobody / admins
  2. 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`
    EOF
    git add . && git commit -q -m "Add pull request template"
  3. 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 main

    In the ruleset, enable: restrict deletions, block force pushes, require a pull request with your chosen approval count, require the check status check, and set the merge method you chose under Settings → General → Pull Requests (allow only that one).

  4. Try to break each rule with one pull request:

    Terminal window
    git switch -c try-to-break
    echo "BREAK" >> README.md
    git commit -qam "Introduce a failing check"
    git push -u origin try-to-break

    Open 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.md
    git commit -qam "Fix"
    git push

    Observe: 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.

  5. Try a force push to main:

    Terminal window
    git switch main
    git commit --allow-empty -qm "Rewrite"
    git push --force

    It is rejected. Read the message — it names the rule.

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.

One defensible set of choices for the stated team:

DecisionChoiceReason
Merge methodSquash onlyShort branches, several merges a week; one commit per change makes main bisectable and revertable
Required approvals1Six people; two would serialise on the two most senior
Dismiss stale approvalsYesOtherwise a push after approval merges unreviewed code
Required checkcheckThe failing-test merge is the problem being solved
Up to date before mergeYesCheap at this scale; prevents “green in isolation, broken together”
BypassNobodyBreak-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.

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.

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.

Delete the scratch repository on GitHub, then:

Terminal window
cd /tmp && rm -rf lab-prflow

Build a Python CI pipeline — lint, test and cache, with the check that step 4 relied on made real.

Choose a learning pathA sequenced route through the curriculum for wherever you are now.