You will open a pull request from the command line, run reviews, and merge only after checks pass. By the end you will have a review workflow and branch protection that keeps unreviewed code out of main.

A pull request is more than a merge button: it is the checkpoint where code gets read by a second pair of eyes, where CI proves the change builds and passes tests, and where the reasoning behind the change is recorded for future you. Doing the whole flow with the gh CLI means you never leave the terminal, and every stage, opening, reviewing, checking status, merging, is scriptable and repeatable. That repeatability is what turns "we should review code" into a process a team can actually rely on.

Learning Objectives

  • Create and manage PRs with the gh CLI.
  • Request reviews and respond to feedback.
  • Require status checks and reviews before merge.
  • Time: ~2 hours · Difficulty: Intermediate · Prereqs: a GitHub repo and the gh CLI.

Architecture Overview

The diagram shows the merge gate. A feature branch becomes a pull request with gh pr create. Two independent signals then feed the merge decision: the required status checks (CI) must be green, and at least one reviewer must approve. Only when both are true does branch protection allow the merge, through the web button or gh pr merge, to land the change on main. If either signal is missing, the merge is refused no matter how convincing the branch looks locally.

This matters because it removes "it worked on my machine" from the equation. The checks run on a clean runner with a fresh checkout, and the approval comes from a human who is not the author. Branch protection enforces both at the repository level, so the rule holds even if someone tries to push directly to main or merge with the web UI at 2 a.m.

The lab walks the loop in five moves: prepare a branch (step 00), open the PR (step 01), review and wait on checks (step 02), merge (step 03), then lock the gate with branch protection (step 04).

How It Works

How It Works

A pull request is a proposed merge wrapped in discussion: the diff, the checks, the comments, and the decision live together. That packaging is what turns 'please merge my branch' into a reviewable, auditable change record.

Status checks are CI results projected onto the PR. Because they run on the exact merge result, green checks mean the merged code was tested, which is why branch protection can safely require them before the merge button enables.

Merge strategies shape history: a merge commit preserves the PR as a unit, squash condenses it to one commit, and rebase keeps it linear. Teams pick one for consistency; the PR discussion stays readable regardless of the method.

Environment Setup

Prerequisites

  • A GitHub repository you can push to and open PRs in
  • The gh CLI authenticated (gh auth login)
  • Permission to manage branch protection rules (repo admin)
  • Assumed skill: feature branches and pushing (see Git Fundamentals)

You will need: the gh CLI authenticated (gh auth login) and a repo.

Before you begin: enable branch protection on main in repo settings.

Install gh if you do not have it, then confirm you are authenticated against the right account and host. The status output also prints the token scopes, you need the repo scope for private repositories and workflow if your PRs touch GitHub Actions files.

INSTALL AND AUTHENTICATE gh

# macOS
brew install gh

# authenticate against github.com (browser flow is easiest)
gh auth login --hostname github.com --git-protocol https --web

# verify
gh auth status
$ gh auth status
github.com ✓ Logged in to github.com account TYFSADIK (keyring) - Active account: true - Git operations protocol: https - Token: gho_************************************ - Token scopes: 'gist', 'read:org', 'repo', 'workflow'

Common mistake: authenticating with one GitHub account while the repo belongs to another. gh auth status shows the active account for each host; switch with gh auth switch if you have more than one.

Work from a local clone so gh can infer the repository from your git remote. Every gh pr command below assumes you run it inside the clone; otherwise you must pass --repo owner/name to each call.

CLONE THE REPO

gh repo clone TYFSADIK/demo-api
cd demo-api
git remote -v   # origin must point at the GitHub repo

How to verify: gh repo view --json name,owner should print the repo you expect. If it errors with "not a git repository", you are in the wrong directory.

Step-by-Step Execution

00
Create and push a feature branch

A PR is a conversation about a branch, so before you can open one you need a branch with commits on it, pushed to GitHub. Keep the branch small and single-purpose, one endpoint, one fix, so the review stays quick and the diff stays readable. Naming conventions like feature/ and fix/ make the intent obvious in the PR list before anyone clicks.

BRANCH, COMMIT, PUSH

git checkout -b feature/api
# ...make your changes...
git add -A
git commit -m "Add /health endpoint"
git push -u origin feature/api

The -u flag sets the upstream so later git push calls need no arguments, and so gh knows which remote branch the PR should track when it suggests defaults.

How to verify: git status should report Your branch is up to date with 'origin/feature/api' with a clean working tree. Common mistake: if the push is rejected as non-fast-forward, someone else pushed to the same branch name, pick a unique one instead of force-pushing over a teammate's work.

What this teaches: The branch is the unit of review; pushing it publishes the proposal for others to examine.

01
Open a pull request

Opening the PR is where you sell the change. The title shows up in the PR list and becomes the squash-merge commit message, so make it a sentence a stranger could act on. The body should answer three questions: what changed, why, and how you tested it. Reviewers read the body before the diff; a blank body gets a shallow review.

gh pr create --base main --head feature/api --title "Add API" --body "Implements endpoint"

For anything non-trivial, write the body in a file instead of cramming it into one flag. Markdown renders in the PR, and a Closes #17 line auto-links the issue and closes it on merge:

PR BODY WITH CONTEXT (.github/pull_request.md)

gh pr create --base main --head feature/api \
  --title "Add /health endpoint for uptime checks" \
  --body-file .github/pull_request.md

# .github/pull_request.md
## What
Adds GET /health returning { "status": "ok" }.
## Why
The load balancer needs a lightweight liveness probe. Closes #17.
## How tested
curl localhost:8080/health locally + a new pytest case.
$ gh pr create --base main --title "Add /health endpoint" --body-file .github/pull_request.md
https://github.com/TYFSADIK/demo-api/pull/42

gh prints the PR URL, that is your confirmation it worked. Open it in the browser with gh pr view --web, or read it without leaving the terminal:

CONFIRM THE PR EXISTS

gh pr status            # PRs for your branches: review state, checks
gh pr view 42           # title, body, reviewers, check rollup

Common mistake: running gh pr create before pushing the branch, gh errors with Head sha can't be blank because the remote has no such head. Push first (step 00), then create. How to verify: gh pr status lists the PR under "Current branch" with a checks summary.

What this teaches: The PR description is the pitch: what changed, why it matters, and how to verify it.

02
Review and check status

Two gates must go green: automated checks and human review. Check the CI rollup first, there is no point asking a person to read code that does not compile. gh pr checks lists every required and optional check with pass/fail; add --watch to follow a run in real time until it finishes.

$ gh pr checks && gh pr review --approve
All checks were successful Approved pull request #42

In a real team flow the author requests reviewers and a teammate does the review. You can request reviewers at creation or after the fact, and read the exact diff under review before passing judgment:

REQUEST REVIEW AND READ THE DIFF

gh pr edit 42 --add-reviewer alice,team-leads
gh pr diff 42               # full diff, paginated
gh pr view 42 --comments    # discussion so far

Reviews come in three flavours, and picking the right one is half of review culture:

THE THREE REVIEW MODES

gh pr review 42 --approve --body "LGTM - probe returns 200 in 3ms"
gh pr review 42 --request-changes --body "Handle the DB-down case before merge"
gh pr review 42 --comment --body "Question: why not reuse readiness.py?"

--request-changes is a blocking review: with branch protection on, the PR cannot merge until that reviewer dismisses the block or re-approves. --comment records feedback without affecting the merge gate, use it for questions, not requirements.

When feedback arrives, fix it on the same branch and push. The PR updates itself, CI re-runs, and the conversation threads stay attached to the lines they were about. Never open a second PR for review fixes, that throws away the discussion history.

RESPOND TO FEEDBACK

git checkout feature/api
# ...address the review comments...
git commit -am "Handle DB-down case in /health"
git push                    # PR #42 picks up the new commit automatically
gh pr checks 42 --watch     # confirm CI re-runs and goes green

How to verify: gh pr view 42 should show Reviewers: alice (Approved) and a green check rollup. Note: GitHub will not let you approve your own PR as a satisfying review, on a solo repo, test the flow with a second account or a teammate.

What this teaches: Status checks report CI results directly on the PR; red checks mean 'not yet', not 'no'.

03
Merge after checks pass

Only merge when both gates are green. The squash strategy compresses the whole branch into one commit on main, which keeps history linear and makes git revert a single-commit operation if the change misbehaves in production. --delete-branch removes the feature branch on both the remote and your local clone.

gh pr merge --squash --delete-branch
$ gh pr merge 42 --squash --delete-branch
✓ Squashed and merged pull request #42 ✓ Deleted branch feature/api locally and remotely

Pick the strategy deliberately, it shapes your history forever:

  • --squash: one commit per PR. Best default for feature work with messy "wip" commits.
  • --merge: keeps every commit plus a merge commit. Good for release branches where individual commits matter.
  • --rebase: replays the commits onto main with no merge commit. Linear history, but it rewrites commit SHAs.

After merging, sync your local main so the next branch starts from the merged state:

SYNC LOCAL main

git checkout main
git pull --prune            # fetch the squash commit, prune deleted remote branches

Common mistake: merging with --admin to bypass protection when checks are red. It works, and it defeats the entire point of the gate, if a check is flaky, fix the check, do not route around it. How to verify: git log main --oneline -1 shows one new commit whose message matches the PR title.

What this teaches: Merging after green checks keeps main releasable; the merge records the review decision in history.

04
Enforce the gate with branch protection

Everything above is convention until branch protection makes it a rule. Protection on main requires PRs, at least one approval, and passing checks, and applies the same rules to admins, which is the whole point. Set it once per repository, in Settings > Branches or with the API:

ENABLE BRANCH PROTECTION ON main

gh api repos/TYFSADIK/demo-api/branches/main/protection -X PUT \
  --input - <<'EOF'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["test"]
  },
  "enforce_admins": true,
  "required_pull_request_reviews": {
    "required_approving_review_count": 1,
    "dismiss_stale_reviews": true
  },
  "restrictions": null
}
EOF

strict: true means the branch must be up to date with main before merging, so the checks ran against what will actually land. dismiss_stale_reviews invalidates an approval when new commits arrive, closing the "approve, then sneak in one more commit" hole. The check context string (test) must match the exact job name the workflow reports, the workflow / job form only applies to reusable workflows, or the gate waits forever on a check that never reports.

VERIFY THE RULE

gh api repos/TYFSADIK/demo-api/branches/main/protection \
  --jq '.required_pull_request_reviews.required_approving_review_count,
        .required_status_checks.strict,
        .enforce_admins.enabled'

How to verify: open a toy PR with a deliberately failing check and confirm gh pr merge is refused with a message about required status checks. Also try git push origin main directly, the remote should reject it. If either succeeds, the protection rule did not take effect.

What this teaches: Branch protection turns the convention into a rule: nobody can merge past red checks, including you.

Progress So Far

The three core moves, open, review, merge, are the loop you will run dozens of times a week. Steps 00 and 04 bookend it: a well-prepared branch makes the PR easy to review, and branch protection makes the loop mandatory rather than optional.

Anatomy of a Good PR

Reviewers decide how carefully to read based on signals you control. A PR that gets a fast, useful review usually has all of these:

  • Small diff: under ~400 lines changed. Bigger than that and reviewers skim; split large work into stacked PRs.
  • Single purpose: one feature or one fix. "Add endpoint + refactor logging + bump deps" forces the reviewer to hold three conversations at once.
  • A body that answers what / why / how-tested, the template from step 01.
  • Green checks before review is requested: asking a human to read code CI has already rejected wastes the most expensive resource in the loop.
  • Linked issue: Closes #17 ties the change to the reason it exists and auto-closes the ticket on merge.

The inverse is also a skill: as a reviewer, a PR missing these signals is itself feedback. Ask for a split or a better description before diving into the diff, that is a legitimate --comment review.

Draft PRs and Work in Progress

Open a PR as a draft when you want CI feedback and early eyes but the change is not ready to merge. Drafts cannot be merged, which makes them a safe way to share half-finished work without risking an accidental merge:

DRAFT PR WORKFLOW

gh pr create --draft --title "WIP: caching layer" --body "Early feedback welcome"
# ...keep pushing commits; CI runs on every push...
gh pr ready                 # flip to ready-for-review when done
gh pr ready --undo          # back to draft if review uncovers big problems

Drafts are also the answer to "how do I run CI without merging": the checks run on every push to the PR branch whether or not the PR is a draft, so you get the full pipeline signal long before a reviewer is involved.

Keeping a PR Up to Date with main

PRs age. While yours waits for review, main moves, and with strict=true in branch protection you must be current before merging. Two ways to catch up, both ending in a force-free push:

UPDATE THE PR BRANCH

# option A: merge main in (keeps history, adds a merge commit)
git checkout feature/api
git fetch origin
git merge origin/main
git push

# option B: rebase onto main (linear, but rewrites the branch)
git fetch origin
git rebase origin/main
git push --force-with-lease

Prefer merge on shared branches and rebase on branches only you touch; --force-with-lease (never plain --force) refuses to overwrite commits someone else pushed since your last fetch. Either way, expect CI to re-run and, with dismiss_stale_reviews on, expect to ask for a re-approval.

$ gh pr view 42 --json mergeable,reviews --jq '{mergeable, approvals: [.reviews[] | select(.state=="APPROVED")] | length}'
{"mergeable":"MERGEABLE","approvals":1}

How to verify: mergeable reports MERGEABLE and the approval count is back above your required threshold before you reach for gh pr merge.

The Reviewer's Side

Reviews are easier to do well when you can run the code, not just read it. gh can check out any PR into a local branch in one command, so you can run the tests, poke the endpoint, and then leave your verdict:

CHECK OUT AND TEST A PR LOCALLY

gh pr checkout 42           # fetch PR head into a local branch
pip install -r requirements.txt
pytest -q                   # run the suite against the proposed change
curl -s localhost:8080/health   # manual smoke test
$ pytest -q
.... [100%] 4 passed in 0.42s

When you are done, git checkout main drops you back; the PR branch is disposable. Review the behavior first, the diff second, a change that reads beautifully but breaks the smoke test still gets --request-changes.

Review etiquette worth adopting early: comment on the code, not the author ("this branch dereferences null" beats "you broke this"); distinguish blockers from nits explicitly (nit: prefix means "optional, your call"); and respond to every review comment, even if only with "done in 3f2a1b9", silence forces the reviewer to re-read the diff to find out what changed.

Testing & Validation

gh pr list --state merged --limit 1 && git log main --oneline -1

You should see the merged PR and the squashed commit on main. With protection on, an unreviewed or failing PR cannot merge.

FULL VALIDATION SWEEP

gh pr list --state merged --limit 5 --json number,title,mergedAt \
  --jq '.[] | "#\(.number) \(.title) - \(.mergedAt)"'
git log main --oneline -3
gh api repos/TYFSADIK/demo-api/branches/main/protection --jq '.enforce_admins.enabled'
$ gh pr list --state merged --limit 1
42 Add /health endpoint for uptime checks TYFSADIK:feature/api MERGED

For a negative test, push a branch with a deliberately failing test and open a PR. The merge command must refuse. If it does not, your protection rule or required check context is wrong, revisit step 04.

Finally, confirm the audit trail is intact: the PR page should show the full conversation, the approving review, the green check rollup, and the squash commit linking back to the PR number. That trail is what you will dig through six months from now when the endpoint misbehaves, if it is all there, the lab is done.

gh PR Command Cheat Sheet

The commands from this lab, collected for reference. Everything assumes you are inside the repo clone; add --repo owner/name to run them from anywhere:

EVERYDAY gh PR COMMANDS

gh pr create --base main --fill          # open PR, reuse commit messages
gh pr status                             # your PRs + review/check state
gh pr list --state open                  # everything waiting on someone
gh pr view 42 --web                      # open in browser
gh pr diff 42                            # read the patch
gh pr checkout 42                        # run it locally
gh pr checks 42 --watch                  # follow CI until it finishes
gh pr review 42 --approve                # or --request-changes / --comment
gh pr merge 42 --squash --delete-branch  # merge + tidy up
gh pr ready 42                           # flip a draft to ready-for-review
Troubleshooting
  • Merge button blocked: branch protection requires checks/reviews; satisfy them first.
  • gh not authenticated: run gh auth login and select the right account.
  • Stale branch after merge: use --delete-branch to keep the repo tidy.
  • Head sha can't be blank on create: the branch was never pushed, or you are on the wrong branch. Run git push -u origin <branch> first, and confirm with git branch --show-current.
  • Required check never appears in the PR: the contexts[] name in the protection rule must match the check's reported name exactly (job name, not workflow file name). List what the PR actually reports with gh pr checks --json name,state and copy the string.
  • Approval does not unblock the merge: self-approvals never satisfy required reviews, and dismiss_stale_reviews voids approvals given before the latest push. Have a teammate re-approve after the final commit.
  • gh pr merge says the PR is not mergeable: usually a conflict with main. Rebase or merge main into your branch (git fetch origin && git merge origin/main), resolve, push, and wait for checks to re-run.

Troubleshooting

SymptomLikely causeFix
gh: command not foundthe GitHub CLI is not installedinstall it from cli.github.com, then gh auth login
PR shows no checksno workflow runs on the PR's eventsensure the workflow triggers on pull_request and the file is on the default branch
cannot merge: reviews requiredbranch protection demands approvalsrequest a review; the rule is doing its job
merge button offers unexpected methodrepo allows merge, squash, and rebasepick the team's agreed method in the dropdown; consistency beats preference
PR from a fork cannot access secretsfork PRs run with restricted permissionsuse pull_request_target carefully or require maintainer-triggered runs

Extension Ideas

  • Add automated checks in GitHub Actions so the required check has something real to run.
  • Require CODEOWNERS review for sensitive paths, a CODEOWNERS file routes PRs touching /infra/ or auth code to specific reviewers automatically.
  • Auto-merge once checks pass with gh pr merge --auto, so approved PRs land the moment CI goes green without anyone babysitting the queue.
  • Adopt a PR template (.github/pull_request_template.md) so every body starts with the what/why/how-tested structure from step 01.

Practice Challenges

Challenge 1: Review checklist

Goal: Write a PR template with a review checklist (tests, docs, security) and open a PR that fills it in.

Hint: Templates live in .github/pull_request_template.md and apply to every new PR.

Challenge 2: CODEOWNERS

Goal: Add a CODEOWNERS file so the right team is auto-requested on changes to critical paths.

Hint: Test it with a PR touching a covered path and confirm the review request appears.

Key Results

  • Opened and merged a PR entirely from the CLI.
  • Enforced review approval before merge.
  • Required passing checks via branch protection.
  • Kept main clean with squash + branch deletion.

Key Takeaways

  • A PR is a gate, not a formality: it combines human review, automated checks, and a permanent record of why a change happened.
  • Push the branch before gh pr create, and write a body that answers what, why, and how-tested: reviewers read it before the diff.
  • Check CI before requesting review; use --request-changes to block, --comment to ask, and push fixes to the same branch.
  • Squash-merge plus --delete-branch keeps main linear, revertible, and free of stale branches.
  • Branch protection is what turns the workflow from habit into policy: set it once, include admins, and verify it with a deliberately failing PR.