GitHub Actions Checkout: Difference Between `ref` and `head.sha` for Pull Requests

Using ref checks out the virtual merge commit that integrates pull request changes with the target branch, while head.sha checks out the exact contributor commit without any merge logic.

When configuring the actions/checkout action for pull request workflows, the choice between resolving code via symbolic ref or specific head.sha determines exactly which commit lands in your runner workspace. According to the actions/checkout source code, these two approaches trigger fundamentally different Git fetch strategies in src/ref-helper.ts, resulting in either a pre-merged integration state or the raw contributor branch state.

How ref Resolves to a Merge Commit

When you allow the action to resolve the ref input automatically or specify a pull request merge ref explicitly, the action targets GitHub’s virtual merge commit.

The src/input-helper.ts module reads the ref input via core.getInput('ref'). If empty, it falls back to github.context.ref, which for pull request events typically returns refs/pull/<id>/merge. In src/ref-helper.ts, this pattern is converted into a refspec like +refs/pull/<id>/merge:refs/remotes/pull/<id>/merge. This fetches the virtual commit that GitHub creates by merging the PR branch into the target branch.

Key implication: Your workflow tests the exact state that would exist after the PR merges, including any conflict resolutions with the base branch.

How head.sha Resolves to the Exact Commit

When you pass the specific commit SHA via ref: ${{ github.event.pull_request.head.sha }}, the action bypasses the merge logic entirely.

In this mode, src/ref-helper.ts detects that the input is a full SHA rather than a symbolic ref. It constructs a refspec formatted as +<sha>:refs/heads/<branch> to fetch that specific object directly. The src/main.ts file captures this via core.setOutput('sha', sourceSettings.sha), ensuring downstream steps reference the exact contributor commit.

Key implication: You test only the code the contributor pushed, without any automatic merging, which avoids executing potentially untrusted merge logic from the target repository.

Source Code Architecture

The distinction between these approaches is enforced across three core modules:

  • src/input-helper.ts – Retrieves the ref input and determines whether to use the user-provided value or fall back to github.context.ref. For pull requests, the context ref defaults to the merge ref.

  • src/ref-helper.ts – Parses the incoming string to detect SHA vs. ref patterns. It builds the appropriate Git refspec: either fetching the merge ref or directly fetching the commit object.

  • src/main.ts – Orchestrates the fetch and checkout, setting outputs for ref and sha so subsequent workflow steps know precisely which revision is present.

Practical Use Cases

Choose the approach based on your security and testing requirements:

  • Testing final integration state – Use the default ref (merge commit) to verify the PR integrates cleanly with the target branch and catches merge-specific issues like conflicting generated files.

  • Security-hardened fork PRs – Use head.sha when processing pull requests from forks to avoid executing code from the virtual merge ref, which could contain malicious changes from the base repository.

  • Avoiding merge commits – Use head.sha when your repository has disabled merge commits (allow_merge_commit: false) or when you need to analyze the pure contributor diff without base branch contamination.

Configuration Examples

Checkout the Merge Commit (Default)

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0

This checks out refs/pull/<id>/merge, testing the post-merge state.

Checkout the Exact PR Head

steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}
      fetch-depth: 0

This checks out the exact commit from the contributor’s branch, skipping the virtual merge.

Explicitly Target a Specific Merge Ref

steps:
  - uses: actions/checkout@v4
    with:
      ref: refs/pull/42/merge

This explicitly fetches the merge ref for PR #42, useful when running outside the context of a pull request event.

Summary

  • The ref approach fetches the virtual merge commit (refs/pull/<id>/merge), testing the integration of PR changes with the target branch.
  • The head.sha approach fetches the exact contributor commit, avoiding any automatic merging or target branch code execution.
  • src/ref-helper.ts constructs different refspecs for each method: symbolic refs fetch merge commits, while full SHAs fetch direct commits.
  • Use ref for integration testing and head.sha for security-sensitive workflows or when you need the unaltered contributor state.

Frequently Asked Questions

What is the default checkout behavior for pull requests in actions/checkout?

By default, the action uses github.context.ref, which resolves to refs/pull/<id>/merge for pull request events. This means it checks out the virtual merge commit that combines the PR branch with the target branch, allowing workflows to test the post-merge state.

Is it safer to use head.sha for forked pull requests?

Yes. Checking out head.sha fetches only the specific commit from the contributor's fork, avoiding the virtual merge ref that GitHub generates. This prevents the workflow from inadvertently executing code introduced by the merge process itself, which is important when the base repository contains untrusted code.

Can I check out a pull request without creating a merge commit?

Yes. Set the ref input to ${{ github.event.pull_request.head.sha }} (or any specific commit SHA). The action will fetch that exact object directly via a refspec like +<sha>:refs/heads/<branch>, bypassing the merge ref entirely and checking out the raw branch state.

How does the action determine whether to fetch a ref or a SHA?

In src/ref-helper.ts, the action examines the input string. If it matches the pattern of a full Git SHA (40 hexadecimal characters), it treats it as a direct commit reference. Otherwise, it treats the input as a symbolic ref and constructs the appropriate fetch spec for that reference.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →