Documentation
¶
Overview ¶
Package lockfile is the source of truth for the workflow dependency lockfile format and pin grammar.
Parsing as a security boundary ¶
ParseActionRef is the choke point. Untrusted uses: strings enter; only concrete repository actions leave. Everything else — expressions, docker:// images, local paths, reusable workflows, control characters — returns nil, before it can reach a URL or GraphQL builder.
ParseReusableWorkflowRef is its mirror: the reusable-workflow shape ParseActionRef rejects, parsed through the same validation. Both split the ref at the first @, never the last — a ref may legitimately contain one.
owner/repo/path pass isValidSegment: a fixed character set, ".."/"." barred. Drop-in safe. The ref is looser by necessity — git refs carry slashes, dots, even another @ — so isValidRef only guarantees it cannot escape a quoted literal or smuggle a traversal. A ref still needs escaping before it touches a URL path. Owner/repo do not.
Hand-rolled, no regexp, allocation-free, single-pass: it runs per dependency on the hot path and the reject-lists must stay auditable at a glance. They are load-bearing. Do not refactor them into regular expressions.
Index ¶
- Constants
- Variables
- func IndexKey(owner, repo, ref string) string
- func IsFullSha(s string) bool
- func IsLocalReusableWorkflow(localPath string) bool
- func Schema() string
- func ShortSHA(s string) string
- func SplitNWO(nwo string) (owner, repo string, ok bool)
- 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
Constants ¶
const Path = ".github/workflows/actions.lock"
Path is the canonical repo-relative location of the dependency lockfile.
const Version = "v0.0.1"
Version is the only supported lockfile schema version.
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.
Functions ¶
func IndexKey ¶
IndexKey builds the normalized lookup key for a dependency entry without the digest: "OWNER/REPO@REF".
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 `./...`-prefixed local `uses:` value names a reusable workflow file (rather than a local composite action directory). Exposed for consumers that walk workflows themselves and need to distinguish the two shapes.
func Schema ¶
func Schema() string
Schema returns the embedded JSON Schema document for the supported lockfile version. Callers can surface it for editor integration or external validation. Parse checks the document's shape — known keys, required fields, version — and canonicalizes pin keys, but does not reject entries whose keys aren't canonical pins; they're preserved for consumer diagnostics. Note the schema's pin pattern constrains pin *values* (the workflow and uses arrays), not the dependencies map keys, so schema validation alone won't enforce that every dependency key is a canonical pin either.
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 {
Tag string `yaml:"tag,omitempty"`
Branch string `yaml:"branch,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.
Tag is the discovered release/tag at the commit, if one exists. Optional.
Branch is a branch that contains the pinned commit. Required: a commit not on any branch is an impostor / fork-network signal, so Parse rejects an Action without one. It is the authenticity check that SHA-only pinning lacks.
Commit holds the digest in algo-prefixed form (e.g. "sha1-..." or "sha256-..."). Required.
Uses lists the action's direct nested dependencies (composite action `uses:` steps) as canonical pin keys. Empty for leaf actions; required for composites, a condition Parse can't enforce structurally.
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 file into an ActionMeta. 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 resolve to ExecUnknown rather than failing.
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 string `yaml:"version"`
Dependencies map[string]Action `yaml:"dependencies"`
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:sha1-8e8c483db84b4bee98b60c0593521ed34d9990e8
dependencies:
actions/checkout@v4.3.1:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5:
tag: v4.3.1
branch: main
commit: sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
owner_id: 44036562
repo_id: 197814629
uses:
- actions/cache@v4.0.0:sha1-...
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 ¶
Parse unmarshals YAML lockfile contents and verifies the version is supported. It enforces structural validity — unknown top-level keys are rejected and required fields must be present — but does not validate pin integrity (e.g. whether a SHA actually matches the ref) or action existence. That belongs to the consumer (e.g. gh-actions-pin's check command).
Action map keys and workflow dependency entries are canonicalized via ParsePin so downstream lookups by canonical key (e.g. pin.String()) match regardless of the source casing of owner/repo/algo/hex in the YAML. Entries that do not parse as a valid pin are left untouched; consumers can flag them via diagnostics. Workflow path keys are NOT canonicalized — filesystem paths are case-sensitive on the platforms we run on.
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 dependency closure for the given repo-relative workflow key (e.g. ".github/workflows/deploy.yml"). The returned bool reports whether the key was found.
type ParseError ¶
ParseError describes a failure to parse a dependency lockfile.
Line and Column, when non-zero, are the 1-indexed position within the lockfile contents that the failure refers to. They index the lockfile itself, never a consumer's workflow file, so callers can anchor diagnostics on the lockfile (.github/workflows/actions.lock) rather than scraping yaml.v3's error string themselves.
Column is populated for semantic failures Parse detects itself (it walks the retained YAML node tree to the offending key/value). It is left zero for raw yaml.v3 decode failures, whose errors report only a line: a malformed document has no node tree to read a column from, and yaml.v3 type errors carry a line but no column.
Msg is the human-readable reason with yaml.v3's "yaml:" package prefix and leading position removed.
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"
Algo string // "sha1"
Hex string // "34e114876b0b11c390a56381ad16ebd13914f8d5"
}
Pin holds the parsed components of a dependency pin key.
"OWNER/REPO@REF:ALGO-HEX"
The pin identifies a downloaded action tarball at repo+SHA granularity — matching the runner, which downloads `owner/repo@sha` 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:ALGO-HEX"
Returns ok=false if the string doesn't match the expected format, including any sub-action path component (e.g. "owner/repo/sub@ref:...") — the lockfile grammar is strictly repo-scoped, matching the runner's tarball download identity.
func (Pin) Canonical ¶
Canonical returns a copy of p with all case-insensitive components (owner, repo, algo, hex) 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 ""
Major int
Minor int
Patch int
Rest string // anything after patch (e.g. "-beta.1")
Raw string
}
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.
func (SemVer) IsMajorOnly ¶
IsMajorOnly reports whether the raw tag is a bare major version (e.g. "v4").
func (SemVer) IsMutable ¶
IsMutable reports whether this version is a partial (major-only or major.minor) tag that should be narrowed to a specific patch version.
func (SemVer) IsStable ¶
IsStable returns true if the tag has no pre-release suffix or trailing junk.
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.