How actions/checkout Handles Pull Request HEAD Commit vs Merge Commit

TLDR: actions/checkout determines which commit to fetch by parsing the ref input, defaulting to refs/pull/<number>/merge for the merge commit, while explicitly providing refs/pull/<number>/head or a raw SHA triggers checkout of the PR head commit through specific fetch spec generation in src/ref-helper.ts.

The actions/checkout GitHub Action controls whether you receive the merge commit or the head commit of a pull request through its ref input parsing logic. By default, the action checks out the merge commit created by GitHub when running in a pull request workflow context, but you can override this to fetch the exact head commit of the PR branch. This behavior is governed by the ref normalization logic in src/ref-helper.ts and the input handling in src/input-helper.ts.

Default Behavior: The Merge Commit

When you use actions/checkout in a pull request workflow without specifying the ref input, the action automatically checks out the merge commit. This occurs because github.ref defaults to refs/pull/<number>/merge in pull request event contexts.

In src/input-helper.ts, the action reads the ref input (which defaults to github.ref) and passes it to the source provider. When this value contains refs/pull/<number>/merge, the getCheckoutInfo function in src/ref-helper.ts preserves the branch suffix and constructs the remote tracking ref as refs/remotes/pull/<number>/merge.

Ref Translation and Fetch Spec Construction

The core logic distinguishing head from merge commits resides in src/ref-helper.ts. The getCheckoutInfo function normalizes the input ref by checking its prefix:

// refs/pull/
else if (upperRef.startsWith('REFS/PULL/')) {
    const branch = ref.substring('refs/pull/'.length)
    result.ref = `refs/remotes/pull/${branch}`
}

This normalization extracts the PR number and suffix (merge or head) and constructs a remote ref path. The getRefSpec function then generates the Git fetch specification that determines which object Git actually retrieves.

For a merge commit, the fetch spec becomes:

+refs/pull/<number>/merge:refs/remotes/pull/<number>/merge

For a head commit (when refs/pull/<number>/head is provided), the spec becomes:

+refs/pull/<number>/head:refs/remotes/pull/<number>/head

If you provide a raw SHA instead of a ref, getRefSpec adds a direct SHA-to-ref mapping like +<sha>:refs/remotes/pull/<number>/head, causing Git to fetch that specific commit object.

Safety Validation for Fork Pull Requests

Before executing the fetch, src/unsafe-pr-checkout-helper.ts validates whether the checkout is safe. If you are checking out a pull request from a forked repository, the action throws an error unless you explicitly set allow-unsafe-pr-checkout: true in your workflow configuration. This security check prevents potentially malicious code from executing in your repository environment without explicit consent.

Execution in the Git Source Provider

Finally, src/git-source-provider.ts orchestrates the Git operations. It executes git fetch using the ref specification generated by ref-helper.ts, then checks out the resulting ref. Because the fetch spec maps the remote PR ref to a local remote-tracking branch, the checkout command receives the exact commit intended—whether that is the merge result or the head commit.

Practical Configuration Examples

To check out the merge commit (default behavior):

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Show merge commit
        run: git rev-parse HEAD

To check out the head commit explicitly:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Show head commit
        run: git rev-parse HEAD

Alternatively, you can use the ref syntax:

- uses: actions/checkout@v4
  with:
    ref: refs/pull/${{ github.event.pull_request.number }}/head

Summary

  • Default behavior: actions/checkout fetches refs/pull/<number>/merge (the merge commit) when the ref input is omitted in PR workflows.
  • Head checkout: Providing refs/pull/<number>/head or ${{ github.event.pull_request.head.sha }} forces checkout of the PR head commit.
  • Core logic: src/ref-helper.ts contains getCheckoutInfo and getRefSpec functions that normalize refs and build Git fetch specifications.
  • Security: src/unsafe-pr-checkout-helper.ts requires explicit opt-in (allow-unsafe-pr-checkout: true) when checking out forks.
  • Execution: src/git-source-provider.ts performs the actual git fetch and checkout using the generated specifications.

Frequently Asked Questions

What is the difference between refs/pull//merge and refs/pull//head?

refs/pull/<number>/merge is a synthetic ref created by GitHub that represents the result of merging the PR branch into the target branch, while refs/pull/<number>/head points directly to the tip commit of the pull request branch. The merge commit allows you to test the integration result, whereas the head commit represents the exact code submitted by the author.

How do I configure actions/checkout to use the head commit instead of the merge commit?

Set the ref input to either ${{ github.event.pull_request.head.sha }} or refs/pull/${{ github.event.pull_request.number }}/head. According to the logic in src/ref-helper.ts, this bypasses the default merge ref mapping and generates a fetch spec that retrieves the head commit instead of the merge commit.

What is the allow-unsafe-pr-checkout flag and when do I need it?

The allow-unsafe-pr-checkout input, declared in action.yml and enforced by src/unsafe-pr-checkout-helper.ts, is required when checking out pull requests from forked repositories. This flag prevents automatic execution of untrusted code from forks by requiring explicit opt-in to the security risk.

Which source files handle the ref parsing logic?

The primary ref parsing occurs in src/ref-helper.ts, specifically in the getCheckoutInfo function which normalizes PR refs, and getRefSpec which builds the fetch arguments. Input acquisition happens in src/input-helper.ts, while src/git-source-provider.ts executes the Git commands using these specifications.

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 →