How actions/checkout Resolves Git References: A Deep Dive into the Source Code

The actions/checkout action resolves git references through a three-phase process in src/ref-helper.ts that determines the checkout target, builds fetch ref-specs, and validates the resulting commit against the requested ref.

Whether you provide a branch name, tag, pull request number, or full commit SHA, actions/checkout must translate that input into a concrete git ref that can be fetched and checked out. This resolution logic lives primarily in the src/ref-helper.ts file within the actions/checkout repository and operates through a carefully orchestrated pipeline.

The Three-Phase Resolution Pipeline

The reference resolution process follows three distinct phases, each handled by specific functions in src/ref-helper.ts.

Phase 1: Determining the Checkout Target with getCheckoutInfo()

The getCheckoutInfo() function builds an ICheckoutInfo object that instructs the runner which ref to fetch and, for branch-type refs, which remote ref serves as the start point (upstream branch).

  • Raw SHA input: When ref is empty but commit contains a SHA, that SHA becomes result.ref directly.
  • Fully-qualified refs: For inputs like refs/heads/main or refs/pull/123/merge, the function extracts the short name and maps it to the corresponding remote ref. For example, refs/heads/main becomes result.ref = "main" with result.startPoint = "refs/remotes/origin/main".
  • Unqualified names: For inputs like "main" or "v1.0", the code queries the remote using git.branchExists() and git.tagExists() to determine whether the name refers to a branch or tag. If neither exists, the action throws an error.

Phase 2: Building Fetch Ref-Specs with getRefSpec()

The getRefSpec() function returns the list of +src:dst specifications that tell Git which refs to bring into the shallow clone.

  • Tag fetching: When fetchTags is true, the generic ref-spec +refs/tags/*:refs/tags/* is always added.
  • Commit SHA resolution: When a specific SHA is supplied, the function creates a ref-spec mapping that SHA to the appropriate remote ref, such as +<sha>:refs/remotes/pull/123 for pull request refs.
  • Unqualified refs: The function falls back to wildcard fetches of both heads and tags (+refs/heads/*:refs/remotes/origin/* and +refs/tags/*:refs/tags/*) so the remote can be matched locally after fetching.
  • Fully-qualified refs: These receive direct mapping in the format +<ref>:<dest>.

Phase 3: Validating the Fetched Reference with testRef()

After the fetch completes, testRef() confirms that the fetched ref points to the expected commit.

  • Branch validation: The function verifies that origin/<branch> exists and that its SHA matches the requested commit.
  • Tag validation: For tags, it dereferences annotated tags using the ^{commit} suffix and compares the resulting SHA.
  • Failure handling: If validation fails, the action triggers the retry logic in src/retry-helper.ts to perform a full fetch of all history before retrying resolution.

Orchestration and Error Handling

These three phases are orchestrated by src/git-source-provider.ts, which calls getCheckoutInfo(), executes git.fetch(refSpec) with the specs from getRefSpec(), and finally invokes testRef() to ensure success. The provider also handles special cases such as shallow clones, sparse checkout, and submodule initialization.

When testRef() reports a mismatch—such as when a force-push occurs between the initial fetch and validation—the retry logic in src/retry-helper.ts implements exponential backoff before attempting a full history fetch.

Practical Examples

Workflow Configuration (YAML)

steps:
  - uses: actions/checkout@v4
    with:
      # Resolve a branch name

      ref: main
  
  - uses: actions/checkout@v4
    with:
      # Resolve a specific commit SHA

      ref: ''
      sha: 4a1b2c3d4e5f6g7h8i9j0klmnopqrstuv
  
  - uses: actions/checkout@v4
    with:
      # Resolve a pull request

      ref: refs/pull/123/merge
      fetch-tags: true

Programmatic Usage (JavaScript)

import * as core from '@actions/core'
import * as checkout from '@actions/checkout'

async function run() {
  const ref = core.getInput('ref')
  const sha = core.getInput('sha')
  
  // Internally runs getCheckoutInfo(), getRefSpec(), 
  // fetch(), and testRef()
  await checkout.checkout({
    repository: 'owner/repo',
    ref,
    sha,
    fetchTags: true
  })
}

run()

Summary

  • Reference resolution occurs in three phases: target determination via getCheckoutInfo(), ref-spec construction via getRefSpec(), and validation via testRef() in src/ref-helper.ts.
  • Unqualified refs (like "main" or "v1.0") require remote existence checks using git.branchExists() and git.tagExists() before resolution.
  • Ref-spec generation adapts to input types, using wildcards for unqualified names, direct mappings for fully-qualified refs, and SHA-specific mappings for commit references.
  • Validation and retry logic ensures correctness through testRef() and falls back to full history fetches via src/retry-helper.ts when shallow clones prove insufficient.

Frequently Asked Questions

How does actions/checkout handle unqualified ref names like "main" or "v1.0"?

When you provide an unqualified name, the action queries the remote repository using git.branchExists() and git.tagExists() (implemented in src/git-command-manager.ts) to determine whether the name refers to a branch or tag. If exactly one matches, the action proceeds with that type; if neither or both match ambiguously, the resolution fails with an error.

What happens when actions/checkout receives a specific commit SHA?

When the ref input is empty and commit contains a SHA, getCheckoutInfo() sets that SHA as the direct target. The getRefSpec() function then creates a specific ref-spec mapping that SHA to the appropriate remote ref location, allowing the fetch to retrieve just that commit rather than entire branch histories.

How does the action validate that the fetched ref matches the requested commit?

The testRef() function performs post-fetch validation by checking that origin/<branch> exists and matches the expected SHA for branches, or by dereferencing annotated tags with ^{commit} and comparing SHAs for tags. This verification catches race conditions where the remote ref might have changed between the fetch and checkout operations.

Where does the retry logic live when ref resolution fails?

The retry logic resides in src/retry-helper.ts, which implements exponential backoff. When testRef() detects a mismatch—such as when the shallow clone lacks sufficient history to verify the ref—the helper triggers a full fetch of all repository history before attempting the resolution process again.

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 →