gh-actions-lock で GitHub Actions のバージョンをコミットに固定する
背景
ワークフローの uses: actions/checkout@v7 のタグは、付け替えられます。
作者がタグを別のコミットに動かすと、ワークフローを変えていなくても、次の実行から別のコードが動きます。
コミットの SHA で指定すれば防げます。 ただ SHA だけが並ぶと、どのバージョンか読めません。 更新のたびに SHA を調べて書き換える手間もあります。
この手間を減らすツールに pinact があります。 GitHub も、ロックファイルの仕様 actions-lockfile を、公開プレビューで出しています。 ロックファイルを作って検証する gh 拡張が gh-actions-lock です。
ここでは gh-actions-lock を使ってみた記録です。
ロックファイルを生成する
拡張を入れて、リポジトリのルートで gh actions-lock を実行します。
.github/workflows/ のワークフローを走査して、アクションをコミットに解決します。
gh extension install github/gh-actions-lock
対象のワークフローは次のとおりです。
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: npm
$ gh actions-lock
Scanning 1 workflow
Planning pins
Pinned 2 actions across 1 workflow
actions/checkout@v7.0.1 (3d3c42e)
└─ .github/workflows/test.yml
actions/setup-node@v7.1.0 (949feb2)
└─ .github/workflows/test.yml
test.yml の uses が完全なバージョンになり、先頭に管理コメントが入ります。
+# This workflow is managed by gh actions-lock.
name: test
@@
- - uses: actions/checkout@v7
- - uses: actions/setup-node@v7
+ - uses: actions/checkout@v7.0.1
+ - uses: actions/setup-node@v7.1.0
.github/workflows/actions.lock もできます。
# 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.2'
workflows:
'.github/workflows/test.yml':
- 'actions/checkout@v7.0.1'
- 'actions/setup-node@v7.1.0'
dependencies:
'actions/checkout@v7.0.1':
ref: 'v7.0.1'
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'actions/setup-node@v7.1.0':
ref: 'v7.1.0'
commit: 'sha1-949feb2413d6458794dcd2491c4babbbce0c15c1'
owner_id: 44036562
repo_id: 189476904
workflows には、ワークフローが使うアクションが並びます。
dependencies には、タグが指すコミットの SHA と、所有者とリポジトリの ID が入ります。
ID を持つのは、同じ名前で作り直された別のリポジトリに気付くためでしょう。
actions-lockfile の例は version: v0.0.3 でしたが、生成されたのは v0.0.2 でした。
仕様はまだ固まっていないようです。
ワークフローにはタグが残る
uses には、タグが残ります。
ロックファイルは、そのタグとコミットの対応を記録します。
キー(actions/checkout@v7.0.1)は uses と一致させます。
@v7 が @v7.0.1 になったのは、ツールが置き換えたからです。
初めてロックするとき、@v7 のような部分的なバージョンは、その範囲で最新の完全なバージョンに置き換わります。
完全なバージョンのタグは、あとから動くことがほとんどありません。
最新バージョンに上げる
ロックしたあとの参照は、ツールが書き換えません。
uses のタグを手で直して、gh actions-lock --relock を実行します。
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.1.0
最新のタグは gh api で調べます。
gh api repos/actions/checkout/releases/latest -q .tag_name
検証のコマンド
--verify は、ワークフローとロックファイルの一致を確かめます。
何も書き換えません。
--verify-local は、オフラインで、各参照にロックファイルの項目があるかだけを見ます。
--relock は、上流で動いた参照を解決し直して、ロックファイルを書き直します。
--no-fix は、書き込まずに状態だけを見ます。
改ざんと不一致の検知
ロックファイルのコミットを 0000000000000000000000000000000000000000 に書き換えて、--verify を実行します。
$ gh actions-lock --verify
Scanning 1 workflow
1 of 1 workflow failed verification
✗ Unreachable pin actions/checkout@v7.0.1
pinned 000000000000 is not an ancestor of 3d3c42e5aac5 — lockfile may have been tampered with
↳ https://github.com/actions/checkout/releases
! investigate immediately — verify the lockfile entry against upstream history
see: how to fix this
↳ .github/workflows/test.yml
ロックファイルのコミットが、タグの指す先の祖先ではないので失敗します。
GitHub 上の履歴を見に行くので、--verify にはネットワークが要ります。
ワークフローのタグだけを v7.0.0 に変えて --verify-local を実行すると、次のようになります。
$ gh actions-lock --verify-local
workflow uses ref "v7.0.0" but lockfile pins "v7.0.1"
! STALE actions/checkout@v7.0.1
lockfile pins actions/checkout@v7.0.1 but no uses: in this workflow references it
1 of 1 workflow failed: 1 ref-changed, 1 stale
Run without --verify-local to resolve and pin missing actions.
タグの食い違いと、使われなくなったロックファイルの項目を、オフラインで見つけます。
CI に検証を足すか
CI で検証すると、ロックファイルのずれを、プルリクエストの段階で見つけられます。
ただ gh actions-lock は拡張なので、CI の step ごとに gh extension install が要ります。
これは新しい外部の依存で、ロックファイルでは固定できません。
公式の CI 向けの検証手段は見つかりませんでした。
私のリポジトリでは、CI に検証を足していません。
ワークフローを変えたら、手元で gh actions-lock を実行して、ロックファイルを同じコミットに入れます。
gh-actions-lock の README によると、ロックの対象のワークフローは、リポジトリ内のローカルのアクション(uses: ./...)を使えません。
まとめ
gh actions-lock を実行すると、uses が完全なバージョンになり、アクションのコミットがロックファイルに残ります。
差分がプルリクエストに出るので、アクションの更新をレビューで確認できます。
最新バージョンへの更新は手動で、CI での検証には拡張のインストールが要ります。
まだ公開プレビューですが、正式にリリースされたら、ぜひ使ってみてください。