Skip to content

How to test a pull request: check it out, run it and merge it safely

Check out a pull request with gh pr checkout or git fetch, run the tests and the app, test the merge result, and keep untrusted PR code away from your secrets.

By the pyrovue team · · 9 min read

To test a pull request, check out its exact code (gh pr checkout 123, or git fetch origin pull/123/head:pr-123 with plain Git), install it from clean, run the test suite, then run the app and use the changed feature the way a user would. Check that the diff matches the description and that any migrations apply and roll back.

Test the merged result too. A branch that passes on its own can fail once it's combined with today's main, and that combination is what you're about to ship.

For a pull request from someone you don't know, the checkout is the easy part. The code runs with your permissions as soon as you install it or run its tests, so run it somewhere without your credentials, and keep CI workflows that hold secrets away from it.

How to check out and run a pull request locally

With the GitHub CLI

gh pr checkout takes a pull request number, URL or branch name, fetches the head commit and switches to a local branch:

gh pr checkout 123
gh pr view 123                 # title, description, status
gh pr diff 123 --name-only     # files it touches
gh pr checks 123               # CI results

Run gh pr checkout 123 again to pick up new commits, and add --force if the author rewrote history. --detach checks out the commit without creating a branch.

With plain Git

GitHub keeps every pull request's head commit under refs/pull/ID/head in the base repository, forks included. The documented way to fetch one is:

git fetch origin pull/123/head:pr-123
git switch pr-123

The refs/pull/ namespace is read-only, so you can't push to it. To update after the author pushes again, fetch into FETCH_HEAD and reset:

git fetch origin pull/123/head
git reset --hard FETCH_HEAD

If you review often, fetch every pull request as a remote branch:

git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
git switch --detach origin/pr/123

Test the merge result, not only the branch

For an open pull request without conflicts, GitHub also maintains refs/pull/ID/merge, a test merge into the base branch. It's what a pull_request workflow checks out by default. Fetch it with git fetch origin pull/123/merge:pr-123-merge, or build the same thing locally:

git fetch origin main
git switch -c pr-123-merged origin/main
git merge --no-edit pr-123

Keep your own work where it is

A worktree gives the pull request its own directory, so you don't have to stash anything. It also forces a clean install, which is what you want:

git worktree add ../myapp-pr-123 pr-123
cd ../myapp-pr-123

When you're done, git worktree remove ../myapp-pr-123 deletes it.

How to test a pull request from a fork

Both methods above work for forks without adding a remote, because the head ref lives in the base repository. The difference is pushing. If you want to push a fix to someone else's pull request, the author has to tick Allow edits from maintainers, which applies to forks owned by a user account. With that on, gh pr checkout sets the branch up to push to their fork, so a plain git push updates the pull request. With plain Git, add the fork as a remote and push to their branch:

git remote add alice https://github.com/alice/myapp.git
git push alice pr-123:fix-date-parsing

Here fix-date-parsing is the pull request's branch name on the fork, shown at the top of the pull request page.

Without that permission, push your commits to a branch of your own and suggest them in a review comment, or open a follow-up pull request.

How to test a GitLab merge request locally

GitLab stores merge requests under refs/merge-requests/IID/head. GitLab's docs cover the refspec and the CLI:

git fetch origin merge-requests/45/head:mr-45
git switch mr-45

With the GitLab CLI it's glab mr checkout 45. To fetch them all, add +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/* as a fetch refspec, the same way as the GitHub example.

What to test in a pull request

  1. Read the description, then the diff. Does the change do what it says, and nothing else? git diff --stat main...pr-123 lists every file. Edits outside the stated scope (CI config, the lockfile, build scripts) deserve a question before anything runs.

  2. Install from clean and look at new dependencies. git diff main...pr-123 -- package.json shows what was added. In the lockfile, look for packages resolved from unfamiliar registries, Git URLs or tarball links.

  3. Run the full test suite, then check the new tests test something. Run the pull request's tests against main's code. They should fail there:

    git switch --detach main
    git checkout pr-123 -- tests/
    pytest tests/
    git reset --hard && git switch pr-123
    

    A test that passes with and without the fix isn't covering the fix.

  4. Run the app and use the change. Do the happy path, then the cases the author probably didn't try: empty input, a very long value, a user without permission, a slow or failed request, a narrow screen. Watch the server log and the browser console for new errors and warnings.

  5. Check migrations. Apply them to a copy of realistic data, roll them back, and ask whether the code currently in production still works against the new schema, because during a rolling deploy it will run against it for a while.

  6. Check what CI doesn't. New environment variables that production needs, configuration and feature-flag changes, query counts on changed pages, bundle size and docs.

  7. Say what you tested. gh pr review 123 --approve --body "Ran the suite, applied and rolled back the migration, tested import with an empty file" tells the next reviewer what's left. Use --request-changes when something fails.

The commands for step 5 in common frameworks:

FrameworkApplyRoll back
Djangopython manage.py migratepython manage.py migrate APP PREVIOUS_MIGRATION
Railsbin/rails db:migratebin/rails db:rollback
Laravelphp artisan migratephp artisan migrate:rollback
Alembicalembic upgrade headalembic downgrade -1
Prismanpx prisma migrate deployNo automatic down migrations; npx prisma migrate reset rebuilds the database and deletes its data

In Django, also run python manage.py makemigrations --check --dry-run. It exits with an error when the pull request changed models without adding the migration.

How to test a pull request safely when you don't know the author

Checking out code runs nothing. Installing it and running its tests runs a lot: install scripts in package.json, setup.py and build backends, conftest.py, test setup files, Makefile targets and .vscode/tasks.json if you open the folder in a trusted VS Code window. A pull request can change any of those, and they run as you, with your SSH keys, cloud credentials and tokens within reach.

Before running a stranger's pull request, list the files that execute during install and test:

gh pr diff 123 --name-only | grep -E 'package\.json|lock|setup\.py|pyproject|conftest|Makefile|\.github/|\.vscode/|\.devcontainer/'

Read those changes first, then run it in a container, a VM or a cloud box that holds none of your credentials. Is it safe to run code from GitHub covers what each of those options shares with your laptop.

The pull_request_target pitfall

CI needs the same care. With the pull_request trigger, a workflow run for a fork's pull request gets no secrets other than a read-only GITHUB_TOKEN, and on public repositories a maintainer may have to approve runs for first-time contributors. Read changes to .github/workflows/ before you approve.

pull_request_target is different. It runs in the context of the base repository's default branch, with access to the repository's secrets and a GITHUB_TOKEN that can have write access, so that it can label or comment on pull requests from forks. Adding a checkout of the pull request's code to it is the pattern GitHub Security Lab calls a pwn request. This workflow is the one to avoid:

on: pull_request_target
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm ci && npm test

Anyone who opens a pull request controls the install and test scripts in its package.json, and those scripts now run with your secrets. Since 8 December 2025, GitHub always takes a pull_request_target workflow and its default checkout from the default branch, which stops outdated workflow files on other branches from running. It does nothing for a workflow that checks out the pull request's code explicitly. The fix is to build and test under pull_request, with no secrets, and to do the privileged part (a comment, a deploy) in a separate workflow_run workflow that treats the first one's artifacts as untrusted.

How to test every pull request before merging

  • CI on every pull request, with required checks. Mark the test job as a required status check in a branch protection rule or ruleset so nothing merges red. GitHub's pull_request runs test the merge commit, which covers the merged-result check above.
  • A merge queue. A merge queue tests each pull request against the latest base branch plus the pull requests queued ahead of it, which catches two changes that pass alone and fail together.
  • Preview deployments. Frontend hosts such as Vercel and Netlify deploy each pull request to its own URL, so reviewers can click through without checking anything out.
  • Ephemeral environments. For a full stack with a database and background workers, a short-lived environment per pull request gives reviewers the whole app. Preview environments compares the ways to build them. Building one for a fork's pull request runs that pull request's code, so the pwn-request rules above apply to your preview pipeline as well.

The shortcut: open the pull request running in a box

pyrovue turns a pull request link into a running app with no preview pipeline to build. Open https://pyrovue.com/github.com/OWNER/REPO/pull/NUMBER, or paste the pull request's GitHub URL into pyrovue. Before anything starts, you see the title, the author and whether the code comes from a fork. pyrovue then runs the pull request's latest commit, forks included, in a fresh box, sets it up the way it sets up any repository, and gives you a live preview, a terminal to run the tests and the setup log. Rebuild picks up new pushes. Pull request previews walks through it.

The box holds none of your files or keys. It is a container that shares the host's kernel, so it's a weaker boundary than a VM, but a stranger's install scripts never touch your laptop. GitLab merge requests can't be run yet. There's no Docker inside a box (pyrovue reproduces container-only projects natively instead) and no GPU.

To get a link on every pull request automatically, add the workflow from the button page. It uses pull_request_target on opened and reopened with only pull-requests: write, and it only posts a comment: it never checks out or runs the pull request's code, which is what keeps that trigger safe. Whoever opens the link runs the pull request in a box on their own plan.

When a pull request is too big to test well

A pull request that touches 60 files across three features can't be tested properly in one sitting. Ask for it to be split, or review it commit by commit, oldest first, with git log --reverse -p main..pr-123. If the change lands in a part of the code you don't know, spend ten minutes on the surrounding modules first; how to understand a new codebase has a quick method. Then test the riskiest piece first, so a problem shows up before you've spent an hour on the rest.

Run the repo you bookmarked and never tried.

Hobby is free and needs no card. Paste a link and your first box is up in about a minute.