ブログ一覧

GitHub Actions の lockfile は composite action の中の action まで記録する

GitHub から、action とその中の action のコミットを記録する GitHub Actions のワークフローの lockfile が公開プレビューで出ました。私は Dependabot の PR の差分では乗っ取りを見抜けない に書いたとおり、Dependabot の更新を CI 任せにする守りの 1つとして uses: を全部 SHA で固定していたので、lockfile が何を足すのか確かめました。

分かったのは次の 3つです。

  1. uses: を SHA で固定しても、composite action が中で @v4 のようなタグやブランチ名で指定している action は固定されず、lockfile はそのコミットも記録する(古い withastro/action v3 で確かめた)
  2. 中でタグやブランチ名で呼ぶ action を使っていなければ、lockfile に記録される物は SHA の固定と同じで、試した範囲で増えたのは、どのブランチにも無いコミットを lockfile を作るときに拒む確認と、uses: を足すたびに lockfile を作り直す手間(私のワークフロー 5本はこちら)
  3. どちらに当たるかは、gh actions-lock が出す Transitive dependencies の行で @ の後ろがタグやブランチ名かを見れば分かる(gh actions-lock は lockfile を作る GitHub CLI の拡張)

SHA で固定した composite action でも、中でブランチ名で呼ぶ action は書き換えた中身が動く

検証では、中で別の action(inner)をブランチ名で呼ぶ composite action(outer)を作りました。lockfile なしの方は、私のワークフローと同じく outer をコミットの SHA で固定して呼びます。lockfile ありの方は outer をブランチ名で書き、どのコミットで動かすかは lockfile に持たせました。

lockfile は GitHub CLI の拡張 gh actions-lock の v0.2.0 で作っていて、gh actions-lock は SHA で書いた uses: をタグやブランチに書き換えるため、lockfile を入れたワークフローの uses: には SHA が残りません。

lockfile を入れた後に inner と outer の中身を書き換えてブランチを進め、2本を同じ push で走らせました。ログのリポジトリ名とブランチ名は置き換えていて、ほかは実際の出力のままです。lockfile なしの方では、outer は固定した SHA で取ってきましたが、inner はブランチの新しいコミットを取りに行き、書き換えた inner が動きました。

Download action repository 'owner/repo@ed89f1b9e0a740bfa4949e3f2b2b7d62f59d603f' (SHA:ed89f1b9e0a740bfa4949e3f2b2b7d62f59d603f)
Download action repository 'owner/repo@trial-branch' (SHA:52338ac53a6790a093e066ede563a23193ce7b3c)
OUTER
INNER VERSION 2 (branch moved)

lockfile ありの方は、ブランチが進んだ後も lockfile に記録したコミットを取り、inner は書き換える前の中身で動きました。

Using locked action versions from the workflow's lockfile
Download action repository 'owner/repo@trial-branch' (SHA:b725633122563775010e1f974e5c75122ec1b883)
OUTER
INNER VERSION 1

2本とも outer は書き換える前の中身で動き、違ったのは中の inner だけでした。ブランチを進めた後の outer は echo "OUTER VERSION 2" に書き換えてありますが、2本とも OUTER と出ています。lockfile なしで固定した ed89f1b… と lockfile に記録された b725633… は、ワークフローのファイルを足しただけの違いで、outer と inner の中身は同じです。

中の action をタグで呼んでいる composite action は実際にあります。actions/upload-pages-artifact の v1〜v3 は中で actions/upload-artifact を @v3 や @v4 のタグで呼び、Astro のサイトを GitHub Pages に出す withastro/action の v1〜v3 も中の 4つの action をタグで指定しています。

これらの版をコミットの SHA で固定しても中の action はタグのまま動き、どちらも最新のリリース(upload-pages-artifact v5.0.0、withastro/action v6.1.3)では中の参照が SHA です。withastro/action の v3 と upload-pages-artifact の v3 を SHA で固定したワークフローを gh actions-lock にかけると、中でタグで呼ばれている 5個の action も、そのときタグが指していたコミットで lockfile に記録されました。記録したコミットで動くのを確かめたのはブランチを進めた場合だけで、タグを付け替えた後の実行は試していません。

Pinned 2 actions (+ 5 transitive) across 1 workflow
  withastro/action@v3.0.2 (56781b9)
  actions/upload-pages-artifact@v3.0.1 (56afc60)
Transitive dependencies (from composite actions):
  pnpm/action-setup@v4 (b906aff) via withastro/action@v3.0.2
  oven-sh/setup-bun@v2 (0c5077e) via withastro/action@v3.0.2
  actions/setup-node@v4 (49933ea) via withastro/action@v3.0.2
  actions/upload-pages-artifact@v3 (56afc60) via withastro/action@v3.0.2
  actions/upload-artifact@v4 (ea165f8) via actions/upload-pages-artifact@v3.0.1

どのブランチにも無いコミットは SHA で動き、lockfile の作成で拒まれる

lockfile を入れると、どのブランチにも無いコミットを lockfile を作るときに拒む確認が増えます。フォークに積んだコミットは、元のリポジトリの名前と SHA を指定すると呼び出せてしまいます。このようなコミットは impostor commit(なりすましのコミット)と呼ばれます。どのブランチにも含まれないコミットも、リポジトリの名前と SHA で呼び出せます。検証では inner を書き換えたコミットを push した後にブランチを 1つ前に戻して force push し、フォークのコミットと同じく、リポジトリの名前と SHA でだけ呼び出せる状態にしました。lockfile なしでこのコミットを SHA で固定して呼ぶと、ランナーは止めずにそのまま動きました。

Download action repository 'owner/repo@ed1a14ddeae09097a2036aca198e5622f2a48f7f' (SHA:ed1a14ddeae09097a2036aca198e5622f2a48f7f)
INNER EVIL (commit later removed from every branch)

同じワークフローに gh actions-lock をかけると、エラーになって lockfile には何も書きませんでした。

✗ Pinned commit not in upstream history owner/repo@ed1a14ddeae09097a2036aca198e5622f2a48f7f
  commit ed1a14ddeae0 is not on any tag or branch of owner/repo: it may be a fork or impostor commit served under the upstream name
Nothing was written: fix the errors above and re-run

gh actions-lock の README によると、ブランチに含まれるコミットかを確かめるのは lockfile に記録するときで、ランナーがジョブの開始時に確かめるのは次の 3つです。

  • 記録したコミットが今もあること
  • リポジトリの名前の付け替えや移転が無いこと
  • v4.2.1 のような版の番号まで書いたタグが、今もそのコミットを指すこと

拒まれたコミットは lockfile に載らないので、lockfile を入れたワークフローで、ランナーが実行のときにもどのブランチにも無いコミットを止めるかは試していません。

入れると uses はタグに戻り、足すたびに作り直しが要る

私は個人で複数のサイトを運用していて、デプロイやバックアップを GitHub Actions のワークフロー 5本で回しています。この 5本に gh actions-lock をかけると、30か所の uses: がすべて SHA からタグに戻り、コミットの SHA は lockfile の方に記録されました。

-      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - uses: actions/checkout@v7.0.1
-      - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
+      - uses: pnpm/action-setup@v6.1.0

ワークフローの依存関係の lockfile についての GitHub のドキュメントは、lockfile を入れたリポジトリではワークフローの中の SHA の固定は要らず、ワークフローのファイルが読みやすくなると書いています。その代わり、タグに戻した後はワークフローのファイルだけを見てもどのコミットで動くかは分からず、lockfile を見る必要があります。

lockfile に載せたワークフローに、作り直さずに uses: を 1行足すと、ジョブが 1つも作られない startup_failure になり、ワークフローは始まりませんでした(公開プレビューでの結果)。ブランチで呼んでいる action のブランチが新しいコミットに進んだ後に gh actions-lock をかけ直すと、1 action requires investigation — do not auto-pin と出て記録を自動では進めず、何も書きませんでした。README によると、ブランチや v4 のようなメジャー版だけのタグが進んだ後に lockfile の記録を新しいコミットに進めるには、--relock を付けて作り直します。

中の action が固定済みかは Transitive dependencies の @ の後ろで分かる

Web 開発でよく見る action を私が 50個選び、それぞれの最新のリリースの action.yml を読みました。GitHub 公式の actions/*、docker/*、各言語のセットアップ、デプロイやリリースの action から選んだ物で、利用数の順位のような基準で選んだ物ではありません。中で別の action を呼ぶ composite action は次の 6個で、その中の uses: 16か所は 2026-10-14 時点で全部コミットの SHA でした。

  • actions/upload-pages-artifact
  • actions/attest-build-provenance
  • codecov/codecov-action
  • withastro/action
  • anthropics/claude-code-action
  • supabase/setup-cli

このうち最新版の withastro/action v6.1.3 と upload-pages-artifact v5.0.0 を SHA で固定したワークフローも gh actions-lock にかけると、出力は (+ 6 transitive) でしたが、@ の後ろはタグではなくコミットの SHA で、記録されたのは中で固定済みのコミットそのものでした。次のログは Transitive dependencies の 6行のうち 2行だけを載せています。

Pinned 2 actions (+ 6 transitive) across 1 workflow
  withastro/action@v6.1.3 (3eafd00)
  actions/upload-pages-artifact@v5.0.0 (fc324d3)
Transitive dependencies (from composite actions):
  pnpm/action-setup@ea17c68 via withastro/action@v6.1.3
  actions/upload-artifact@bbbca2d via actions/upload-pages-artifact@v5.0.0

残りの 44個は中で別の action を呼ばず、ほとんどが JavaScript の action です。私の 5本が使う 6種類の action も全部 JavaScript の action で中で別の action を呼ばず、lockfile に記録されたのは SHA で固定して指していたのと同じ 6件でした。

このほかに試していない物には次があります。

  1. Dependabot が action を上げる PR で lockfile も直す動き(発表は直すと書いている)
  2. 管理者が lockfile の無いワークフローを動かさないようにする「Require lockfile」の方針
  3. action が実行中に取ってくる npm のパッケージ、バイナリ、Docker のイメージ(ドキュメントによると、lockfile に記録されるのは action のコミットとリポジトリの ID だけ)
  4. 再利用可能なワークフロー(reusable workflow)の中の依存(ドキュメントは、定義したリポジトリの lockfile が持つと書いている)
  5. リポジトリの名前の付け替えや作り直しを、lockfile が記録するリポジトリの ID で見つけられるか

自分のワークフローで、lockfile が中の action のコミットを新しく固定するのか、記録が SHA の固定と同じになるのかは、gh actions-lock をかけて Transitive dependencies の各行の @ の後ろを見れば分かります。@v4 のようなタグやブランチ名なら lockfile で中の action のコミットが新しく固定され、コミットの SHA なら記録は SHA の固定と同じで、行が無ければ中で別の action を呼ぶ物を使っていません。gh actions-lock はかけた時点でワークフローのファイルの uses: も SHA からタグに書き換えるので、確かめるだけなら、書き換わったワークフローのファイルと lockfile のどちらもコミットせずに戻します。

この記事をシェア