Skip to content

Lock the Transitive CI Dependency Graph

A uses: reference resolves at run time, so locking the CI dependency graph is what ties a reviewed workflow to the commits you read.

Transitive CI dependency locking records the commit every action and reusable workflow in your CI graph resolved to, along with the dependencies those actions pull in, and keeps runs on those commits when a tag moves. Coverage has a documented gap, below. GitHub Actions workflow lockfiles reached public preview on 14 October 2026 for Enterprise Cloud, Enterprise Cloud with data residency, Team, and Free plans (GitHub Changelog).

Read it as a reproducibility control. GitHub says so directly: "Locking does not determine whether the dependency itself is safe, so review dependencies before locking them" (same source). The lockfile makes the artifact you reviewed the artifact that executes.

When locking earns its place

Three conditions. The third is the one teams skip.

Your workflows depend on actions you do not own. A uses: octo-org/deploy-action@v3 line is a constraint, because "GitHub Actions resolves that reference to whatever commit the tag or branch points to at run time" (GitHub Docs). Nothing in your repository changes when that commit does.

Your dependencies have dependencies you cannot see. A composite action resolves its own references, and your workflow file never names them.

Someone regenerates the entries. Dependabot does this without extra configuration: when it opens a version update for an onboarded action, "it also regenerates the matching lockfile entry in the same pull request" (GitHub Changelog). Pair it with the staleness check, which verifies the lockfile "is up to date without modifying files, including in a CI check" (same source). Without both, you have frozen a version rather than pinned one.

What the lockfile records

The file lives at .github/workflows/actions.lock, is generated by the gh actions-lock CLI extension, and "should never be edited by hand" (GitHub Docs). Each dependency entry carries the ref, the commit it resolved to, and an owner_id and repo_id. A bare SHA has no room for those. Actions "uses these to confirm at run time that the repository behind the name is still the one you locked" (same source).

Coverage has a stated boundary. The lockfile extends to transitive dependencies, "except for dependencies of reusable workflow calls. A reusable workflow is covered by the lockfile of the repository where it is defined" (same source). One more constraint: the locked commit "must exist within a branch of the source repository" (same source).

Why it works

Resolution moves from run time to review time. Andrew Nesbitt, credited by name in GitHub's announcement, named the causal step before the feature shipped: "Every run re-resolves from your workflow file, and the results can change without any modification to your code" (nesbitt.io). Recording the resolved commit removes the second resolution, so a moved tag cannot change what executes.

Pinning a reference you can read leaves the ones you cannot: "When you pin an action to a SHA, you only lock the outer file", and the pre-lockfile mitigations "do nothing for transitive dependencies, which is the primary attack vector" (same source).

When this backfires

  • Your CI is centralized in reusable workflows. Transitive coverage stops at those calls, so the dependencies a platform team wants pinned sit in someone else's lockfile.
  • The action fetches code at run time rather than drifting its reference. Palo Alto Networks measured this class in 2023 across the top 1,000 starred Marketplace actions: "We learned that 32% of the actions in the top starred list were, in fact, unpinnable." Of roughly 6,000 workflows in 2,000 projects, "we discovered that 67% of the projects pinned unpinnable actions" (Yaron Avital, Palo Alto Networks). JavaScript actions "can still fetch outside resources at runtime, so you should consider them unpinnable actions" (same source). A lock entry there is accurate and worth less than it looks.
  • Nobody wires the regeneration loop. The USENIX Security 2022 survey of 447,238 workflows across 213,854 repositories found "18% of repositories in our dataset execute at least one Action with missing security updates" (Koishybayev et al.). That survey predates lockfiles, so it measures what no-update looks like rather than locking's cost, and locking is what makes that state durable.
  • Upstream deletes the branch holding your locked commit, or force-pushes past it. An abandoned action is where this lands first.
  • A plan or policy rules it out. The feature is public preview and subject to change, and the Require lockfile policy needs admin reach.

Example

GitHub documents the two-section shape. workflows says which dependencies each onboarded workflow uses; dependencies says which commit each reference resolves to (GitHub Docs):

# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
version: 'v0.0.2'
workflows:
    '.github/workflows/ci.yml':
        - 'octo-org/deploy-action@v3.1.0'
dependencies:
    'octo-org/deploy-action@v3.1.0':
        ref: 'v3.1.0'
        commit: 'sha1-...'
        owner_id: 44036562
        repo_id: 197814629

The workflow file still reads @v3.1.0. The commit line is what runs, and the two id fields are what stop a repository that reclaimed the name from being accepted in its place.

Key Takeaways

  • Treat the lockfile as reproducibility, never as vetting. GitHub's own caveat is the one sentence to carry into a review conversation.
  • A diff-clean repository is not evidence that CI runs the same code it ran last week. The reference stayed; the commit behind it did not.
  • Onboard workflows one at a time: "You can onboard a repository's workflows to the lockfile one at a time or all at once", and afterwards "Every action dependency the workflow uses must be present in the lockfile" (GitHub Docs).
  • Land the staleness check and Dependabot in the same change as the lockfile, or you have shipped a freeze.
  • Check how much of your graph reaches run-time fetching before you claim coverage. A lock over an action that fetches at run time still leaves new code arriving.