GitHub Actionsのactions.lockでタグ指定のまま中身を固定!startup_failureの落とし穴も
🙌 結論から
GitHub Actionsで actions/checkout@v4 のようなタグ指定をしているなら、10月14日にパブリックプレビューになった「ワークフローロックファイル」はかなり試す価値があります。
.github/workflows/actions.lock に「どのタグが、どのコミットだったか」を記録しておくと、あとからタグが別のコミットに付け替えられても、Actionsはロックに書いたコミットを実行してくれます👀
もう当たり前感あるけど、セキュリティの記事だと「全部40桁のSHAでピン留めしよう」とよく言われますよね。
正直、あれは読みにくいし、Dependabotの差分もSHAだらけになるので、私はずっと後回しにしてました・・・(・_・;)
ロックファイルならワークフローの見た目は @v5.1.0 のようなタグのまま、中身だけコミットに固定できます。
ただしまだプレビューで、ロックが古いとジョブが1つも始まらずに startup_failure で止まるので、PRで検証を回す仕組みとセットで入れるのがおすすめです!
💡 タグ指定のままだと何が怖いのか
uses: actions/checkout@v4 の v4 は、ただのタグです。
タグもブランチも、あとから別のコミットに付け替えられます。
つまり、レビューしたときの中身と、来月CIで動く中身が同じとは限らないんですよね。
GitHubの公式チェンジログでも、まさにこの「タグやブランチが動いて、あとの実行で違うコードが走る」ことをロックファイルの目的に挙げています。
このブログを動かしているリポジトリにもワークフローが7本あって、actions/checkout@v4 と actions/setup-node@v4 を全部タグで指定していました。
自分の書いたコードは毎回レビューしているのに、毎朝動くActionsの中身は確認していなかった、ということです(´・ω・`)
もう1つ厄介なのが、使っているアクションがさらに別のアクションを呼んでいるケースです。
ワークフローのSHAを手でピン留めしても、その先で actions/cache@v4 のようにタグで呼ばれていたら、そこは固定されません。
ロックファイルはこの孫の依存(推移的な依存)までまとめてコミットに固定してくれるのが、手でSHAを書く方法との一番大きな違いです✨
🔧 actions.lockを作るまでの手順
ロックファイルは手で書くものではなく、GitHub CLIの拡張 gh actions-lock で作ります。
# 拡張を入れる(gh auth login 済みが前提)
gh extension install github/gh-actions-lock
# リポジトリのルートで実行。.github/workflows/ 以下を全部見てロックを作る
gh actions-lock
# 1本だけ先に試すなら、ワークフローを指定する
gh actions-lock .github/workflows/ci.yml
できあがる actions.lock は、version・workflows・dependencies の3つを持つYAMLです。
公式ドキュメントによると、各依存には ref(タグやブランチ)、commit(実際に動くSHA)、owner_id と repo_id(元のリポジトリの番号)が入ります。
# This file is machine-generated by `gh actions-lock`.
version: 'v0.0.3'
workflows:
'.github/workflows/ci.yml':
- 'actions/checkout@v5.1.0'
dependencies:
'actions/checkout@v5.1.0':
ref: 'v5.1.0'
commit: 'sha1-fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09'
owner_id: 44036562
repo_id: 197814629
(↑ クラスメソッドさんの検証記事に載っている出力を短くした例です)
リポジトリの番号まで持っているので、同じ名前のリポジトリを作り直されても見分けられるのがいいですね。
ここで1つ注意で、ワークフロー側のファイルも書き換わります。
@v5 のような短いタグは @v5.1.0 のような正確なタグに絞り込まれます。
同じリポジトリ内の ./ で始まるローカルアクションは $/ という書き方に変わります。
コミット前に、actions.lock だけでなくワークフローの差分も必ず見たほうがいいです。
短いタグのまま残したいなら --no-narrow を付けます🙌
👀 PRで—verifyを回す私の設定
ロックファイルを入れるなら、PRの段階で古いロックに気づける仕組みを一緒に入れたいです。
gh actions-lock にはファイルを書き換えないモードが2つあります。
| コマンド | ネットワーク | 確認すること | 使いどころ |
|---|---|---|---|
gh actions-lock --verify | 使う | 修正するときと同じチェック(中身と出どころまで) | PRのCI |
gh actions-lock --verify-local | 使わない | 全部の参照にエントリがあるかだけ | コミット前のフック |
私が入れようとしているのは、--verify-local をコミット前のフック、--verify をPRのジョブ、という分け方です。
--verify-local はネットワークを使わないので一瞬で終わりますが、エントリがあるかしか見ないので、PR側の --verify は外せません。
# .github/workflows/verify-lockfile.yml
name: Verify actions lockfile
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
verify:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- run: gh extension install github/gh-actions-lock
env:
GH_TOKEN: ${{ github.token }}
- run: gh actions-lock --verify
env:
GH_TOKEN: ${{ github.token }}
公式リポジトリにも .github/workflows/verify-lockfile.yml のひな形があるので、実際はそれをベースにするのが早いです。
ここでハマりそうなのが、この検証用ワークフロー自体もロックの対象になることです。
追加したら、もう一度 gh actions-lock を実行して、自分自身もロックに入れておきます!
Dependabotを使っている場合は、ロック済みのワークフローならアクションの更新PRの中で actions.lock のエントリも一緒に作り直してくれます。
追加の設定は要らないので、ここはかなりありがたいです(^^)
🤔 startup_failureで止まるパターンと直し方
ここが一番大事なところです。
ロック済みのワークフローに、ロックに無いアクションを足してpushすると、ジョブが1つも始まらずに実行が startup_failure になります。
クラスメソッドさんの検証では、actions/cache@v4 をロックを更新せずに足したとき、こんな表示になったそうです。
The lockfile could not be validated. Regenerate it by running `gh actions-lock`.
gh actions-lock --verify を手元で叩くと、Not pinned actions/cache@v4 のように足りない参照を教えてくれます。
直し方はシンプルで、gh actions-lock をもう一度実行してロックをコミットするだけです✅
逆に、タグが付け替えられただけでは止まりません。
記録したコミットのまま動き続けるので、新しいバージョンを使いたいときは自分で --relock します。
# 動いたタグ(main や v4 など)を今のコミットに進める
gh actions-lock --relock
--accept-moved という、元のリポジトリから消えたコミットを受け入れるオプションもあります。
ただ公式のREADMEでも「なぜ動いたか確認してから」「自動化では使わない」とはっきり書かれているので、私はCIには入れないつもりです🙏
あと、書き込みは全部成功か何もしないかの二択です。
1つでも解決できない参照があると、ほかのワークフローの分も含めて何も書き込まれないので、エラーが出たら最初の1件から順に見ていくのが近道になりそうです。
🔒 ロックしてくれないもの
便利な反面、今の時点で対象外のものもあります。
- 別のリポジトリにある再利用ワークフロー(
jobs.<id>.uses)は固定されない。固定したいなら、そこだけ40桁のSHAで書く - コンテナイメージは固定されない。
@sha256:のダイジェストが無いイメージは警告が出るだけ - 入力の値・Secrets・ランナーのイメージは対象外。ロックするのはランナーで動くアクションのコードだけ
- GitHub Enterprise Server は非対応(github.com と GHE.com のデータレジデンシーのみ)
それと、公式も念押ししていますが、ロックしたからといって、そのアクション自体が安全になるわけではありません。
「レビューした時点の中身に固定できる」だけなので、新しく足すアクションは、ロックする前に中身と元のリポジトリをちゃんと見ておきたいです。
CI周りだと、以前GitHub Actionsのnpmキャッシュの記事で速くする話を書きました。
あちらは速度、今回は「毎回同じ中身が動くか」の話です。
キャッシュの actions/cache も、ほかのアクションの中から呼ばれていればロックの対象になります。
組織やEnterpriseの管理者なら、ロックが無いワークフローの実行を止めるポリシー(Require lockfile)もプレビューで用意されています。
いきなり全体で必須にすると、まだ入れていないリポジトリのCIがまとめて止まりそうなので、まずは1リポジトリで --verify まで回してから広げるのが安全そうです(`・ω・´)
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
👍 まとめ
タグ指定の読みやすさはそのままで、動く中身だけコミットに固定できるのが、ワークフローロックファイルの一番うれしいところです。
SHAのピン留めが面倒で後回しにしていた人ほど、入れやすいと思います。
ただ、ロックが古いと startup_failure で止まるので、gh actions-lock --verify をPRで回すところまではセットで入れたほうがいいです。
再利用ワークフローやコンテナイメージは対象外なので、そこは今までどおりSHAやダイジェストで固定します。
まだプレビューでファイル形式やオプションが変わる可能性もありますが、まずは自分の個人リポジトリの1本から試してみます!
みんなはActionsのピン留め、どうしてます?👀
※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報はGitHub公式ドキュメントをご確認ください👀