Help Shape Workflow Dependency Locking 👋 🔒 #194494
Replies: 11 comments 10 replies
-
|
Thank you for opening this discussion. I think this proposal is important because it addresses a concrete GitHub Actions instance of a broader architectural condition I previously isolated as architectural non-determinism: the reviewed object and the executable object can diverge unless the effective execution context is fixed before execution. Reference: Identical Inputs, Divergent Outcomes: Architectural Non Determinism as a Governance and Liability Risk. So my question is: could GitHub expose the resolved dependencies: graph as an input to policy/security tooling before run admission, so that automation-created or AI-assisted workflow changes can fail closed before any job executes? That would make dependency locking not only a reproducibility mechanism, but a pre-execution admission surface. |
Beta Was this translation helpful? Give feedback.
-
|
First thoughts:
|
Beta Was this translation helpful? Give feedback.
-
|
One major difference between the sha based locking and traditional lockfiles is that the latter often include a hash of the contents of the dependency. While the SHA itself might not be mutated, an action can easily include things that can change, e.g. because they're pulling in external things. So it kinds sounds like the SHA based approach is just kicking the can down the road? For example, Python's |
Beta Was this translation helpful? Give feedback.
-
|
I don't really see how any of this will work for me. I'd love for a human to spend some time talking to me, but I also want to talk about other things, so it's a tradeoff. My stack has (some optional, some not) dependencies from:
Many of these dependencies are optional (hunspell is only needed if a workflow asks to use hunspell dictionaries; tesseract is only needed if a workflow asks to check images, ...) or can be provided by the caller (if a dependency is already available and has a satisfactory version, it'll be used as is, e.g. Classically, build systems aiming for reproducible builds tended to fail miserably because they neglected to record information about what the OS provided, or about transient dependencies that were satisfiable by some In theory, GitHub's runner images themselves are fully pinned, but almost nothing in the apt world is truly fully pinned, and most apt packages (especially those that use -devel/-dev headers) don't fully pin, which means that the binaries they provided themselves aren't truly reproducible -- but if you rely on a specific signed binary package, you could at least go for "traceable". (I worked on a Debian based linux distribution for 5 years, and quickly learned that the version definitions people imagined did not apply to rebuildable binaries.) -- And I should note that GitHub's runners turned out to be considerably more nondeterministic than people expected (the GitHub Runner itself can significantly influence how a runner image behaves -- ask me how I know). Would people be interested in providing a way for actions to provide a SBOM/BOM reporting which things they retrieved so that someone who is curious could review that and try to reproduce a build by satisfying the SBOM/BOM contents before running steps? I'm not strictly opposed to providing an SBOM-like thing if someone provided an easy way to do so. (Requiring users to pay for GitHub Advanced Security or requiring Which CPAN package manager would people actually expect developers to use to satisfy the goals presented here? https://github.com/tokuhirom/anton (died 12 years ago) A number of my GitHub action dependencies are runtime selected. e.g., actions/upload-artifact is only used in some cases, and when it isn't used, my action does not download it. A pinning file that forced all possible dependencies to be downloaded even in cases where they weren't used would waste immense amounts of time. -- I work very hard to make my action start quickly -- I took the effort to replace github/codeql-action because it had a really horrible packaging story (they've since fixed it, but I have no intention of adding it as a dependency going forward). Note that Debian/Ubuntu images themselves do not pin the world, and even GitHub kicks packages out to make space (GitHub's setup-java action broke scala at some point and didn't care to fix it when GitHub dropped sbt or something), they just provide the ability to dynamically add packages based on requests (apt-get install). Also fun, Similarly, my action can use any number of external dictionaries (configured by the workflow caller). It does not make sense for me to bundle an infinite number of potential dictionaries into my action or into a dependency lock file (beyond the fact that it's obviously impossible), as the specific dictionaries that a consumer will use will vary based on the consumer. I'm more than happy to provide URLs and SHAs for such files (again, see SBOM), but I can't reasonably inline such content into my action. @samus-aran I'm open to a call (or a recurring call, I collect these). But, I'd also encourage you to take a look at my action first. |
Beta Was this translation helpful? Give feedback.
-
|
Personally, i'd prefer to see the hashes in a separate single lockfile (for the whole repo, and/or per workflow) - for CODEOWNERS control, diff churn, etc. In other words, like in almost every ecosystem, the dependency manifest should be separate from the lockfile. |
Beta Was this translation helpful? Give feedback.
-
|
This sounds like a great endeavor! I'm excited for it :D I will echo the preference for the I'd also prefer to avoid using comments to indicate transitive dependencies. My motivation comes from two directions:
Maybe a lockfile format like this? .github/workflows/dependencies.lock # Automatically generated and managed by gh-actions-pin
<workflow-1.yml>:
- name: github.com/actions/checkout@v4.3.1:sha1-34e114876b0b11c390a56381ad16ebd13914f8d5
- name: github.com/nodeselector/actions-test-fixtures/nested-composite@updated:sha1-ea53476fdc172d8552df5af9658a45a367e4f41d
dependencies:
- name: github.com/nodeselector/actions-test-fixtures/simple-composite@main:sha1-d010fee89969c44873ca2c1b501905190eb7410d
dependencies:
- github.com/nodeselector/actions-test-fixtures-b/simple-echo@main:sha1-92b7b0058bc223c6e9dd4e19ef9247c934ba7637
- github.com/nodeselector/actions-test-fixtures/simple-node@main:sha1-d010fee89969c44873ca2c1b501905190eb7410d
<workflow-2.yml>:
- name: ...I don't have meaningful experience building package management ecosystems, so I'm happy to be corrected on the feasibility of this proposed lockfile format. I recognize that a flat list is easier to load than a recursive walk, but a richer structure akin to the one I've sketched here would be helpful. |
Beta Was this translation helpful? Give feedback.
-
|
Speaking as zizmor's maintainer, I think my primary concerns are:
Like others, I think I would strongly prefer it if GitHub were to pursue a detached lockfile approach here. Going even further, I think it would be really great if the ultimate lockfile was global across all workflows, and not just per-workflow. In other words, I think it would be really great if there was a single Finally, I think it would be great for GitHub to ideate (if not necessarily share) long-term plans for making locked resolutions of actions mandatory, rather than opt-in. Users would ideally not have to opt into locking, because the median user won't. Edit: I'll also echo the sentiment that, whatever GitHub does here, it should not continue the unfortunate trend of communicating important semantic information (like transitive relationships) via non-semantic state (YAML comments). Ideally a lockfile/lock representation here would be an actual machine-readable graph representation like |
Beta Was this translation helpful? Give feedback.
-
|
We're pinning all of our actions in |
Beta Was this translation helpful? Give feedback.
-
|
Do you still plan to keep the current behavior of using git as binary repository? If not, and if you plan to finally support an OCI images registry or another file registry, the lock file should also include the origin. |
Beta Was this translation helpful? Give feedback.
-
|
Quick datapoint from the field, hopefully useful for the design. I have been auditing public
A few observations that may inform the 1. The distribution is bimodal. Repos either fully SHA-pin (bun, uv, vitest, archestra) or barely pin at all (prisma at 89). There is almost no "half-pinned" middle. This is consistent with the idea that pinning is a culture / tooling decision more than an incremental hygiene one — once a repo has the muscle memory plus Dependabot config it stays pinned, and once it doesn't it stays unpinned. A 2. The 3. SHA pinning is necessary but not sufficient. @provinzkraut's point upthread about the SHA pin not covering transitive runtime fetches is correct. In the four-repo corpus above, I see 4. 5. False-positive sensitivity matters more than people expect. The first version of my rule 3 (command injection) over-flagged I am happy to feed the raw scan outputs into any spec-review process and to update gha-shield to emit the new Looking forward to the design landing. |
Beta Was this translation helpful? Give feedback.
-
|
It would be nice to be able to pin permissions (OCAP-style) separately from SHAs - i'd love to avoid the problems caused by pinning SHAs, allowing free-flowing updates - but lock down the permissions a workflow has, ensuring that if they widen, the update is blocked. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Actions / Workflow Dependency Locking
TLDR;
This discussion is focused on feedback for:
dependencies:workflow contractgh actions pinCLI experienceHi everyone 👋
We are designing workflow-level dependency locking for GitHub Actions and we want your opinion before we finalize the shape of it. This was previewed in our 2026 GitHub Actions Security Roadmap earlier this quarter, and it is the first deliverable on that roadmap.
This post focuses on two things:
dependencies:section that records the pinned state of every direct and transitive action your workflow uses.gh actions pinCLI command that generates and maintains that section, as well as integration into Dependabot.Please share thoughts, concerns, and alternative designs. Nothing here is final.
The problem
GitHub Actions has no persistent record of what dependencies a workflow actually resolved when it ran. Resolution happens at runtime, the results are discarded, and there is no way to diff what changed between two runs.
Composite actions make this worse — they embed nested
uses:steps that are invisible to the workflow author and only discovered after the top-level action is downloaded by the runner.Every mature package ecosystem — npm, Cargo, Go modules, Bundler, NuGet — has a lockfile, a manifest, and a deterministic resolver. Actions has none of them.
That gap means:
These problems exist independent of whether action refs are mutable or immutable. Even if every release were immutable tomorrow, without a pinned dependency manifest there is still no committed record, no PR diff, and no transitive visibility.
Mutable refs make the problem more dangerous, supply chain attacks like the
tj-actions/changed-filesincident silently moved tags to malicious commits. Solving mutability is complementary work on the security roadmap. Dependency pinning gives you a point-in-time resolution: good or bad, it is what you reviewed and checked in.The impact today:
actions/checkout@abc123…) are unreadable, hard to upgrade, and still expose no transitive dependencies. Additionally, they do not guarantee that the SHA itself exists in the named repository and not an attacker controlled fork.We want to close that gap, and we want to do it in a way that does not break the workflows you have today.
What we are proposing
We are introducing a
dependencies:section in workflow YAML that locks every direct and transitive dependency to an exact commit SHA. Think of it as Go'sgo.mod+go.sum, but for your workflow, with full reproducibility and auditability.For the first time, workflows can record the exact commit SHAs of direct and indirect (via composite actions) dependencies, directly in a
dependencies:section inside the workflow YAML. The runner verifies and uses those exact resolutions before any job executes.Note
The CLI is the first phase of the developer experience, not the end state. We are starting here because it gives us a fast surface to validate the workflow contract and the resolution behavior. Once that shape is settled, we plan to extend the experience to other touchpoints, including the workflow editor on github.com, IDE integrations, and Dependabot, so that pinning and updating dependencies feels native wherever you author or review workflows.
Guaranteeing resolution at job run time
What is Actions resolution? It's the behind the scenes process of turning a
uses:statement referencing an action into bits on the runner. Today this involves resolving a ref to a sha, applying policies, and downloading the contents of the action. This proposal makes workflows deterministic by shifting the ref-to-sha resolution into the workflow definition itself. What you resolved and pinned is what gets executed, even if the ref no longer points to the same sha. Resolution happens once, via a CLI command, and the result is committed into the workflow file.The workflow then runs against that locked state on every subsequent run. This aligns with how every mature package ecosystem (npm, Cargo, Go modules, Bundler, NuGet) already works: a manifest you author, a lockfile that pins the resolved graph, and a deterministic resolver that connects the two.
Example: a fully resolved workflow
The
dependencies:section is generated and maintained by thegh actions pinCLI (seecli/cli#13314for the RFC). You author thejobs:section as you do today; the CLI walks everyuses:reference, recurses into compositeaction.ymlfiles, and writes the locked graph back into the same file. Demo and example workflow output below:Demo.mov
A few things worth noting in the example above:
lintjob declares two directuses:. The composite atnodeselector/actions-test-fixtures/nested-composite@updatedpulls insimple-composite, which in turn pulls insimple-echoandsimple-node. All four transitive actions are surfaced explicitly independencies:.buildjob usesreusable-build.yml@main, and its dependency graph would be locked the same way.@v4.3.1,@updated,@main) next to the resolved SHA, so reviewers can see what was asked for and what it actually resolved to at the same time.# Automatically generated and managed by gh-actions-pin) makes ownership explicit and signals that the section should be regenerated, not hand edited.What this changes in practice
In your workflows, this means you will be able to:
By making dependency resolution explicit, durable, and reviewable, we turn what runs in CI into something you can audit, diff, and trust, without forcing changes on workflows that are happy with the current behavior.
Thanks for reading 🙏
Beta Was this translation helpful? Give feedback.
All reactions