はじめに

2026年10月14日、GitHub は GitHub Actions のワークフロー依存 lockfile をパブリックプレビューとして公開しました。GitHub CLI の拡張 gh actions-lock を走らせると、リポジトリに .github/workflows/actions.lock ができます。そこには、ワークフローが uses: で呼んでいるアクションと、そのアクションがさらに呼んでいるアクションまで、実際に走らせるコミット SHA が記録されます。

ワークフローに uses: actions/checkout@v4 と書いたとき、走るのは「v4 というタグがその時点で指しているコミット」です。タグは持ち主があとから別のコミットへ付け替えられます。つまり CI は、自分のリポジトリを 1 行も変えていないのに、他人の判断ひとつで中身が変わりうる状態にあります。

目指すのは、自分のリポジトリの CI を「他人のタグ」から切り離し、自分が確かめたコミットだけが走る状態にすることです。npm の package-lock.json が JavaScript の依存に対してやってきたことを、ようやく CI そのものにも持ち込める。そのための道具が今回の lockfile です。

この記事でわかること:

  • uses: のタグ・ブランチ・SHA は何が違い、なぜタグは「動く」のか
  • 2025年3月の tj-actions/changed-files と reviewdog の事件で、タグの付け替えが何を起こしたか
  • actions.lock に何が書かれ、従来の「SHA を直書きする」対策と何が違うか
  • Free プランの自分のリポジトリで試す手順と、うまくいったかを判断するチェックリスト
🗓️ 2026年10月20日時点の状況

lockfile も gh actions-lock もパブリックプレビューです。GitHub Docs には「public preview and subject to change」、README には「lockfile の形式・コマンドのフラグ・挙動はリリース間で予告なく変わりうる」とあります。拡張の最新版は v0.2.0(2026年10月12日公開) で、このとき lockfile のスキーマが v0.0.3 に上がりました。


🏷️ 1. uses: の参照が「可変」であるとは

GitHub Actions のワークフローは、他人が公開したアクションを uses: <owner>/<repo>@<ref> の形で呼び出します。@ の後ろの <ref> には 3 種類を書けます。

書き方 例 何を指すか 動くか
ブランチ @main そのブランチの最新コミット push のたびに動く
タグ @v4/@v4.2.1 タグが付いているコミット 持ち主が付け替えれば動く
コミット SHA @<40 桁の SHA> そのコミットそのもの 動かない

Git のタグは、コミットに貼った名札です。リリースのたびに v4 という名札を最新の v4.x.y に貼り替える運用は、Actions では一般的です。利用者はそのおかげで @v4 と書くだけで修正版を受け取れます。便利さと引き換えに、名札を貼り替える権限を持つ人が、利用者全員の CI で走るコードを決められる、ということでもあります。

GitHub Docs はこの点をはっきり書いています。タグやブランチで参照すると、Actions は実行時にそれが指すコミットへ解決する。だから「走るコードが、ワークフローを書いた人がレビューしたコードと同じだとは限らない」。

flowchart TD W["あなたのワークフロー
uses: some/action@v4"] -->|"実行時に解決"| T["タグ v4"] T -->|"9月: レビューした時点"| A["コミット A
(確かめた中身)"] T -.->|"10月: 誰かが付け替え"| B["コミット B
(中身は未確認)"]

図の点線が問題の動きです。タグ v4 の付け先が A から B に変わると、あなたのリポジトリには何の差分も出ないまま、次の実行から B が走ります。


🪝 2. タグすり替え攻撃とは — 2025年3月の 2 つの事件

タグすり替え攻撃は、人気のあるアクションのリポジトリに書き込める権限を奪い、既存のタグを悪意のあるコミットへ付け替える攻撃です。利用者は何も変えていないので、気づくきっかけがありません。

実際に起きた例として、GitHub Advisory Database と CISA(米国サイバーセキュリティ・インフラストラクチャセキュリティ庁)が告知した 2025年3月の事件があります。

tj-actions/changed-files(CVE-2025-30066)

プルリクエストで変わったファイルを列挙する、広く使われていたアクションです。GitHub の勧告 GHSA-mrrh-fwg8-r2c3 によると:

  • 攻撃者は複数のバージョンタグをさかのぼって書き換え、悪意のあるコミットを指させた(勧告は v1.0.0・v35.7.7-sec・v44.5.1 などが同じコミット 0e58ed86… を指したと列挙)
  • そのコードはランナーのプロセスのメモリから秘密情報を探し、base64 で符号化してワークフローのログに出力した。ログが公開されているリポジトリでは、誰でも秘密を読めた
  • 影響は「23,000 を超えるリポジトリ」、期間は 2025年3月14日〜15日。v46.0.1 で修正

reviewdog/action-setup@v1(CVE-2025-30154)

こちらは推移的依存の怖さを示した事件です。勧告 GHSA-qmg3-hpqr-gqvc によると、reviewdog/action-setup@v1 が 2025年3月11日 18:42〜20:31(UTC)に侵害されました。そして、このアクションを内部で @v1 参照している他の reviewdog アクション(action-shellcheck・action-staticcheck・action-typos など)も、利用者側のバージョンやピン留めの方法にかかわらず影響を受けた、と書かれています。CISA は、tj-actions の侵害がこの reviewdog の侵害によって可能になった可能性があると追記しています。

flowchart TD U["あなたのワークフロー
uses: reviewdog/action-shellcheck@SHA"] --> S["action-shellcheck
(SHA で固定済み)"] S -->|"内部で uses: …/action-setup@v1"| V["タグ v1"] V -.->|"付け替え"| M["悪意のあるコミット"]

外側のアクションを SHA で固定しても、そのアクションの中の uses: はあなたの手が届かない場所にあります。従来の SHA 直書きでは推移的依存を守れない。今回の lockfile が「推移的依存まで固定する」と強調する理由はここにあります。

💡 ワード解説:推移的依存(transitive dependency)

自分が直接使っている部品が、さらに内部で使っている部品のこと。Actions では、コンポジットアクション(複数のステップを束ねたアクション)が内部で uses: している別のアクションがこれに当たります。自分のワークフローには現れないので、ふだんは見えません。


🔒 3. lockfile とは — package-lock.json や Cargo.lock と同じ発想

lockfile は、「依存の名前とバージョンの指定」と「実際に使う中身」を分けて記録するファイルです。人が書く側(package.json の ^1.2.0 のような幅のある指定)は読みやすいまま残し、機械が書く側に「今回は厳密にこれを使った」を記録します。これで、いつ誰がビルドしても同じ中身になります。

人が書く指定 lockfile lockfile に記録されるもの
npm package.json(^1.2.0) package-lock.json 解決した版・取得元・ハッシュ
Rust Cargo.toml("1.2") Cargo.lock 解決した版・取得元・チェックサム
GitHub Actions ワークフローの uses:(@v4) .github/workflows/actions.lock 参照・コミット SHA・リポジトリの数値 ID

どれも推移的依存まで記録するのが要点です。npm でも、自分が直接入れたパッケージだけでなく、その依存の依存まで lockfile に並びます。Actions の lockfile も同じで、gh actions-lock はコンポジットアクションの中をたどって、内部の uses: まで固定します。

ただし、lockfile が言えるのは「中身が変わっていないこと」だけです。GitHub の changelog も「固定することは、その依存自体が安全かどうかを決めない。固定する前に依存をレビューすること」と書いています。最初に固定したコミットが既に悪意のあるものなら、それを忠実に再現してしまう点は、どの言語の lockfile でも変わりません。


📢 4. 発表の中身 — 何が変わったのか

対象プラン

changelog の本文は、対象を GitHub Enterprise Cloud・データレジデンシー付き Enterprise Cloud・Team・Free としています。公開当日(10月14日)に Editor’s note で「このプレビューを使えるプランを更新した」と追記されており、個人の Free プランも対象です。一方 README には「GitHub Enterprise Server(GHES)はサポートしない」とあります。

生成されるファイルの形

lockfile は .github/workflows/actions.lock に置かれ、workflows と dependencies の 2 つの部分からなります。GitHub Docs の例(version は 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
フィールド 意味
workflows 取り込み済みのワークフローごとに、使っている依存の一覧
ref ワークフローが書いている参照(タグやブランチ)
commit その参照が解決したコミット。実際に走るのはこれ
owner_id・repo_id 取得元リポジトリの数値 ID。名前の裏のリポジトリが固定時と同じかを実行時に確かめる

推移的依存は、dependencies の各エントリの下に uses: として並びます。gh-actions-lock 自身のリポジトリにある lockfile では、たとえば actions/attest-build-provenance@v1 のエントリの下に、それが内部で呼んでいる 2 つのアクションが SHA 付きで記録されています。

owner_id・repo_id を持っているのが、Actions ならではの工夫です。リポジトリ名は、改名や譲渡のあと別人が同じ名前で作り直すことができます。名前だけを記録していると、その「なりすまし」を見分けられません。数値 ID で照合すれば、名前が同じでも中身が別のリポジトリなら止められます。

実行時に何が強制されるか

lockfile はワークフロー単位で「取り込む(onboard)」方式です。取り込まれたワークフローについては、GitHub Docs と changelog によると:

  • 使っているアクションの依存は、すべて lockfile に載っていなければならない
  • 実行されるのは lockfile に記録されたコミット。タグやブランチが動いても、記録したコミットが走る
  • 記録されたコミットは、取得元リポジトリのどこかのブランチに含まれている必要がある。README はこれを「impostor commit(なりすましコミット)型の攻撃を難しくするため」と説明している
  • 許可するアクションを絞る設定(allowlist)も、タグではなく記録した SHA で照合される

再利用ワークフローの扱い

changelog は「再利用ワークフローの依存も固定する」と書いていますが、Docs はその範囲を正確に区切っています。lockfile は自分のリポジトリのワークフローと、アクションの推移的依存をカバーします。ただし、呼び出した再利用ワークフローの中身の依存は、その再利用ワークフローが定義されているリポジトリの lockfile がカバーする。共通ワークフローを持つ組織は、そちらのリポジトリを先に取り込むよう Docs は勧めています。

SHA 直書きとの関係

GitHub はこれまで、外部アクションを 40 桁のコミット SHA で直書きすることを勧めてきました。Docs は、lockfile を採用したリポジトリではワークフロー内での SHA 直書きは「もはや必要ない(no longer necessary)」とし、lockfile の方が良い理由を 3 つ挙げています。推移的依存を扱える・ワークフローが読みやすいまま・取得元リポジトリの ID を記録できる、です。

管理者向けの「Require lockfile」ポリシー

Enterprise・Organization・リポジトリの管理者は、Require lockfile ポリシー(これもパブリックプレビュー)で、lockfile に取り込まれていないワークフローの実行を止められます。いきなり全体で有効にすると準備のできていないリポジトリの CI が止まるため、Docs は対象を絞った試行から始める手順を示しています。結果だけを記録する Evaluate モードは GitHub Enterprise Cloud だけで使えます。

Dependabot との連携

Dependabot がアクションのバージョン更新のプルリクエストを出すとき、そのワークフローが取り込み済みなら、同じプルリクエストの中で lockfile の該当エントリも作り直します。追加の設定はいりません。ただし、まだ lockfile に載っていないワークフローを Dependabot が取り込むことはなく、その場合は「取り込みが必要」と報告するだけです。

⚠️ プレビューゆえの制限
  • ローカルパスのアクション(uses: ./…)を使うワークフローは取り込めない、と README の Limitations にある(「短期的なギャップ」)。同じリポジトリのアクションは、実行中のコミットに解決される新しい書き方 uses: $/… に書き換えて扱う
  • v0.2.0 で挙動が変わった: --verify が厳しくなり、どのワークフローも使っていないエントリや、エントリのないワークフローでも失敗するようになった。--rescan は廃止
  • 仕様はリリース間で予告なく変わりうる。チームで使うなら拡張の版をそろえる

🛠️ 5. 自分のリポジトリで試す手順

コマンドは GitHub Docs と README のとおりです。GitHub CLI(gh)が入っていて、gh auth login 済みであることが前提です。

① 拡張を入れる

gh extension install github/gh-actions-lock

あとで更新するときは gh extension upgrade gh-actions-lock です。

② 参照を確かめる

lockfile は今のタグやブランチが指すコミットを固定します。古い版や、確かめていない版を固定しないように、先に各 uses: が使いたい版を指しているかを見直します。Docs は、Dependabot をまだ有効にしていなければ、lockfile を作る前に有効にすることを勧めています。

③ lockfile を生成する

リポジトリのルートで、引数なしで実行します。.github/workflows/ 以下のワークフローをすべて走査し、解決できたアクションをコミット SHA で固定して lockfile を書きます。

gh actions-lock

1 つのワークフローだけを取り込むなら、パスを渡します。

gh actions-lock .github/workflows/ci.yml

途中で直せないエラーが 1 つでも出ると、どのワークフローも lockfile も書き換えない(全部か無しか)のが README の仕様です。

④ 差分を確かめてコミットする

拡張は lockfile 以外にもワークフローを書き換えます。octo-org/deploy-action@v3 のような参照は、解決した具体的な版 @v3.1.0 に書き換わります(指すコミットは同じ)。uses: ./… は uses: $/… に変わります。差分全体を読んでから、lockfile とワークフローを一緒にコミットします。

⑤ その後の運用

# 何も書き換えずに検査する(CI 向け)
gh actions-lock --verify

# 正当に動いたブランチ/部分バージョン(main・v4 など)を新しいコミットへ取り直す
gh actions-lock --relock
  • ワークフローを足したとき、uses: を増やしたり減らしたりしたときは、gh actions-lock をもう一度実行する
  • main や v4 のような参照は、上流が動いても自動では追従しない。追従させたいときは、上流の履歴を確かめてから --relock
  • 固定していたコミットが上流から消えた(force push など)場合、--relock もエラーのまま止まる。受け入れるなら --relock --accept-moved。ここは一番疑うべき場面なので、上流の告知を確かめてから

lockfile の差分を読むときの目安も Docs にあります。commit が変わったら「走るコードが変わった」なので、依存の版上げと同じ目で見る。owner_id や repo_id が変わるのはまれで、名前の裏のリポジトリが固定時と別物になったことを意味するので、マージ前に調べる。


🏠 6. 当サイトの場合 — このブログも Actions でデプロイしている

このブログは、master に push すると GitHub Actions で Hugo のサイトをビルドし、Azure Static Web Apps に配信しています。ワークフローが呼んでいる外部アクションは 2 つです。

steps:
  - uses: actions/checkout@v4
  - uses: Azure/static-web-apps-deploy@v1

どちらもタグ参照です。Azure/static-web-apps-deploy@v1 にはデプロイ用のトークンが渡されているので、もし v1 が付け替えられれば、そのトークンが狙われる構図は tj-actions の事件と同じです。lockfile を入れる価値は十分あります。

💡 lockfile の外に残るもの

Azure/static-web-apps-deploy は Docker コンテナで動くアクションで、その Dockerfile は FROM mcr.microsoft.com/appsvc/staticappsclient:stable と、コンテナイメージをタグで参照しています。Docs が挙げる lockfile の記録項目は ref・commit・owner_id・repo_id で、コンテナイメージは含まれていません。lockfile が固定するのはアクションのリポジトリのコミット(この Dockerfile が書かれた版)までで、stable が指すイメージの中身は別の層の話です。拡張の v0.1.7-rc.1 では「ワークフローが可変な Docker イメージタグを使うと警告する」変更が入っており、この層も意識されています。

この記事を書く段階では、このリポジトリのワークフローには手を入れていません。人が試すときの手順と判定基準を、下にまとめます。

✅ 試すときの手順(このブログのリポジトリ)
  1. 作業用ブランチを切る(master に直接は入れない。master への push がそのままデプロイになるため)
  2. gh extension install github/gh-actions-lock → リポジトリのルートで gh actions-lock
  3. git diff で差分を読む
  4. ブランチを push してプルリクエストを開き、プルリクエストのビルドが通るかを見る(このワークフローは pull_request でもビルドする)
  5. 問題なければマージ。だめならブランチごと捨てれば元どおり
📌 何が出れば OK か — チェックリスト
  • .github/workflows/actions.lock ができ、先頭に version: 'v0.0.3'(v0.2.0 の場合)がある
  • workflows: の下に 自分の workflow のファイル名(azure-static-web-apps-<アプリ名>.yml のような形)があり、2 つのアクションが並んでいる
  • dependencies: の各エントリに commit: 'sha1-…'・owner_id・repo_id がある
  • ワークフローの @v4・@v1 が、具体的な版(@v4.x.y のような形)に書き換わっている。それ以外の行は変わっていない
  • gh actions-lock --verify がエラーなしで終わる
  • プルリクエストのビルドが通り、デプロイのプレビューが従来どおり出る
  • Dependabot を使うなら、次のアクション更新のプルリクエストに actions.lock の変更も含まれている

🔧 技術者の所感 — CI が「依存の 1 つ」として扱われるようになった

ここからは筆者の意見です。

📌 筆者の見方

電子工作で基板を発注するとき、BOM には「抵抗 10kΩ」ではなく、メーカー型番と部品番号まで書きます。同じ「10kΩ」でも、届く部品が変われば基板の振る舞いが変わりうるからです。ソフトウェアの依存では、それを lockfile がやってきました。ところが依存を取ってきてビルドする CI そのものは、長いあいだ @v4 という「抵抗 10kΩ」の書き方のままでした。今回の lockfile は、その最後の穴に型番を書けるようにした、と受け止めています。

特に良いと思ったのは、推移的依存と repo_id まで踏み込んだ点です。reviewdog の事件は、SHA を直書きする真面目な利用者でも防げませんでした。「中の uses: まで固定する」「名前ではなく ID で照合する」は、実際に起きた攻撃の形から逆算された設計に見えます。Dependabot が lockfile を一緒に更新してくれるので、「固定したまま古くなる」という lockfile の定番の弱点にも手当てがあります。

一方で、lockfile は差分を誰かが読んではじめて効く道具です。Docs も「誰かが差分を読まない限り、lockfile はセキュリティ対策にならない」と書き、CODEOWNERS への登録を勧めています。個人のリポジトリでは、Dependabot のプルリクエストを中身を見ずにマージする癖があると、ここが素通りになります。当サイトでも、入れるなら「actions.lock の commit が変わったら上流のリリースノートを 1 回見る」を習慣にするつもりです。


✅ まとめ

  • 2026年10月14日、GitHub Actions の lockfile がパブリックプレビューになった。 対象は Enterprise Cloud・Team・Free。GHES は非対応
  • uses: …@v4 のタグやブランチは実行時に解決されるので、持ち主が付け替えれば走るコードが変わる。 2025年3月の tj-actions/changed-files と reviewdog の事件はこの性質を突いた
  • gh actions-lock が .github/workflows/actions.lock を作り、推移的依存までコミット SHA と取得元の数値 ID で固定する。 取り込んだワークフローは、タグが動いても記録したコミットを走らせる
  • 再利用ワークフローの中身は、それを定義したリポジトリの lockfile がカバーする。 Require lockfile ポリシーで未取り込みのワークフローを止められ、Dependabot は更新時に lockfile も作り直す
  • プレビューなので形式やフラグは変わりうる。 v0.2.0 でスキーマ v0.0.3・--verify の厳格化・--rescan 廃止があった
  • 次の一手は、自分のリポジトリでブランチを切って gh actions-lock を 1 回走らせ、差分を読むこと。 外部アクションが 2 つだけの小さなワークフローなら、数分で全体を見渡せる

よくある質問(FAQ)

Q: 個人の Free プランでも使えますか?

A: 使えます。changelog は対象を GitHub Enterprise Cloud・データレジデンシー付き Enterprise Cloud・Team・Free としています。公開当日に Editor’s note で対象プランが更新されています。GitHub Enterprise Server は README で非対応とされています。

Q: すでにワークフローで SHA を直書きしています。lockfile は必要ですか?

A: 直書きでは、使っているアクションの内部の uses:(推移的依存)は固定できません。2025年3月の reviewdog の事件では、外側をどう固定していても内部の @v1 経由で影響を受けました。GitHub Docs は、lockfile を採用したリポジトリでは直書きは不要としています。また、管理者向けの「フルの SHA でのピン留めを必須にする」設定は、lockfile の対象については記録した SHA で判定されるので、移行中は両方を有効にしておけます。

Q: lockfile を入れると、アクションの修正版を受け取れなくなりますか?

A: 自動では追従しません。Dependabot にアクションの更新を任せれば、版上げのプルリクエストで lockfile も一緒に更新されます。main や v4 のような参照を手動で新しいコミットへ取り直すときは gh actions-lock --relock を使います。

Q: lockfile を手で直してもいいですか?

A: いけません。ファイルの先頭に「手で編集しない」と書かれており、Docs も「おかしければワークフローを直して作り直す」としています。

Q: lockfile があれば、使っているアクションは安全ですか?

A: いいえ。lockfile が守るのは「固定したときから中身が変わっていないこと」で、固定したコードが安全かどうかは判断しません。changelog も、固定する前に依存をレビューするよう書いています。

関連記事

参考