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
refis empty butcommitcontains a SHA, that SHA becomesresult.refdirectly. - Fully-qualified refs: For inputs like
refs/heads/mainorrefs/pull/123/merge, the function extracts the short name and maps it to the corresponding remote ref. For example,refs/heads/mainbecomesresult.ref = "main"withresult.startPoint = "refs/remotes/origin/main". - Unqualified names: For inputs like "main" or "v1.0", the code queries the remote using
git.branchExists()andgit.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
fetchTagsis 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/123for 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.tsto 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 viagetRefSpec(), and validation viatestRef()insrc/ref-helper.ts. - Unqualified refs (like "main" or "v1.0") require remote existence checks using
git.branchExists()andgit.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 viasrc/retry-helper.tswhen 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →