Documentation
¶
Overview ¶
Package lockfile is the source of truth for the GitHub Actions dependency lockfile format and its Go parser.
Getting started ¶
Parse is the primary entry point. Hand it the raw bytes of .github/workflows/actions.lock and it returns a File whose Workflows and Dependencies maps let you look up every pinned action for a workflow:
f, err := lockfile.Parse(contents)
pins, ok := f.LookupWorkflow(".github/workflows/release.yml")
File.LookupWorkflow returns canonical pin key strings. Each key can be looked up in File.Dependencies to retrieve the associated Action metadata (commit hash, branch, tag, repository IDs).
Parsing uses: strings ¶
ParseActionRef parses a single `uses:` string from a workflow step into its owner/repo/ref components. It returns nil for anything that is not a repository action — expressions, docker:// images, local paths, reusable workflow files — so callers never need to classify the input themselves.
ParseReusableWorkflowRef is its mirror for the reusable-workflow shape (owner/repo/.github/workflows/name.yml@ref) that ParseActionRef deliberately rejects. Use IsLocalReusableWorkflow for the local ./.github/workflows/... shape, which has no owner/repo and is handled differently.
Both parsers split the ref at the FIRST @, not the last — a ref may legitimately contain @ (e.g. a branch named "release@2024").
Security note for contributors ¶
owner/repo/path components pass isValidSegment (fixed character set, ".."/"." barred) before reaching any URL or GraphQL builder. Ref validation is minimal (non-empty, no colons) — the workflow parser itself does no ref character validation, so neither do we. These validators are hand-rolled, allocation-free, and single-pass because they run on the hot path. Do not replace them with regular expressions.
Index ¶
- Constants
- Variables
- func BestRef(tag, branch string) string
- func IndexKey(owner, repo, ref string) string
- func IsFullSha(s string) bool
- func IsLocalReusableWorkflow(localUses string) bool
- func Schema() string
- func SchemaForVersion(version string) (string, bool)
- func ShortSHA(s string) string
- func SplitNWO(nwo string) (owner, repo string, ok bool)
- func SplitRef(ref string) (tag, branch string)
- type Action
- type ActionMeta
- type ActionRef
- type ExecutionType
- type File
- type ParseError
- type Pin
- type ReusableWorkflowRef
- type SemVer
- func (s SemVer) Greater(o SemVer) bool
- func (s SemVer) IsFull() bool
- func (s SemVer) IsMajorOnly() bool
- func (s SemVer) IsMutable() bool
- func (s SemVer) IsStable() bool
- func (s SemVer) MajorTag() string
- func (s SemVer) MinorTag() string
- func (s SemVer) Narrows(other SemVer) bool
- func (s SemVer) UpgradeOver(other SemVer) bool
- type VersionPolicy
Constants ¶
const CLIName = "gh actions-lock"
CLIName is the canonical name of the CLI extension that manages lockfiles. Use this in user-facing messages instead of hardcoding the string, so all consumers (parser, launch, docs) stay consistent if it ever changes again.
const MaxActionMetaSize = 1 << 20 // 1 MiB
MaxActionMetaSize is the maximum byte length ParseActionMeta will accept. action.yml files in the wild are well under 1 MiB.
const MaxParseSize = 1 << 20 // 1 MiB
Parse unmarshals the raw bytes of a lockfile and returns the parsed File. Pass the contents of .github/workflows/actions.lock (available as the Path constant) or any other lockfile source.
Parse checks structural validity — unknown top-level keys are rejected and required Action fields must be present — but does not verify pin integrity (e.g. that a SHA matches the ref) or that actions exist on GitHub. Those checks belong to the caller (e.g. the check command in gh-actions-lock).
MaxParseSize is the maximum number of bytes Parse will accept. Inputs larger than this are rejected before any YAML parsing takes place to prevent memory-exhaustion DoS from oversized or yaml-bomb documents.
const Path = ".github/workflows/actions.lock"
Path is the canonical repo-relative location of the dependency lockfile.
const Version = "v0.0.2"
Version is the latest lockfile schema version this binary writes.
Variables ¶
var ErrFutureVersion = errors.New("lockfile version is newer than this binary supports")
ErrFutureVersion is the sentinel returned (via errors.Is) when Parse refuses a lockfile whose schema version is newer than this binary supports. External consumers (e.g. Dependabot) can detect this specific failure mode without scraping the error string.
var ErrUnsupportedVersion = fmt.Errorf("lockfile version is older than this consumer supports")
ErrUnsupportedVersion is the sentinel returned when ParseWithPolicy refuses a lockfile whose version is older than the consumer's minimum.
Functions ¶
func BestRef ¶ added in v0.0.4
BestRef picks the single ref value for lockfile serialization. Tag wins if non-empty, else branch. Both empty yields empty.
func IsFullSha ¶
IsFullSha reports whether s looks like a full commit hash (SHA-1 or SHA-256). Callers use this to distinguish bare-SHA `uses:` refs from symbolic refs.
func IsLocalReusableWorkflow ¶
IsLocalReusableWorkflow reports whether a local `uses:` value (one that starts with "./") names a reusable workflow file rather than a composite action directory.
Pass the raw, untrimmed `uses:` string from the workflow step — the leading "./" must be present:
- "./.github/workflows/ci.yml" → true (local reusable workflow)
- "./my-composite-action" → false (local composite action)
Call this only after confirming the `uses:` value has a "./" prefix; a value without "./" is a repository action or reusable workflow, not a local reference, and should be parsed with ParseActionRef or ParseReusableWorkflowRef instead.
The distinction matters because composite action directories and reusable workflow files are resolved differently by the runner: workflow files are fetched from a specific checked-out path, while directories are run as composite actions.
func Schema ¶
func Schema() string
Schema returns the embedded JSON Schema document for the latest lockfile version (v0.0.2). Callers can surface it for editor integration or external validation.
func SchemaForVersion ¶ added in v0.0.4
SchemaForVersion returns the embedded JSON Schema for a specific lockfile version. Returns ("", false) for unknown versions.
func ShortSHA ¶
ShortSHA returns the first 12 characters of a SHA, or the full string if shorter. Used for human-readable log and diagnostic output.
func SplitNWO ¶
SplitNWO splits an owner/repo (Name-With-Owner) string into its two components. It returns ok=false for inputs that don't carry both an owner and a repo segment: the empty string, anything without a slash, a leading slash ("/repo"), and a trailing slash without a repo ("owner/").
For inputs with extra path segments ("owner/repo/sub/..."), only the first two segments are returned; the remainder is dropped. This matches Dependency.OwnerRepo and the lockfile's repo-granularity pin grammar (sub-action paths are graph traversal details, not pin identity).
SplitNWO does not validate the owner/repo character set — use ParseActionRef when parsing a verbatim `uses:` value where stricter charset rules apply.
Types ¶
type Action ¶
type Action struct {
Ref string `yaml:"ref,omitempty"`
Commit string `yaml:"commit,omitempty"`
OwnerID int64 `yaml:"owner_id"`
RepoID int64 `yaml:"repo_id"`
Uses []string `yaml:"uses,omitempty"`
}
Action carries the per-action metadata recorded in the lockfile under the pin key.
Ref is the git ref the commit was resolved from. Required: every dep that passes impostor checks has a resolvable ref. The CLI picks the best ref with this priority: full semver tag (e.g. v4.3.1) > any tag (including major-only like v4) > branch (protected > default > release/v* or releases/v* > any). The parser enforces presence and non-emptiness but not the priority ordering — that's the CLI's concern.
Commit holds the digest in algo-prefixed form (e.g. "sha1-abc123..." or "sha256-def456..."). This is the same digest that appears in the pin key. Required.
OwnerID and RepoID are the GitHub numeric IDs for the action's repository owner and repository respectively. Consumers use them to detect if the action has been transferred to a new owner between lockfile regenerations — a repository transfer changes the owner name but not the owner ID.
Uses lists the action's direct nested dependencies (composite action `uses:` steps) as canonical pin keys. Empty for leaf actions (node, docker); populated for composite actions.
type ActionMeta ¶
type ActionMeta struct {
Name string
Execution ExecutionType
NestedUses []string
}
ActionMeta is the parsed subset of `action.yml` (or `action.yaml`) relevant to dependency resolution: the action's name, how it executes, and composite action nested `uses:` strings.
func ParseActionMeta ¶
func ParseActionMeta(content string) (*ActionMeta, error)
ParseActionMeta parses the contents of an action.yml or action.yaml file into an ActionMeta. Pass the raw file bytes as a string; the file name itself is not needed. Composite actions emit their nested step `uses:` strings in NestedUses; non-composite actions return an empty NestedUses.
Returns an error only on malformed YAML. Unknown `runs.using` values (e.g. a future executor type) resolve to ExecUnknown rather than failing, so callers can handle them gracefully.
type ActionRef ¶
type ActionRef struct {
Owner string // e.g. "actions"
Repo string // e.g. "checkout"
Path string // e.g. "save" for actions/cache/save@v4
Ref string // tag, branch, or full SHA as written after `@`
Raw string // original `uses:` string (post-trim)
}
ActionRef is a parsed `uses:` reference to a repository action. It captures only the components the lockfile grammar cares about: owner, repo, optional sub-action path, ref string, and the original raw value for diagnostics.
ParseActionRef is the only constructor; consumers should treat zero values as invalid.
func ParseActionRef ¶
ParseActionRef parses a `uses:` string into an ActionRef. It returns nil for any input that is not a repository action — expression-based refs, local paths, docker images, reusable workflow files, or any input whose owner/repo/path/ref components are unsafe to hand to the downstream URL/GraphQL builders (control characters, traversal tokens, or quote/whitespace metacharacters in the ref).
The returned pointer is non-nil iff the input names a real repository action (composite or javascript) at owner/repo[/path]@ref.
type ExecutionType ¶
type ExecutionType string
ExecutionType describes how an action runs.
const ( ExecNode ExecutionType = "node" ExecDocker ExecutionType = "docker" ExecComposite ExecutionType = "composite" ExecUnknown ExecutionType = "unknown" )
type File ¶
type File struct {
// Version is the lockfile schema version string (e.g. "v0.0.1"). It is
// always equal to the [Version] constant for files Parse accepts.
Version string `yaml:"version"`
// Dependencies maps each canonical pin key (OWNER/REPO@REF) to
// the resolved [Action] metadata for that pin. The map is deduplicated:
// multiple workflows that share an action produce a single entry here.
// Use [File.LookupWorkflow] to find the pin keys for a specific workflow,
// then index into this map to retrieve each action's metadata.
Dependencies map[string]Action `yaml:"dependencies"`
// Workflows maps each repo-relative workflow file path (e.g.
// ".github/workflows/release.yml") to the flat, transitive list of
// canonical pin keys that workflow depends on. Pin keys are in
// OWNER/REPO@REF form and serve as lookup keys into
// Dependencies. Prefer [File.LookupWorkflow] over indexing this
// map directly.
Workflows map[string][]string `yaml:"workflows"`
// contains filtered or unexported fields
}
File is the parsed lockfile shape.
# .github/workflows/actions.lock
version: v0.0.1
workflows:
.github/workflows/deploy.yml:
- actions/checkout@v6
dependencies:
actions/checkout@v4.3.1:
ref: v4.3.1
commit: sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
owner_id: 44036562
repo_id: 197814629
uses:
- actions/cache@v4.0.0
The Go field `Dependencies` maps to the YAML key `dependencies:` — the lockfile's deduplicated action DAG. Each entry's `uses:` list names the action's direct nested dependencies, reusing the same canonical pin keys. Workflow entries hold the full transitive closure as a flat list of pin keys for cold readability.
func Parse ¶
Optional paths parameter ¶
The variadic paths parameter is optional. Most callers should omit it.
Omit paths (or pass nil) to validate every dependency entry in the lockfile. This is the right choice for whole-file tooling: CLI regeneration, Dependabot, schema linters.
Pass one or more repo-relative workflow file paths (e.g. ".github/workflows/deploy.yml") to limit field validation to only the dependency entries referenced by those workflows. Entries outside the requested set are still parsed and returned, but required-field checks are skipped for them. This lets a single corrupt unrelated entry fail without blocking the workflows you actually care about.
A path that does not appear in the lockfile's workflows map silently contributes zero entries to validate — the lockfile is returned as-is for that path. This is intentional: a workflow not yet onboarded into the lockfile should not cause Parse to fail.
Canonicalization ¶
Action map keys and workflow dependency entries are lowercased via ParsePin so that lookups by Pin.String succeed regardless of the source casing of owner, repo, algorithm, or hex in the YAML. Entries that are not valid pin strings are preserved verbatim for caller diagnostics. Workflow path keys are NOT canonicalized — file paths are case-sensitive on Linux.
func ParseWithPolicy ¶ added in v0.0.4
func ParseWithPolicy(contents []byte, policy VersionPolicy, paths ...string) (File, error)
ParseWithPolicy is like Parse but enforces version bounds. The lockfile's declared version must be within [policy.Min, policy.Max] inclusive.
func (File) KeyPosition ¶
KeyPosition is like Position but resolves the position of the final path segment's *key* node rather than its value. It is the right anchor for map entries whose key is the meaningful token (e.g. a dependency pin key or a workflow path under "workflows").
func (File) LookupWorkflow ¶
LookupWorkflow returns the flat, transitive list of canonical pin keys for the given repo-relative workflow path (e.g. ".github/workflows/deploy.yml"). The returned bool reports whether the workflow path was found in the lockfile.
Each string in the returned slice is a canonical pin key in OWNER/REPO@REF form. To retrieve the full action metadata for a pin, look it up in File.Dependencies:
pins, ok := f.LookupWorkflow(".github/workflows/deploy.yml")
for _, key := range pins {
action := f.Dependencies[key]
fmt.Println(action.Ref, action.Commit)
}
A workflow that is present in the lockfile but has no dependencies returns an empty slice and ok=true. ok=false means the workflow path was never onboarded into the lockfile at all.
type ParseError ¶
ParseError describes a failure to parse a dependency lockfile. It is always returned (via errors.As) by Parse rather than plain errors, so callers can print file:line:col diagnostics without scraping error strings.
Line and Column, when non-zero, are the 1-indexed position within the lockfile bytes that the failure refers to. They index the lockfile file (.github/workflows/actions.lock), not any workflow .yml file.
Column is set for semantic failures that Parse detects by walking the retained YAML tree (e.g. an unknown field, a duplicate pin key). It is zero for low-level YAML syntax errors where only a line number is available — a structurally malformed document has no node tree to resolve a column from.
Msg is the human-readable description of the failure, without any position prefix. Use Error() to get the full "line N, column M: reason" string.
func (*ParseError) Error ¶
func (e *ParseError) Error() string
func (*ParseError) Unwrap ¶
func (e *ParseError) Unwrap() error
type Pin ¶
type Pin struct {
NWO string // "actions/checkout"
Owner string // "actions"
Repo string // "checkout"
Ref string // "v4"
}
Pin holds the parsed components of a dependency pin key.
"OWNER/REPO@REF"
The pin identifies a downloaded action tarball at repo+ref granularity — matching the runner, which downloads `owner/repo@ref` once per ref and reuses the tree for any sub-action path. Sub-action paths (e.g. the `save` in `actions/cache/save@v4`) are graph traversal details, not pin identity, and do not appear in this serialized form.
func ParsePin ¶
ParsePin parses a pin string of the canonical form:
"OWNER/REPO@REF"
Returns ok=false for any input that does not match — including all of:
- Missing "@" separator between repo and ref
- A sub-action path in the repo portion (e.g. "owner/repo/sub@ref") — the lockfile grammar is strictly repo-scoped, matching the runner's tarball download identity
On success, owner and repo are normalized to lowercase. Ref preserves source casing — git refs are case-sensitive. The returned Pin is always in canonical form.
func (Pin) Canonical ¶
Canonical returns a copy of p with all case-insensitive components (owner, repo) normalized to lowercase. Ref preserves source casing — git refs are case-sensitive.
This is the single normalization point for the lockfile pin grammar: String, IndexKey, and ParsePin all funnel through it, so callers never need their own ToLower bookkeeping when handing pins through this package.
type ReusableWorkflowRef ¶
type ReusableWorkflowRef struct {
Owner string // e.g. "octo"
Repo string // e.g. "workflows"
Path string // e.g. ".github/workflows/release.yml"
Ref string // tag, branch, or full SHA as written after `@`
Raw string // original `uses:` string (post-trim)
}
ReusableWorkflowRef is a parsed `uses:` reference to a reusable workflow file — the owner/repo/.github/workflows/<name>.yml@ref shape that ParseActionRef deliberately rejects. It carries the same components as ActionRef; Path is the in-repo workflow file path (e.g. ".github/workflows/release.yml"), and Ref is the full ref as written after the FIRST `@`, so a ref containing `@` survives intact.
ParseReusableWorkflowRef is the only constructor; treat zero values as invalid.
func ParseReusableWorkflowRef ¶
func ParseReusableWorkflowRef(uses string) *ReusableWorkflowRef
ParseReusableWorkflowRef parses the *remote* reusable-workflow `uses:` shape that ParseActionRef rejects: owner/repo/.github/workflows/<name>.yml@ref. It returns nil for anything that is not a remote reusable workflow — repository actions, expression refs, docker images, or any input whose components are unsafe for the downstream URL/GraphQL builders.
It deliberately rejects LOCAL reusable workflows (./.github/workflows/...); those have no owner/repo and a different resolution path. Use IsLocalReusableWorkflow for that shape. It also rejects nested paths such as .github/workflows/sub/ci.yml: GitHub reusable workflows live directly under .github/workflows/, so only a single file segment is accepted.
It is the mirror of ParseActionRef for the reusable shape, and shares the same first-`@` split and security validation. Downstream consumers that must derive a reusable workflow's repository and file path (e.g. to locate that repo's detached lockfile) should use this rather than hand-rolling the split: a naive last-`@` split mis-parses refs that contain `@`.
The returned pointer is non-nil iff the input names a reusable workflow file at owner/repo/.github/workflows/<name>.{yml,yaml}@ref.
func (ReusableWorkflowRef) FullName ¶
func (r ReusableWorkflowRef) FullName() string
FullName returns owner/repo/path — the fully-qualified reusable workflow identity.
func (ReusableWorkflowRef) NWO ¶
func (r ReusableWorkflowRef) NWO() string
NWO returns owner/repo (Name With Owner). Zero-value refs return the empty string.
type SemVer ¶
type SemVer struct {
Prefix string // "v" or "" — whether the original tag had a "v" prefix
Major int
Minor int
Patch int
// Rest is everything after the patch number, e.g. "-beta.1" for
// "v1.2.3-beta.1". An empty Rest means the version is stable (no
// pre-release suffix). See [SemVer.IsStable].
Rest string
Raw string // original tag string as written (e.g. "v4" or "2.0.0-rc.1")
}
SemVer holds parsed semantic version components.
API stability: the type and its comparison helpers (Greater, Narrows, UpgradeOver, MajorTag, MinorTag, IsFull) are part of the exported surface because the tag recommendation engine in downstream consumers relies on them. Their semantics are deliberately non-strict-semver (see below) and are committed to as-is; they are not internal helpers despite the recommendation-engine flavor.
GitHub Actions has no first-class version scheme — a uses: ref can be any git ref (tag, branch, SHA, or even "main"). In practice most action authors follow semver-ish conventions, but the ecosystem diverges from strict semver in ways that golang.org/x/mod/semver cannot handle: bare versions without a "v" prefix ("2.0.0"), partial versions ("v4", "v4.2"), and arbitrary suffixes all appear in the wild. x/mod/semver rejects bare and partial tags, and doesn't expose individual components — we need Major/Minor/Patch to compute MajorTag, MinorTag, and IsFull for the tag recommendation engine.
func ParseSemVer ¶
ParseSemVer parses a version tag into its components. Returns false if the tag doesn't look like a version (or is a full SHA that happens to start with a digit).
func (SemVer) Greater ¶
Greater reports whether s should be preferred over o: higher major.minor.patch wins; on a tie a stable version beats a pre-release, a v-prefixed tag beats the same bare version, then a lexicographic compare of the raw tags provides a deterministic final tie-break.
func (SemVer) IsFull ¶
IsFull returns true if the version has all three components (major.minor.patch) and no pre-release suffix. Tags like "v4" or "v4.2" return false. IsFull is the prerequisite for a locked dependency: only a full version uniquely identifies a release.
func (SemVer) IsMajorOnly ¶
IsMajorOnly reports whether the raw tag is a bare major version (e.g. "v4").
func (SemVer) IsMutable ¶
IsMutable reports whether the version is a partial (major-only or major.minor) tag — the opposite of SemVer.IsFull. A partial tag like "v4" or "v4.2" is "mutable" because the author can silently move it to a new patch commit without changing the tag name, which makes it unsafe to trust without a SHA pin. Use SemVer.Narrows to find a full patch version that narrows a mutable tag.
func (SemVer) IsStable ¶
IsStable returns true if the version has no pre-release suffix (Rest == ""). "v1.2.3" is stable; "v1.2.3-beta.1" is not.
func (SemVer) Narrows ¶
Narrows reports whether s is a more specific patch version of other. e.g. other="v4", s="v4.1.0" → true; other="v4.2", s="v4.2.1" → true.
func (SemVer) UpgradeOver ¶
UpgradeOver reports whether s represents a real version upgrade over other. Returns false for noops where other is already at or more specific than s.
type VersionPolicy ¶ added in v0.0.4
type VersionPolicy struct {
// Min is the oldest version the consumer can read. Lockfiles older than
// this are rejected with ErrUnsupportedVersion.
Min string
// Max is the newest version the consumer can read. Lockfiles newer than
// this are rejected with ErrFutureVersion.
Max string
}
VersionPolicy controls which lockfile schema versions a consumer accepts. Servers set their own policy to control the rollout of new formats.
func DefaultPolicy ¶ added in v0.0.4
func DefaultPolicy() VersionPolicy
DefaultPolicy returns a policy that accepts all versions this binary can parse — from the oldest supported through the latest.