gh-actions-lock

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

18posts · 30d
Nov 31 post
  1. github/gh-actions-lock ってのが出てたんだ〜

    24 views
  2. DEV · · @sascha-ms

    GitHub Actions lockfile: pin your workflows safely A GitHub Actions lockfile is something I have missed for a long time. With npm, Maven or Go it has been normal for years that exactly the version someone reviewed is the one that runs. In workflows, on the other hand, you would find uses: someone/something@v3, and which code sat behind that tag was only decided when the job started. Since October 28, 2026, workflow lockfiles are generally available, and with them you can finally close this gap without unreadable walls of SHAs. In this article we set up the lockfile step by step in a sample repository (a fictional online bookshop), build a check for pull requests that highlights suspicious changes, and look at how to introduce the mandatory policy in your team without seeing every pipeline turn red on Monday morning. > In short > > The new lockfile .github/workflows/actions.lock records the commit and the repository ID for every action and its transitive dependencies, so moved tags can no longer bring unknown code into your pipeline. You create and verify it with the CLI extension gh actions-lock, and Dependabot keeps it up to date. The biggest pitfall are tag-based allowlists for actions: they stop matching after the switch. What you will learn in this article - Which problem the GitHub Actions lockfile solves and which it does not - How to install gh actions-lock and generate your first lockfile - How CI, Dependabot and CODEOWNERS keep the lockfile current and reviewed - How a small TypeScript tool flags suspicious lockfile changes in a PR - How to roll out the “Require lockfile” policy without outages Why tags in workflows are a risk A Git tag is a pointer. Anyone with write access to a repository can move it to a different commit. That is exactly what makes @v3 convenient, because maintainers move the major tag forward with every patch release. At the same time it is the weak spot: if an attacker takes over a maintainer account or a token with write access, v3 may point tomorrow to code nobody on your team has ever seen. Your workflow loads it anyway, with access to secrets, tokens and build artifacts. The classic countermeasure is SHA pinning directly in the workflow, i.e. uses: actions/checkout@<40-character hash>. It works, but it is tedious to read and only protects the first level. If a composite action calls further actions by tag, you are unprotected again at that point. In addition, a hash does not answer whether the repository behind the name is still the same one, for example after a rename when someone registers the old name again. What a lockfile changes here The lockfile separates readability from security. Your workflow keeps the descriptive tag, and the lockfile records which commit it resolved to when it was locked. GitHub also stores the numeric IDs of the owner and the repository. When a run starts, Actions takes the stored commit and checks whether the repository behind the name is still the locked one. A moved tag is not even resolved again during a normal run. One thing matters for the big picture: the lockfile does not judge whether an action is good or bad. It makes sure that what you locked is what runs. If you lock a compromised version, you reliably get the compromised version. Reviewing before locking therefore remains mandatory. !Diagram: workflow reference @v3 is checked via actions.lock against commit and repository ID, moved tags are ignored At start, Actions takes the locked commit and checks the repository ID; a moved tag does not matter. How the GitHub Actions lockfile is structured The file always lives at .github/workflows/actions.lock, it is YAML and it is generated by the CLI extension. You should not edit it by hand. An excerpt for our bookshop looks roughly like this (the hashes are shortened): # .github/workflows/actions.lock # 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': - 'actions/checkout@v7.0.1' - 'actions/setup-node@v6.0.0' dependencies: 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-...' owner_id: 44036562 repo_id: 197814629 'actions/setup-node@v6.0.0': ref: 'v6.0.0' commit: 'sha1-...' owner_id: 44036562 repo_id: 123456789 The workflows section lists which workflow file uses which dependencies. Under dependencies you find, for each reference, the tag (ref), the resolved commit (commit) and both IDs. The IDs of setup-node in this excerpt are placeholders, your file contains the real values. A workflow counts as “onboarded” as soon as it is listed in the lockfile. From then on, every one of its action dependencies must appear there, and Actions runs the stored commit. What the lockfile covers It covers the actions in your workflows and their transitive dependencies, including what a composite action calls internally. Reusable workflows are an exception: their dependencies are covered by the lockfile of the repository in which the reusable workflow is defined. If your team maintains shared workflows in a central repository, that is where you should start. Set up the GitHub Actions lockfile step by step Our starting point is a plain CI workflow for the bookshop. It checks out the code, installs Node.js and runs the tests. The action versions here are only examples, use the ones you already rely on today. # .github/workflows/ci.yml name: ci on: pull_request: push: branches: [main] permissions: contents: read jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 22 - run: npm ci - run: npm test Prerequisites - The GitHub CLI gh, signed in via gh auth login, and write access to the repository. - A repository on github.com or in GitHub Enterprise Cloud with data residency. According to its README, the extension does not support GitHub Enterprise Server. - According to the changelog, lockfiles are available on the Free, Team and Enterprise Cloud plans. At the time of writing, some docs pages still carry the preview notice, so details may still change. - Recommended: Dependabot version updates for GitHub Actions are already enabled (more on that shortly). Step 1: Install the extension gh extension install github/gh-actions-lock gh actions-lock --help Later, gh extension upgrade gh-actions-lock fetches new versions. If you work with an AI coding agent, gh skill install github/gh-actions-lock teaches it to regenerate the lockfile itself after workflow changes. That is a convenience, not a control. Step 2: Generate the lockfile In the root directory of the repository, a single command generates the lockfile for all workflows under .github/workflows/. If you want to start small, pass individual files. # all workflows at once gh actions-lock # or just one workflow to begin with gh actions-lock .github/workflows/ci.yml If the extension reports that it skips a workflow because of local actions, a reference of the form uses: ./… could not be resolved. Local paths always start at the repository root, not at the folder of the workflow file. For special cases there are the options --migrate-local-actions and --ignore-local-actions. Step 3: Actually read the diff Now comes the part many people skip. The extension changes more than just the lockfile. For example, it rewrites partial versions like @v7 to the full tag, i.e. @v7.0.1, while the commit stays the same. According to the docs, local references uses: ./… become uses: $/…, because they always belong to the commit that is currently running and need no lockfile entry. Use git diff to look at every changed file and then commit everything together. git diff -- .github/ git add .github/workflows/ git commit -m "ci: lock GitHub Actions dependencies" Step 4: Check your allowlists This is where the most common pitfall hides. If your repository, organization or enterprise restricts the allowed actions, GitHub matches that list against the locked commit SHAs after the switch. An entry like actions/checkout@v6 will then no longer match. Replace such entries with a pattern like actions/checkout@* or with the full SHA before you push the commit. After that, the CI workflow runs as before, just with pinned code. Expected result: a green run, and a new file actions.lock sitting next to your workflows in the repository. Keep the lockfile current A lockfile that is generated only once goes stale quickly. As soon as someone adds or removes an action, gh actions-lock has to run again. Four building blocks make sure this does not depend on anyone’s memory. Dependabot updates the lockfile too When Dependabot bumps the version of an action in an already onboarded workflow, it regenerates the matching lockfile entry in the same pull request, without extra configuration. However, Dependabot does not onboard new workflows by itself, it only reports that onboarding is missing. I like to combine this with a cooldown so that fresh releases can mature for a few days before they reach us. # .github/dependabot.yml version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cooldown: default-days: 7 Check pull requests automatically The command gh actions-lock --verify writes nothing, but runs the same checks as a normal run and exits with an error if the lockfile no longer matches the workflows. That makes it suitable as a required check. The extension’s repository ships a template; my variant adds a step that writes lockfile changes neatly into the job summary (we build the tool for that in the next section). # .github/workflows/verify-lockfile.yml name: verify-lockfile on: pull_request: push: branches: [main] permissions: contents: read jobs: verify: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v7.0.1 with: persist-credentials: false fetch-depth: 0 - run: gh extension install github/gh-actions-lock --pin v0.2.0 # Fails if actions.lock does not match the workflows - run: gh actions-lock --verify - uses: actions/setup-node@v6 with: node-version: 22 - name: Summarize lockfile changes if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} run: | git show "origin/$BASE_REF:.github/workflows/actions.lock" > "$RUNNER_TEMP/base.lock" 2>/dev/null || true npm ci --prefix tools/actions-lock-review node tools/actions-lock-review/src/cli.ts "$RUNNER_TEMP/base.lock" .github/workflows/actions.lock >> "$GITHUB_STEP_SUMMARY" Two details matter to me here. The workflow deliberately has no paths filter, because a required check that does not even start for some PRs blocks merging. And the name of the target branch reaches the script through an environment variable instead of directly via ${{ }}, so no expression slips into the shell unchecked. After adding this workflow, run gh actions-lock once, because it uses actions too and has to be in the lockfile. Pre-commit hook and CODEOWNERS Locally, a hook helps that checks before every commit whether each dependency has an entry. It works offline and needs no authentication, but it does not verify the pins against GitHub. That remains the job of the CI check. # .git/hooks/pre-commit (make it executable with chmod +x) #!/bin/sh gh actions-lock --verify-local --no-fix So that lockfile changes do not slip through unnoticed, the file belongs in CODEOWNERS. Combined with a branch rule that requires code owner review, someone responsible for the pipeline always looks at new commits. # .github/CODEOWNERS /.github/workflows/actions.lock @bookshop/platform-team > 🎬 Pull request with a changed action – this animated diagram only runs on my site: watch it on sascha-ms.de Make lockfile changes visible in reviews Nobody enjoys reading a YAML diff full of 40-character hashes. The docs name two signals that matter: a changed commit is new code that will run, so treat it like a version bump. A changed owner_id or repo_id should almost never happen and means that a different repository is behind the name. A small TypeScript tool that I keep in the folder tools/actions-lock-review/ highlights exactly these two cases. Create the project The tool runs directly on Node.js 22.18 or newer, which executes TypeScript without a build step as long as only “erasable” syntax such as type annotations is used. The only runtime dependency we need is the YAML parser yaml. mkdir -p tools/actions-lock-review && cd tools/actions-lock-review npm init -y npm pkg set type=module npm install yaml npm install -D typescript @types/node { "compilerOptions": { "target": "ES2024", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noEmit": true, "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "types": ["node"] }, "include": ["src", "test"] } Read the lockfile The first file reads the lockfile and checks that the fields we rely on are present. Empty text stands for the first pull request, when there is no lockfile in the target branch yet. // tools/actions-lock-review/src/lockfile.ts import { parse } from "yaml"; export interface LockedDependency { ref: string; commit: string; owner_id: number; repo_id: number; } export interface ActionsLock { version: string; workflows: Record<string, string[]>; dependencies: Record<string, LockedDependency>; } // Reads the content of an actions.lock and checks the fields we need. // Empty text means "there is no lockfile yet" (e.g. in the first PR). export function parseLockfile(text: string): ActionsLock { if (text.trim() === "") { return { version: "none", workflows: {}, dependencies: {} }; } const data = parse(text) as Partial<ActionsLock> | null; if (!data || typeof data !== "object") { throw new Error("actions.lock is not a valid YAML object"); } const dependencies = data.dependencies ?? {}; for (const [key, dep] of Object.entries(dependencies)) { if (!dep || typeof dep.commit !== "string" || typeof dep.repo_id !== "number" || typeof dep.owner_id !== "number") { throw new Error(`Entry "${key}" is incomplete (expected commit, owner_id, repo_id)`); } } return { version: String(data.version ?? "unknown"), workflows: data.workflows ?? {}, dependencies, }; } Find and report differences The second file compares the lockfile of the target branch with the one from the pull request and produces a Markdown table that GitHub renders nicely in the job summary. // tools/actions-lock-review/src/diff.ts import type { ActionsLock } from "./lockfile.ts"; export type ChangeKind = "added" | "removed" | "commit" | "identity"; export interface LockChange { key: string; kind: ChangeKind; before?: string; after?: string; } const short = (sha: string) => sha.replace(/^sha1-/, "").slice(0, 12); // Compares the lockfile from the target branch (base) with the one from the PR (head). export function diffLockfiles(base: ActionsLock, head: ActionsLock): LockChange[] { const changes: LockChange[] = []; const keys = new Set([...Object.keys(base.dependencies), ...Object.keys(head.dependencies)]); for (const key of [...keys].sort()) { const oldDep = base.dependencies[key]; const newDep = head.dependencies[key]; if (!oldDep && newDep) { changes.push({ key, kind: "added", after: short(newDep.commit) }); } else if (oldDep && !newDep) { changes.push({ key, kind: "removed", before: short(oldDep.commit) }); } else if (oldDep && newDep) { // Different repository ID behind the same name: this is the dangerous case. if (oldDep.owner_id !== newDep.owner_id || oldDep.repo_id !== newDep.repo_id) { changes.push({ key, kind: "identity", before: `${oldDep.owner_id}/${oldDep.repo_id}`, after: `${newDep.owner_id}/${newDep.repo_id}`, }); } else if (oldDep.commit !== newDep.commit) { changes.push({ key, kind: "commit", before: short(oldDep.commit), after: short(newDep.commit) }); } } } return changes; } const labels: Record<ChangeKind, string> = { added: "new", removed: "removed", commit: "different commit", identity: "DIFFERENT REPOSITORY", }; // Builds a Markdown report you can use as a PR comment or job summary. export function toMarkdown(changes: LockChange[]): string { if (changes.length === 0) { return "No changes to pinned actions."; } const rows = changes.map( (c) => `| ${c.key} | ${labels[c.kind]} | ${c.before ?? "–"} | ${c.after ?? "–"} |`, ); return ["| Action | Change | before | after |", "| --- | --- | --- | --- |", ...rows].join("\n"); } The command line connects both. If it finds a change of the repository ID, it exits with code 1 and the check turns red. New or changed commits are only reported, because they are normal with every Dependabot update. // tools/actions-lock-review/src/cli.ts import { readFile } from "node:fs/promises"; import { parseLockfile } from "./lockfile.ts"; import { diffLockfiles, toMarkdown } from "./diff.ts"; const [basePath, headPath] = process.argv.slice(2); if (!basePath || !headPath) { console.error("Usage: node src/cli.ts <base.lock> <head.lock>"); process.exit(2); } // If the file is missing in the target branch, we treat it as empty. const readOrEmpty = (path: string) => readFile(path, "utf8").catch(() => ""); const base = parseLockfile(await readOrEmpty(basePath)); const head = parseLockfile(await readFile(headPath, "utf8")); const changes = diffLockfiles(base, head); console.log(toMarkdown(changes)); const identity = changes.filter((c) => c.kind === "identity"); if (identity.length > 0) { console.error(`Warning: ${identity.length} action(s) point to a different repository. Please review.`); process.exit(1); } Test it The tests use the built-in Node.js test runner. They cover the normal case, a new commit, a foreign repository ID, the missing lockfile and broken entries. // tools/actions-lock-review/test/diff.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { parseLockfile } from "../src/lockfile.ts"; import { diffLockfiles, toMarkdown } from "../src/diff.ts"; const base = parseLockfile(` version: 'v0.0.2' workflows: '.github/workflows/ci.yml': - 'actions/checkout@v7.0.1' - 'bookshop/deploy-action@v2.3.0' dependencies: 'actions/checkout@v7.0.1': ref: 'v7.0.1' commit: 'sha1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' owner_id: 44036562 repo_id: 197814629 'bookshop/deploy-action@v2.3.0': ref: 'v2.3.0' commit: 'sha1-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' owner_id: 1001 repo_id: 2002 `); test("no change yields an empty list", () => { assert.deepEqual(diffLockfiles(base, base), []); assert.match(toMarkdown([]), /No changes/); }); test("new commit is detected as a commit change", () => { const head = structuredClone(base); head.dependencies["bookshop/deploy-action@v2.3.0"].commit = "sha1-cccccccccccccccccccccccccccccccccccccccc"; const changes = diffLockfiles(base, head); assert.equal(changes.length, 1); assert.equal(changes[0].kind, "commit"); assert.equal(changes[0].after, "cccccccccccc"); }); test("different repo_id behind the same name is an identity finding", () => { const head = structuredClone(base); head.dependencies["bookshop/deploy-action@v2.3.0"].repo_id = 9999; const changes = diffLockfiles(base, head); assert.equal(changes[0].kind, "identity"); assert.match(toMarkdown(changes), /DIFFERENT REPOSITORY/); }); test("missing lockfile in target branch: everything is new", () => { const changes = diffLockfiles(parseLockfile(""), base); assert.deepEqual(changes.map((c) => c.kind), ["added", "added"]); }); test("incomplete entries are rejected", () => { assert.throws(() => parseLockfile("dependencies:\n 'x/y@v1':\n ref: 'v1'\n"), /incomplete/); }); npx tsc --noEmit node --test test/*.test.ts node src/cli.ts base.lock ../../.github/workflows/actions.lock Expected result: five green tests and a table like | actions/checkout@v7.0.1 | different commit | aaaaaaaaaaaa | cccccccccccc |. If it says “DIFFERENT REPOSITORY”, look into where the action comes from before you merge anything. Roll out the “Require lockfile” policy Up to this point, the lockfile is voluntary. Repository, organization or enterprise administrators can make it mandatory with the “Require lockfile” policy. It blocks runs of workflows that use actions but are not onboarded to the repository’s lockfile. You find the setting among the rules that govern workflow execution. I find the order GitHub recommends sensible and would adopt it exactly like this: 1. Pilot: Apply the policy only to repositories and workflows that already have a lockfile. 2. Observe: Start in “Evaluate” mode and analyze the results under “Policy insights”. According to the docs, this mode is only available in GitHub Enterprise. 3. Automate: Set up Dependabot, the verify check and the hook so nobody has to run the lock command manually. 4. Enforce: Only then switch to “Active” and include all onboarded repositories. The verify check stays active even after enforcement. The policy stops runs without a valid lockfile, while the check warns the author of a pull request beforehand. Without it, you only find out after the merge that the pipeline is stuck. When a tag moves legitimately If a tag like v7 or a branch like main points to a new commit after a regular release, gh actions-lock --relock brings the pins up to date. If the originally locked commit can no longer be reached upstream, for example after a force push, that fails as well. Only --relock --accept-moved accepts the new commit. This option never belongs in automation, so look at the history of the foreign repository first. # pick up regular releases gh actions-lock --relock # only after a manual review: replace a vanished commit gh actions-lock --relock --accept-moved Common mistakes and how to avoid them | Symptom | Cause | Fix | | --- | --- | --- | | Workflow is skipped during generation | Local action ./… given relative to the workflow folder | Write the path from the repository root and regenerate | | Actions are rejected after the switch | Allowlist contains tags like @v6 | Use a pattern with @* or the full SHA | | Required check hangs on “Expected” | Verify workflow has a paths filter | Remove the filter or do not mark the check as required | | --relock fails | Locked commit was removed upstream | Check the history, then deliberately use --accept-moved | | Rate limit during verify | Anonymous requests to github.com (60 per hour) | Set GH_TOKEN, as in the workflow above | The whole setup costs very little: the verify job is a short run on a standard runner, and in private repositories it counts towards your normal Actions minutes. In terms of security, the basic rule from above applies: the lockfile protects against surprising changes, while reviewing new versions remains human work. My take I consider the GitHub Actions lockfile one of the most useful GitHub features this year, precisely because it is unspectacular. It is the same principle we have long used with npm with a lockfile and a cooldown, finally applied to the pipeline, which often has more permissions than the application itself. Readability and security are no longer played off against each other, and that removes a lot of friction in teams. I would push back on the feeling that this settles the supply chain topic for workflows. If you lock an action without looking at it, you have merely frozen the problem. Keeping the permissions of the GITHUB_TOKEN small, handing out secrets sparingly and choosing third-party actions consciously remain just as important as before. My advice: start in the repository that holds your shared workflows, and give yourself half an hour for the first diff. It rarely takes more. If you want to go one step further after pinning your actions: in GitHub Agentic Workflows: Issue Triage Step by Step, I show how an AI agent in Actions sorts issues with read-only permissions. FAQ Do I still need to pin actions by SHA in the workflow? According to the GitHub docs, SHA pinning in the workflow is no longer necessary once a repository uses the lockfile. You can keep readable tags like @v7.0.1, and the commit lives in the lockfile. Which GitHub plans include workflow lockfiles? According to the changelog from October 28, 2026: Free, Team, Enterprise Cloud and Enterprise Cloud with data residency. The “Evaluate” mode of the mandatory policy is only available in GitHub Enterprise, and the CLI extension does not support GitHub Enterprise Server. Does the lockfile also cover reusable workflows? Only indirectly. The dependencies of a reusable workflow are locked by the lockfile of the repository in which that workflow lives. That is why you should switch central workflow repositories first. May I edit actions.lock by hand? No. If an entry looks wrong, fix the workflow files and regenerate the lockfile with gh actions-lock. Your next step Pick a single repository today, ideally the one with your shared workflows: install the extension, run gh actions-lock, read the diff calmly, check the allowlist, commit. The verify check and Dependabot follow in the second pull request. You can find more posts on secure pipelines in the English blog archive. I am curious: do you already pin your actions by SHA today, or do you rely on tags? And what has kept you from changing that so far? Let me know in the comments. Sources - GitHub Changelog: GitHub Actions workflow lockfiles are generally available (Oct 28, 2026) - GitHub Docs: Workflow dependency lockfiles - GitHub Docs: Locking action dependencies in a repository’s workflows - GitHub Docs: Enforcing lockfiles for action dependencies - github/gh-actions-lock (README, MIT license) - GitHub Docs: Dependabot options reference (cooldown) - Tested with Node.js 22.23, TypeScript 7.0.2 (tsc --noEmit), yaml 2.9.1 and the Node test runner (5 tests). The YAML files were parsed for valid syntax; the workflows themselves and gh actions-lock were not executed. This post was created with AI assistance. --- 👉 Next step: Not sure which AWS service fits your use case? My AWS service finder asks a few questions and points you to the right one. Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de

    3 reactions · 2 comments
  3. Qiita · · @Nekorock

    actions.lock を試した。タグの force push は防げたが、別リポジトリの再利用ワークフローと --relock は素通りした 2026 年 10 月 28 日に GA になった GitHub Actions のロックファイル(actions.lock)を、自分で用意したアクションのタグを実際に force push して試しました。 - ロックしたワークフローは、タグを別のコミットに動かされても、ロックしたコミットのまま動きました。アクションの中で使っているアクションも同じです。 - タグを、ロックしたコミットから辿れない履歴に付け替えられると、ワークフローはエラーで止まり、どちらのコードも動きませんでした。 - 一方で、次の 3 つは素通りしました。 - ロックファイルに載っていない新しいワークフローは、警告なしで動かされた先のコミットを実行した - 別のリポジトリの再利用ワークフロー(jobs.<id>.uses)は、ロック済みのワークフローから呼んでも、動かされた先を実行した - gh actions-lock --relock は「タグが動いた」と警告を出したうえで、動かされた先のコミットにロックを進め、終了コード 0 で終わった actions.lock を入れようとしている人と、入れたあとの更新や CI での確認の手順を決めたい人向けです。試した手順、結果、入れるときにやることをまとめました。 actions.lock の紹介と導入手順は GitHub Actions で checkout@v7 のようなバージョン指定を書くのはやめよう!時代は actions.lock! に分かりやすくまとまっています。この記事は、その先の「実際に攻撃されたときに何が起きるか」を確かめたものです。 対象と前提 - gh actions-lock 拡張 v0.4.0、ロックファイルの形式 v0.0.3 - 2026 年 10 月 31 日、GitHub.com の Free プラン、ubuntu-24.04 のランナー - gh actions-lock は手元では動かさず、使い捨てのランナーの上で動かしました - 試したリポジトリは公開しているので、実行のログもそのまま見られます - アクションの側(タグを force push する): OTA-EITA/actions-lock-lab-action - 使う側(ロックファイルを置く): OTA-EITA/actions-lock-lab 背景には、2026 年 3 月 19 日に aquasecurity/trivy-action の 77 個のバージョンのタグのうち 76 個が、認証情報を盗むコードに force push で書き換えられた事件があります(GHSA-69fq-xp46-6x23)。タグで指定していたワークフローは、約 12 時間、書き換えられたコードを動かしていました。 試したこと アクションの側は、OUTER_MARKER を出す composite アクションです。中で同じリポジトリの inner@v1 を呼び、こちらは INNER_MARKER を出します。どのコミットが動いたかは、この 2 つの値で分かります。 runs: using: composite steps: - shell: bash run: echo "OUTER_MARKER=A" - uses: OTA-EITA/actions-lock-lab-action/inner@v1 使う側は、ただこのアクションを呼ぶだけのワークフローです。 on: workflow_dispatch permissions: {} jobs: run: runs-on: ubuntu-24.04 steps: - uses: OTA-EITA/actions-lock-lab-action@v1 手順は次のとおりです。 1. 版 A(289fb71)に v1 のタグを付ける 2. 使う側で gh actions-lock を実行し、できたロックファイルを commit する 3. 版 B(875663c、A の上に積んだコミット)を作り、v1 を force push で B に動かす 4. ワークフローを動かし、どのコミットが動いたかを見る ロックファイルはこうなりました(一部)。 version: 'v0.0.3' workflows: '.github/workflows/use-action.yml': - 'ota-eita/actions-lock-lab-action@v1' dependencies: 'ota-eita/actions-lock-lab-action@v1': ref: 'v1' commit: 'sha1-289fb71b6ad20503e1a75480232a9ea0277cd855' 結果 | # | 試したこと | 動いたコミット | 結果 | |---|---|---|---| | E2 | v1 を B に動かして、ロック済みのワークフローを実行 | A | 守られた。中の inner@v1 も A | | E2 | 同じリポジトリの再利用ワークフロー(./.github/workflows/…)経由 | A | 守られた | | E9 | v1 を A から辿れない別の履歴 C に動かして実行 | なし | エラーで止まった | | E3 | ロックファイルにない新しいワークフローで @v1 を使う | B | 素通り。警告もなし | | E8 | 別リポジトリの再利用ワークフロー …/remote.yml@v2 を、v2 を動かしてから呼ぶ | B | 素通り。ロックファイルにも載らない | | E4 | ロック済みのワークフローに、ロックしていないアクションを足す | なし | 起動できない(422) | | E5 | gh actions-lock --verify | なし | E3 と E4 を検出して終了コード 1。タグが動いたことは報告しない | | E6 | gh actions-lock(通常の再生成) | なし | 既存の行は A のまま。タグが動いたことは報告しない | | E7 | gh actions-lock --relock | なし | 「タグが動いた」と警告したうえで B に進め、終了コード 0 | タグを動かされても、ロックしたコミットが動いた(E2) v1 が B を指している状態でロック済みのワークフローを動かすと、ログの最初にこう出て、A がダウンロードされました。 Using locked action versions from the workflow's lockfile Download action repository 'OTA-EITA/actions-lock-lab-action@v1' (SHA:289fb71b6ad20503e1a75480232a9ea0277cd855) OUTER_MARKER=A INNER_MARKER=A 中で呼んでいる inner@v1 は、ロックファイルに別の行として載っていませんが、A のコミットのものが動きました。同じリポジトリの再利用ワークフローを経由しても同じです。 辿れない履歴に付け替えられると、止まった(E9) 攻撃者は、元のコミットの上に積むとは限りません。v1 を A と無関係の履歴 C に付け替えると、ロック済みのワークフローは次のエラーで止まりました。 ##[error]Lockfile pin 289fb71b6ad20503e1a75480232a9ea0277cd855 for `ota-eita/actions-lock-lab-action` does not match ref `v1`. Ensure the pinned commit exists on the expected branch or tag. ドキュメントには、ロックしたコミットは記録したタグやブランチから辿れる必要があり、辿れなくなったものは実行時に拒否されると書かれています。そのとおりの動きで、攻撃者のコードも元のコードも動かずに止まります。安全側ですが、上流がまっとうな理由で履歴を書き換えたときも、同じように止まります。 新しいワークフローは、ロックし直すまで守られない(E3) ロックファイルを作ったあとに、同じ @v1 を使うワークフローを 1 本足しました。ロックし直さずに動かすと、警告もなく B が動きました。 Download action repository 'OTA-EITA/actions-lock-lab-action@v1' (SHA:875663c7885e676528d7f888745932fec4d63a0e) OUTER_MARKER=B ロックファイルは「載っているワークフロー」にだけ効きます。ドキュメントにも、ワークフローは 1 本ずつでもまとめてでも onboard できる、とあります。載っていないワークフローは、これまでどおりタグで解決されます。 別リポジトリの再利用ワークフローは、ロックの外(E8) 別リポジトリの再利用ワークフローを jobs.<id>.uses で呼ぶワークフローをロックすると、そのワークフローの行は空([])になり、警告も出ませんでした。v2 を動かしてから実行すると、動かされた先がそのまま動きました。 Uses: OTA-EITA/actions-lock-lab-action/.github/workflows/remote.yml@refs/tags/v2 (6e74cf98180d2b9bba393e9fc1fb4493c5f9f040) REMOTE_MARKER=B ドキュメントにも、ロックファイルは再利用ワークフローの呼び出しを固定しないので、フルのコミット SHA で固定する、と書かれています。 --relock は、動いたタグに警告を出しつつ進める(E7) ロックを更新するための --relock を、v1 が B に動いた状態で実行しました。 warning ref-moved .github/workflows/use-action.yml ref v1 now resolves to 875663c7885e, lockfile pins 289fb71b6ad2 警告は出ますが、ロックファイルの v1 は B(875663c)に書き換わり、終了コードは 0 でした。README には「動いたピンを受け入れる」ための --accept-moved というフラグがありますが、付けなくても進みました。 更新を CI で定期的に回していると、タグを書き換えられた直後の --relock で、攻撃者のコミットをそのまま取り込むことになります。通常の再生成(E6)と --verify(E5)は、タグが動いたことを報告しませんでした。 ロックしていないアクションを足すと、起動できない(E4) ロック済みのワークフローに actions/setup-node@v6 を足して、ロックし直さずに動かそうとすると、起動の時点で弾かれました。 HTTP 422: Invalid Argument - failed to parse workflow: Invalid dependency lockfile ".github/workflows/actions.lock": workflow ".github/workflows/use-action.yml" references actions not present in the lockfile: actions/setup-node@v6 ほかのロック済みのワークフローは、この間も普通に動きました。 そのほかに分かったこと - gh actions-lock は、ロックファイルを作るだけでなく、ワークフローも書き換えます。actions/checkout@v7 は @v7.0.1 に、actions/upload-artifact@v7 は @v7.0.2 に変わりました。v1 のように semver でないタグは書き換えず、「完全な semver の参照を使う」よう info で勧めてきます。 - container: のイメージ(alpine:3)はロックされず、「digest で固定していない」と warning が出ます。 入れるときにやること - PR で gh actions-lock --verify を動かす。ロックファイルに載っていないワークフロー(E3)と、載っていないアクション(E4)を、マージ前に止められます - --relock の出力の ref-moved を確かめてから commit する。動いたタグは、上流のリリースノートやタグの履歴で、まっとうな更新か確かめます(E7) - 別リポジトリの再利用ワークフローはコミット SHA で固定する。ロックファイルでは守られません(E8) - コンテナのイメージは digest で固定する - ワークフローを足したらロックし直す。--verify を CI に入れておけば、忘れても止まります 確かめていないこと - Dependabot がアクションを更新するときに、タグが動いていたらどうなるか。Changelog には、Dependabot がロックファイルの行も同じ PR で作り直すとあります - Docker のアクション(docker:// や Dockerfile のアクション) - プライベートリポジトリのアクションと、GitHub Enterprise Server(gh actions-lock は対応していません) - ブランチ(@main など)で指定したアクションのロック 参考 - GitHub Actions workflow lockfiles are generally available(GitHub Changelog, 2026-10-28) - Workflow dependency lockfiles(GitHub Docs) - github/gh-actions-lock - GHSA-69fq-xp46-6x23: Trivy ecosystem supply chain was briefly compromised - GitHub Actions で checkout@v7 のようなバージョン指定を書くのはやめよう!時代は actions.lock!(Zenn) - OTA-EITA/actions-lock-lab、OTA-EITA/actions-lock-lab-action

  4. Source: @github.com changelog (Oct 28, 2026), plus the gh-actions-lock README for the flags. github.blog github/gh-actions-lock

  5. こんなのできたんだ github/gh-actions-lock: A gh CLI extension that generates and verifies the GitHub Actions dependency lockfile, pinning every action your workflows use to an exact commit. github/gh-actions-lock

    19 views
  6. extension として install する必要がある github/gh-actions-lock

    1 like · 49 views
  7. YouTube · · YouTube description · Pipeline Security Lab

    GitHub Actions tags move. Lock them with actions.lock Your workflow says uses: acme-lab/deploy-action@v3, but a tag can be moved after you review it, so a later run can execute different code with no change in your repo. Fix: GitHub Actions workflow dependency lockfiles (public preview since 14 Oct 2026, subject to change). Run gh extension install github/gh-actions-lock, then gh actions-lock. It writes .github/workflows/actions.lock with the exact commit for every action, including transitive actions (the actions your actions use), plus the source repo's owner_id and repo_id. Then run gh actions-lock --verify in a pull_request workflow and make it a required check, so a stale lockfile fails the PR. Add actions.lock to CODEOWNERS so someone reviews every lockfile change. Caveat: locking does not make an action safe. Review the action's code before you lock it, and treat a changed commit in actions.lock as new code. Reusable workflows from other repos are covered by their own repo's lockfile. Sources: GitHub Docs, Workflow dependency lockfiles: docs.github.com GitHub Docs, Locking action dependencies in a repository's workflows: docs.github.com GitHub Docs, Enforcing lockfiles for action dependencies: docs.github.com GitHub Changelog, workflow lockfiles in public preview (14 Oct 2026): github.blog github/gh-actions-lock (README and verify-lockfile.yml template): github/gh-actions-lock OWASP CICD-SEC-8, Ungoverned Usage of 3rd Party Services: owasp.org Full OWASP CI/CD Top 10 breakdown: Your CI/CD Pipeline Has 10 Attack Paths (OWASP) youtube.com The narration uses an AI-generated voice. acme-lab, the commit SHAs and the owner/repo IDs shown are fictional. Defensive education only. #Shorts #GitHubActions #DevSecOps #CICDSecurity

    1 like · 1 comment · 107 views
  8. DEV · · @pwd9000

    GitHub Actions Workflow Lockfiles: Pin What Actually Runs --- title: 'GitHub Actions Workflow Lockfiles: Pin What Actually Runs' published: true description: 'Use GitHub Actions workflow lockfiles to pin action dependencies, review updates, and reduce supply-chain risk in CI.' tags: 'githubactions, security, devops, tutorial' cover_image: 'raw.githubusercontent.com' canonical_url: null id: 4927956 series: null date: '2026-10-27T11:59:53Z' --- GitHub Actions Workflow Lockfiles: Pin What Actually Runs A workflow can look unchanged in Git while the code it runs has changed upstream. A reference such as actions/checkout@v4 points to a tag, and tags can move. GitHub Actions workflow dependency lockfiles address that gap by recording the exact commits behind a workflow's action references. GitHub announced workflow lockfiles in public preview on 14 October 2026. They are maintained with a GitHub CLI extension, reviewed like other dependency changes, and can be enforced by repository, organisation, or enterprise policy. This is a practical supply-chain control, not a guarantee that every dependency is safe. What changed? Before lockfiles, teams could pin actions to full commit SHAs directly in workflow YAML. That is still a valid security practice, but it can make files harder to read and does not provide the same built-in inventory of transitive action dependencies. GitHub's new lockfile records dependency references and their resolved commits separately. | Before | With a workflow lockfile | | --- | --- | | A tag or branch is resolved when a job runs. | An onboarded workflow runs the commit recorded in its lockfile. | | Teams maintain full commit SHAs in workflow YAML or rely on tags. | The workflow can keep a readable version reference while the lockfile records the commit. | | Transitive action dependencies are difficult to inventory consistently. | The lockfile records transitive action dependencies, subject to documented scope limits. | | Reviewers may not see that a tag now resolves to different code. | A lockfile change makes the resolved commit visible in a pull request. | The generated file is .github/workflows/actions.lock. It includes the workflow-to-dependency mapping and, for each dependency, the reference, resolved commit, and numeric owner and repository identifiers. GitHub uses those identifiers to check that the repository behind a name is still the repository that was locked. Prerequisites - A repository hosted on GitHub.com or GitHub Enterprise Cloud, including Enterprise Cloud with data residency. GitHub Free, Team, and Enterprise plans are included in the public preview. - Git and an authenticated GitHub CLI installation (gh) on the machine where you will onboard the workflows. - Permission to read the repository and create or update local files. An administrator is needed to configure organisation or enterprise enforcement policies. - Existing GitHub Actions workflows whose uses: references you have reviewed and intend to lock. The extension does not currently support GitHub Enterprise Server. The feature is in public preview and can change. How it works The extension reads workflows under .github/workflows/, resolves supported action references, and writes the generated lockfile. Once a workflow is onboarded, its action dependencies must be represented in the lockfile. At run time, GitHub Actions uses the recorded commit rather than trusting a tag or branch to remain unchanged. The lockfile is maintained by gh actions-lock; do not edit it by hand. Dependabot can update a matching lockfile entry when it updates an action in an already-onboarded workflow. It does not onboard workflows that have no lockfile entries. Step-by-step: onboard a repository Start from the repository root. First make sure the action references point to versions you actually want to use. A lockfile faithfully pins the current resolution; it does not review the action for you. Install the extension: gh extension install github/gh-actions-lock Generate or update the lockfile: gh actions-lock To onboard just one workflow, pass its path: gh actions-lock .github/workflows/ci.yml Review the complete diff before committing it: git status --short git diff -- .github/workflows The extension may rewrite workflow references to more specific versions and will create or update actions.lock. Check that every change is expected, especially changes to a locked commit or to owner_id and repo_id. A changed repository identity is a reason to investigate, not to approve automatically. Before merging, verify the lockfile against the repository: gh actions-lock --verify This performs network checks. The separate --verify-local option is an offline coverage check only; it does not verify dependency provenance. Commit the workflow and lockfile changes together. Then configure Dependabot for GitHub Actions if it is not already enabled, so supported version updates can update the corresponding lockfile entry in the same pull request. Enforcing lockfiles The preview includes a Require lockfile policy for enterprise, organisation, and repository administrators. Use the policy to move from a pilot to broader enforcement. GitHub's Evaluate mode is available to Enterprise customers; consult the current enforcement documentation for the available settings and rollout behaviour. Start with shared workflow repositories and a small set of representative repositories. Resolve local actions, check any actions allowlist, and review the generated changes before enabling enforcement. An allowlist may need to match locked commit SHAs rather than the old tags. Testing and verification Run gh actions-lock --verify before merging a workflow change. It checks that the lockfile matches the workflow dependencies and performs network-backed validation. Add this check to your CI process if your team wants ongoing verification; the extension repository provides a sample workflow to adapt. For ongoing operational assurance, review lockfile diffs as dependency changes. A successful verification does not mean an action's behaviour is harmless; it confirms the lockfile state and provenance checks described by the tool. Troubleshooting - A workflow is skipped because a local action cannot be resolved: local paths such as uses: ./path resolve from the repository root. Correct the path, then rerun the extension. The tool has options to migrate or ignore local actions, but review their consequences first. - A tag moved upstream: inspect the upstream repository history. If the change is legitimate, use gh actions-lock --relock to resolve it again. Do not use --accept-moved in automation; it accepts a new resolution that needs deliberate review. - An action is no longer allowed: after onboarding, the allowlist is checked against locked commit SHAs. Update the policy deliberately rather than loosening it broadly. - Dependabot reports that onboarding is required: Dependabot updates entries for locked workflows, but it does not add lockfile coverage for a workflow automatically. Onboard that workflow with gh actions-lock. Security boundaries and limitations Lockfiles reduce the risk of a tag or branch moving to different code without a corresponding repository change. They do not prove that the locked code is trustworthy. Important boundaries: - Container images are not locked. Use image digests where appropriate and review image updates separately. - A reusable workflow's dependencies are covered by the lockfile in the repository where that reusable workflow is defined. Onboard shared workflow repositories as well as their callers. - Local ./... actions are not followed like remote actions, and can require migration or an explicit ignore option. - A lockfile may need to be regenerated when workflows or uses: dependencies change. - The extension and lockfile format are in public preview and pre-1.0; review release notes before relying on automation or policy behaviour. Production considerations Treat lockfile adoption as dependency governance, not as a one-time pinning exercise. Keep Dependabot enabled, review commit changes in pull requests, test policy enforcement on a representative set of repositories, and assign an owner for shared workflows. If you have repositories on GitHub Enterprise Server, keep using your existing pinning controls until the extension supports that platform. Conclusion Workflow lockfiles make the code selected by a workflow more visible and reproducible, while retaining readable references in the YAML. They can close an important gap in Actions supply-chain hygiene, especially when paired with Dependabot and an enforcement policy. The important caveat is scope: lock what the tool supports, review every update, and keep treating dependencies as code that can carry risk. Sources | Source | Type | Date | What it verifies | | --- | --- | --- | --- | | Workflow dependency lockfiles | GitHub documentation | Checked 27 October 2026 | Lockfile format, dependency coverage, preview status, and runtime behaviour | | Locking action dependencies in a repository's workflows | GitHub documentation | Checked 27 October 2026 | Extension installation, onboarding, verification, Dependabot, and troubleshooting | | GitHub Actions workflow lockfiles in public preview | GitHub Changelog | 14 October 2026 | Announcement, plan availability, and enforcement policy | | gh-actions-lock | Official GitHub repository | Checked 27 October 2026 | CLI modes, limitations, and sample verification workflow | | Enforcing lockfiles for action dependencies | GitHub documentation | Checked 27 October 2026 | Administrative policy and enforcement | _Author_ {% user pwd9000 %} Like, share, follow me on: :octopus: GitHub | :penguin: X | :space_invader: LinkedIn Date: 27-10-2026

    1 reaction
  9. A full-SHA pin on a GitHub Action can still point at a fork-only impostor commit. gh-actions-lock v0.3.0 now rejects those: every locked commit must sit in its tag or branch history. It can't pin docker:// or container: images, so pin those yourself. github.com/github/gh-actions-lock/releases

    18 views
  10. What Are GitHub Actions Lockfiles, and Do They Replace SHA Pinning? Your SDK release workflow runs when someone pushes a version tag. Its steps read actions/checkout@v4, actions/setup-node@v4, and actions/attest-build-provenance@v1. Each @v4 or @v1 is a git ref in another team's repository. The commit it points to can change after your team reviewed the workflow, and nothing in your repository records which commit was reviewed. The usual fix is to pin every action to a full commit SHA, as the Lullabot architecture decision record requires. Teams keep those pins current with Renovate, Dependabot, pinact, or ratchet, and frizbee converts tags to SHAs. That fixes the lines you can see. Andrew Nesbitt pointed out the gap: a SHA pin locks only the outer action, and a helper action it calls by tag can still change. GitHub's workflow dependency lockfile moves pinning into one machine-generated file per repository. For each onboarded workflow, it records the ref, the resolved commit, and the source repository's identity, transitive actions included. For repositories that adopt it, GitHub says SHA pinning in workflow files is no longer necessary. The guarantee stops at known edges: the dependencies inside reusable workflows you call, container images pulled by tag, and the safety of the pinned code. Reviewers still read every lock diff. For one release workflow on github.com, a bounded pilot is a reasonable next step, unless you run GitHub Enterprise Server or depend mostly on reusable workflows you do not control. TL;DR - A lockfile at .github/workflows/actions.lock records each onboarded workflow's dependencies, their commits, and their source repositories' IDs, transitive actions included. - For repositories that adopt a lockfile, GitHub says inline SHA pinning is no longer necessary. Keep SHAs wherever no lockfile reaches. - Generating the file, onboarding a workflow, running --verify in CI, and the Require lockfile policy are four separate controls. Only the policy blocks runs of workflows nobody onboarded. - Your lockfile does not control the dependencies inside a reusable workflow you call, and container images pulled by tag are not locked. - The feature and its pre-1.0 CLI are in preview, and GHES is not supported. Pilot one workflow before you require locking. What changed, and what is still preview On October 14, 2026, GitHub put workflow lockfiles into public preview for GitHub Enterprise Cloud (including data residency), Team, and Free plans. Each of GitHub's lockfile pages, starting with the concept page, says the feature is in public preview and subject to change. The announcement now explicitly lists Team and Free, with an editor's note correcting plan availability. The setup guide still labels the feature GitHub Enterprise Cloud. Use the updated announcement for preview eligibility, and confirm which policy controls are available for your target repository. The gh-actions-lock README says the tool is pre-1.0, that its file format, flags, and behavior may change between releases, and that GitHub Enterprise Server is not supported. The test examples below describe v0.2.0, published October 12, 2026, as tested on October 15, 2026. As of October 23, 2026, the latest published release is v0.3.0, released October 22 at 16:08 UTC. It changes local-action handling, adds container-image warnings, and tightens verification. The historical examples are retained unchanged; we have not rerun them with v0.3.0. What the lockfile records Each repository gets one lockfile at .github/workflows/actions.lock. You can onboard workflows to it individually or all at once. The concept page says the file should never be edited by hand. The gh actions-lock extension generates it, which requires write access. The file has two sections. workflows lists the dependencies each onboarded workflow uses. dependencies holds one entry per dependency, with three kinds of field: - ref is the tag or branch the workflow names. - commit is the code that will run, written sha1-.... - owner_id and repo_id let GitHub confirm at run time that the repository behind the name is still the one you locked. Transitive actions appear as a uses: list under the parent entry, as in the lockfile in the extension's own repository. That file has no integrity or content hash field, only the commit (our reading; GitHub does not state it). Once a workflow is onboarded, every action it uses must be in the lockfile. The README describes job-start checks of source-repository identity, the pinned commit, and full-version tags. The released CLI distinguishes a rename or transfer that preserves the repository ID, which produces a warning, from a different repository behind the name, which produces a blocking finding. Its v0.2.0 README says a failed check blocks the run. This describes the documented checks; malformed files and CLI/runtime differences need the separate caveats below. A test run on one release workflow We onboarded the release workflow from the opening in a test run with gh-actions-lock v0.2.0 (gh 2.102.0, Linux). We ran the release binary directly. We did not exercise hosted run-time enforcement, the Require lockfile policy, or Dependabot. The original release.yml: name: release on: push: tags: ['v*'] permissions: contents: read id-token: write attestations: write jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci && npm pack - uses: actions/attest-build-provenance@v1 with: subject-path: '*.tgz' GitHub's how-to installs the extension with gh extension install github/gh-actions-lock. Onboarding one workflow takes its path: gh actions-lock .github/workflows/release.yml It exited 0 and printed: Scanning 1 workflow Planning pins Pinned 3 actions (+ 2 transitive) across 1 workflow actions/checkout@v4.4.0 (11d5960) └─ .github/workflows/release.yml actions/setup-node@v4.4.0 (49933ea) └─ .github/workflows/release.yml actions/attest-build-provenance@v1.4.4 (ef24412) └─ .github/workflows/release.yml Transitive dependencies (from composite actions): actions/attest-build-provenance@36fa7d0 via actions/attest-build-provenance@v1.4.4 actions/attest@67422f5 via actions/attest-build-provenance@v1.4.4 It also rewrote the workflow: @v4 became @v4.4.0, and @v1 became @v1.4.4. The two transitive entries come from inside the composite actions/attest-build-provenance; no workflow line names them. The excerpt below is machine-generated output from the test run, copied verbatim. Each # … marks lines we cut. # This file is machine-generated by `gh actions-lock`. # Do not edit by hand; run `gh actions-lock` to update. # Docs: https://gh.io/actions-lockfile version: 'v0.0.3' workflows: '.github/workflows/release.yml': - 'actions/attest-build-provenance@v1.4.4' - 'actions/checkout@v4.4.0' - 'actions/setup-node@v4.4.0' dependencies: # … 'actions/attest-build-provenance@v1.4.4': ref: 'v1.4.4' commit: 'sha1-ef244123eb79f2f7a7e75d99086184180e6d0018' owner_id: 44036562 repo_id: 760702757 uses: - 'actions/attest-build-provenance@36fa7d009e22618ca7cd599486979b8150596c74' - 'actions/attest@67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d' 'actions/attest@67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d': ref: 'v1.4.1' commit: 'sha1-67422f5511b7ff725f4dbd6fb9bd2cd925c65a8d' owner_id: 44036562 repo_id: 760701061 # … With the lockfile committed, gh actions-lock --verify printed "All 1 workflow valid" and exited 0. We then appended - uses: actions/upload-artifact@v4 without relocking, and --verify exited 1: Scanning 1 workflow 1 of 1 workflow failed ✗ Not pinned actions/upload-artifact@v4 used in workflow but not pinned in lockfile (actions/upload-artifact@v4) see: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions ↳ .github/workflows/release.yml Re-run without --verify to apply fixes. The offline --verify-local also exited 1, with "1 of 1 workflow failed: 1 not-pinned." Its flag help says it only checks that every action has an entry, not that the entries are trustworthy. That makes it a fit for a pre-commit hook, while GitHub's docs use --verify in CI. Direct, transitive, and reusable workflows: where the boundary sits The concept page draws the boundary at the repository: its lockfile covers its onboarded workflows and transitive actions, but the actions inside a called reusable workflow belong to the defining repository's lockfile. Your release workflow's direct steps land in your file, and so do the actions inside its composite actions. The dependencies inside a reusable workflow it calls do not. The lockfile of the repository that defines that reusable workflow covers them, if that repository has one. Our test matched the dependency boundary but recorded less than the concept page describes. That page says the lockfile pins the reusable workflows your workflows depend on, too. We onboarded a workflow whose only dependency was a call to slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0. The tool reported "All 1 workflow valid" and left the file unchanged, and the caller's lockfile recorded '.github/workflows/release.yml': [], with no entry for the call. The how-to recommends onboarding repositories that hold shared workflows first. In v0.2.0, resolvable same-repository local uses: ./… actions are rewritten to uses: $/…, which always resolves to the running commit, though that release's README also lists local-path actions as a short-term gap. The v0.3.0 README says local ./ actions are never followed or locked. A local action that uses remote actions now blocks fix and verify by default: explicitly migrate it with --migrate-local-actions, or accept warning-only treatment with --ignore-local-actions. The latter leaves that local-action dependency path unlocked. The linked concept and setup pages do not explain container-image coverage. The CLI source warns that a docker://image:tag step falls outside lockfile coverage because image tags can move. Do lockfiles replace inline SHA pinning? For repositories that adopt one, GitHub says yes. The concept page says SHA pinning in workflow files "is no longer necessary." The secure use reference keeps full-length SHA pinning as the guidance for repositories that cannot adopt a lockfile. The condition applies per workflow. Keep full-length SHA pins on workflows you have not onboarded and on remote job-level reusable-workflow calls in v0.2.0, even when the callee repository has its own lockfile. That callee lockfile governs the actions inside the called workflow, not the caller's job-level ref. A GitHub maintainer confirms that job-level calls are not pinned. Keep separate image-digest pins on docker:// dependencies. The SHA policy keeps working. GitHub's page on governing workflow execution says you can leave "Require actions to be pinned to a full-length commit SHA" enabled. For locked dependencies, GitHub checks the locked commit. During a staged rollout, the policy still enforces SHA pinning for workflows that have not adopted the lockfile. Onboarding does change existing pins. In our test, actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 became actions/checkout@v4.4.0 on a default run, with the commit moved into the lockfile. With --no-narrow, the SHA stayed in the workflow file, which is the fix a maintainer gave in issue #130. If your organization allows only selected actions, the how-to warns that allowlist entries pinned to a tag stop matching. Use a wildcard or the full commit SHA instead. Inline SHA pinning and a lockfile, side by side | Criterion | Inline full-length SHA pinning | Lockfile (onboarded workflow) | | --- | --- | --- | | Where the pin lives | Each uses: line in each workflow file | One actions.lock per repository; workflow lines carry a version tag unless you use --no-narrow | | Transitive actions | Whatever the composite action's author wrote | Recorded under the parent entry, except inside reusable workflow calls | | Repository identity | Not recorded | owner_id and repo_id, checked at job start | | What a reviewer sees | A changed SHA on each edited line | One diff with ref, commit, IDs, and transitive edges | | Enforcement | Full-length SHA policy | Job-start checks, --verify as a required check, Require lockfile policy, SHA policy on the locked commit | | Update automation | Renovate, Dependabot, pinact, or ratchet | Dependabot regenerates lock entries; Renovate documents experimental regeneration of an existing lockfile. Neither onboards new workflows | | Container images by tag | Not covered by action pins | Not locked | | Availability | GitHub's guidance for repositories that cannot adopt a lockfile | Public preview on Enterprise Cloud (including data residency), Team, and Free per the updated announcement; no GHES. Confirm target-repository policy availability | File, onboarding, CI check, and policy are four different controls A lockfile in the repository does not prove that every workflow is governed. | Control | What it does | Availability | On failure | | --- | --- | --- | --- | | Generating the file | Pins each resolvable action and writes the lockfile | People with write access, through the gh extension | All or nothing: if it hits an error it cannot fix, it changes no workflow and no lockfile entry | | Onboarding a workflow | Adds it to workflows; every dependency must then be locked | Individually or all at once | Documented job-start checks block failures; malformed-file fail-closed behavior requires the Require lockfile policy. We did not test hosted enforcement | | --verify in CI | Read-only CLI validation for coverage and pin checks; use it as a required check, not as proof of identical hosted enforcement | Any CI job that runs the CLI | Exits 1 (our test); blocks merges only as a required check | | Require lockfile policy | Blocks runs of targeted workflows that use actions but are not onboarded | Public repositories, and private repositories on Team or Enterprise; Evaluate mode only on GitHub Enterprise | Blocks runs in Active mode; Evaluate reports to Policy insights; we did not test this | Until the policy targets it, a workflow nobody onboarded runs with whatever refs its file contains. The enforcement guide covers both the policy rollout and the verify job. The workflow-policy page lists public repositories and private repositories on Team or Enterprise; private Free repositories are not listed. Malformed lockfiles are a separate case. A GitHub maintainer says the Require lockfile policy supplies fail-closed behavior when the file cannot be reliably read. Onboarding and a CLI check alone are not equivalent to an active runtime policy. CLI/runtime parity also needs a version boundary. In issue #174, the maintainer reported a backend bypass repair on October 16. On October 21, the maintainer said the CLI fix was on main and planned for the next release. The historical v0.2.0 verification code has the full-SHA ancestry-check limitation. That repair has now shipped: v0.3.0 rejects fork-only impostor commits and checks locked commits against the history of their recorded tag or branch. Its release notes also describe incomplete-lockfile failures for older lock graphs, exact-tag/commit mismatch errors, failures when GitHub cannot answer a pin check, and refusal to write cyclic or incomplete lock graphs. Follow the release-specific repair instructions and review the resulting diff before adopting it. We checked the release notes and v0.3.0 verification source; we did not reproduce hosted enforcement or rerun the CLI with this version. What a reviewer approves on a lock update The enforcement guide sets the condition: "A lockfile is only a security control if someone reads the diff." On each lock pull request, check: - Each changed commit, which is the code your release will run. The how-to treats it like any dependency version bump. - Each changed owner_id or repo_id, which the how-to calls rare and worth investigating before you merge. - New entries in a uses: list, which are transitive actions nobody typed into a workflow. - The workflows section, to see whether a workflow dropped out. A normal run trusts refs such as main or v3 from the lockfile and does not re-resolve them. If upstream force-pushed or deleted a locked commit, --relock errors. Accepting the new commit takes gh actions-lock --relock --accept-moved, and that change deserves its own review. For onboarded workflows, Dependabot regenerates the matching lockfile entry in the same pull request as the version update. It does not onboard workflows; it reports that onboarding is required. Beyond having Dependabot manage GitHub Actions dependencies, no configuration is needed. GitHub notes that this maintains coverage rather than extending it. Renovate also documents experimental regeneration of an existing actions.lock after action updates; workflows not yet onboarded are skipped. Neither tool onboards new workflows automatically. The cited guides do not specify a minimum Dependabot version. The case for staying with SHA pins and Dependabot The objection: "We already pin to SHAs and have Dependabot; this is a preview that adds another file and another failure mode." SHA pins plus Dependabot already fix and update every direct reference. The preview adds a pre-1.0 CLI, workflow rewrites, allowlist changes, CodeQL alerts, and early policy bugs. In the proposal thread, one commenter wrote in May 2026 that they would not use it instead of SHA pinning until it worked with Dependabot or Renovate. For direct references the objection holds, because an inline SHA and a lock entry fix the same commit. Our inference from the documented behavior is that lockfiles address three gaps left by inline SHAs, within the coverage and enforcement limits above: - Transitive actions. - Repository-name reuse that points to a different repository identity. - A run-time check that covered dependencies are locked, with the active Require lockfile policy supplying fail-closed behavior for malformed files. Dependabot now regenerates lock entries, and Renovate documents experimental regeneration for existing lockfiles, which addresses the update-integration objection. --no-narrow keeps SHAs in the workflow file. The pre-1.0 CLI, allowlist changes, and CodeQL alerts still argue for testing on one workflow first. Early-adopter reports These come from the issue tracker, not from GitHub's documented behavior. - Issue #130 (closed): fix mode rewrote commit pins into tags. The maintainer pointed to --no-narrow. - Issue #131 (open): after a lockfile rewrite, CodeQL flags the new tag references as unpinned. - Issue #168 (closed October 21): the report describes how a locked Docker container action, or a docker:// step inside a locked composite action, still pulls the image its action.yml names, usually by tag. Moving that tag changes the image without a lockfile diff. v0.3.0 now emits non-blocking container-image warnings for docker:// steps, job containers and services, and remote Docker actions when images lack a SHA-256 digest, including expression-selected images. Those warnings do not pin images. The no-warning observation is the issue author's historical report, not a description of the latest release. - Issue #169 (closed): with the SHA-pinning policy on, version-tagged actions run through a reusable workflow were rejected despite a valid lockfile. A GitHub maintainer said a backend fix should address it, and the issue is closed. What a lockfile does not establish The changelog says locking "does not determine whether the dependency itself is safe," so review dependencies before you lock them. The secure use reference still says to audit the source code of every action you use. The remaining boundaries combine the CLI's documented limits with our inference about what a commit lock can govern. The lock records commits without a content hash, while the earlier third-party gh-actions-lockfile records integrity hashes. Code an action downloads at run time is outside the lock, a point raised in the proposal thread. So are images pulled by tag, the runner image, workflow permissions, and secret handling. Checklist: pilot one SDK or API release workflow 1. Inventory every uses: line: direct and composite actions, reusable workflows and their repositories, local ./ actions, and image-based actions. 2. Onboard the repositories that hold reusable workflows you control first. Keep the caller's remote job-level reusable-workflow refs pinned to full-length SHAs even when those repositories have lockfiles. Record repositories you do not control as out of scope. 3. Onboard only the release workflow. Add --no-narrow if you want SHAs kept in the file. We expect that to make the pilot easier to back out, but we did not test a rollback. 4. Review every commit, owner_id, repo_id, and transitive entry in the generated lockfile. 5. Move tag-based allowlist entries to a wildcard or full SHA, and decide how to triage CodeQL alerts. 6. Require CODEOWNERS review of .github/workflows/actions.lock, with a named owner. 7. Make a gh actions-lock --verify CI job a required check. For pre-commit, the enforcement guide uses gh actions-lock --verify-local --no-fix. 8. Keep Dependabot on GitHub Actions dependencies, and keep the full-length SHA policy enabled. 9. On GitHub Enterprise, run the Require lockfile policy in Evaluate mode, scoped to this workflow. GitHub advises switching to Active only after you have dependable lockfile-maintenance processes for every targeted workflow. 10. Exit test over the next few releases: each release ran onboarded, --verify failed only on real drift, every lock diff had a named reviewer, and a Dependabot update arrived with its lock entry. Defer the pilot if any of these apply: - You run GitHub Enterprise Server. - You depend mostly on reusable workflows you do not control. - You rely on Docker-image-based actions that you need pinned. - Nobody owns lock diffs. If you defer, keep SHA pins with Dependabot and revisit after the CLI reaches 1.0. To start, follow GitHub's guide to locking action dependencies in a repository's workflows, then the guide to enforcing lockfiles when you are ready for the policy. FAQ Will a lockfile break my existing SHA-pinned actions? The commit you pinned stays. A default run rewrites the line to its version tag and moves the commit into the lockfile; --no-narrow keeps the SHA in the workflow. Tag-based allowlist entries stop matching, and CodeQL may flag the rewritten tags. Can I keep the full-length commit SHA policy enabled? Yes. For locked dependencies, GitHub evaluates the policy against the locked commit. Workflows that are not onboarded still need full-length SHAs in the workflow file. Will Dependabot continue to update my actions? For onboarded workflows, Dependabot regenerates the matching lockfile entry in its version-update pull request. It does not onboard new workflows. Does my lockfile cover the reusable workflows I call? Not their dependencies. The actions inside a reusable workflow are covered by the lockfile of the repository where it is defined, so GitHub recommends onboarding shared-workflow repositories first. GitHub's concept page says reusable workflows themselves are pinned, but in our v0.2.0 test the caller's lockfile recorded no entry for the call. Keep remote job-level calls SHA-pinned even when the called repository has its own lockfile.

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

No packages found.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
github/gh-actions-lock mentions in the last 30 days