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 therefinput and determines whether to use the user-provided value or fall back togithub.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 forrefandshaso 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.shawhen 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.shawhen 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
refapproach fetches the virtual merge commit (refs/pull/<id>/merge), testing the integration of PR changes with the target branch. - The
head.shaapproach fetches the exact contributor commit, avoiding any automatic merging or target branch code execution. src/ref-helper.tsconstructs different refspecs for each method: symbolic refs fetch merge commits, while full SHAs fetch direct commits.- Use
reffor integration testing andhead.shafor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →