メインコンテンツへスキップ
見出し画像

gh-actions-lock v0.2.0 の検査は、どこで失敗し、どこで通るか――終了コードと警告を測った

    GitHub Actions の uses のタグは、あとから別の commit を指すように付け替えられます。その依存を commit に固定する gh 拡張 gh-actions-lock が、2026-10-12 に v0.2.0 として公開されました(prerelease ではありません)。GitHub Docs のセキュリティ強化ガイドが、このロックファイルを「最良の手段」と書いている一方で、README と help には終了コードの一覧がなく、既存のワークフローに入れたとき、検査がどこで通り、どこで失敗するのかが分かりませんでした。そこで、検査の側を、ダミーのリポジトリで測りました。測定日は 2026-10-17 です。

    用語

    • gh-actions-lock は、ワークフローの uses が指す action と再利用ワークフローを commit に固定し、結果を .github/workflows の下の actions.lock に記録する gh の拡張です。README の冒頭に「Public preview」「pre-1.0」とあり、フォーマットとフラグは予告なく変わりうると書かれています

    • --verify-local は、すべての uses に対応する lock の項目があるかを、ネットワークなしで調べる検査です。lock の中身(commit が正しいか)は調べません

    • --verify は、--verify-local と同じ検査に加えて、lock の commit が GitHub 上で確認できるかを API で調べる検査です。README は CI で使うよう勧めています

    • findings は、検査が返す指摘の一覧です。JSON で出すと、指摘ごとに category(種類)、severity(error か warning)、confidence が付きます

    • valid は、検査の結果の JSON にある真偽の値です。測定では、warning の指摘だけのとき true でした

    • lock の version は、lock ファイルの形式の version で、ツールのバージョン(v0.2.0)とは別です。測定では、公式リポジトリの lock が v0.0.2、v0.2.0 のバイナリが書く lock が v0.0.3 でした

    • 手書きの lock は、公式リポジトリの actions.lock の構造(ヘッダー、workflows、dependencies)をまねて、手で書いた lock です。ツールが作った lock ではありません。GitHub Docs は、lock を手で編集しないものと説明しています

    結論

    測れたのは、検査の側だけです。バージョン v0.2.0、Linux の Docker コンテナの中で、実在するトークンを使わずに測りました。

    • 終了コードは、0(通った)、1(findings がある)、2(実行できない)の 3 種類でした。認証がない、lock が読めない、ワークフローがない、のどれかのとき、終了コードは 2 です

    • ステップの uses が ./ で始まるローカルアクションは、--verify-local で local-action の指摘になり、終了コード 1 でした。一方、ジョブの uses が ./ で始まるローカルの再利用ワークフローと、docker:// のステップは、lock にそのワークフローの項目(空の配列)があれば、終了コード 0 で通りました

    • ダミーのトークンのとき、および API に到達できないとき、--verify は API 呼び出しの失敗を警告(severity は warning、valid は true)にして、「All 1 workflow valid」と出力し、終了コード 0 で終了しました。lock の commit を 0 だけの値に書き換えた場合も同じでした。本来は検出してほしい状況でも、終了コードだけを見る CI では、通ったように見えます。ref の不一致や lock の欠落のように、ローカルで判断できる不一致は、同じ条件でも終了コード 1 になりました

    実トークンで API に届く環境の結果は、測っていません。この記事から言えるのは、検証の呼び出しが失敗した場合に、警告つきで終了コード 0 になることまでです。

    lock の作成、uses の書き換えの前後の差分、実トークンでの --verify は、認証が要るため測っていません。詳しくは「測っていないこと」に書きました。

    測り方

    • Docker コンテナ(debian の bookworm-slim、CPU 2 つ、メモリ 4GB)の中で測りました。gh 2.102.0 を GitHub の公式の apt リポジトリから入れ、拡張は gh extension install で入れました。バージョンは v0.2.0 です

    • 拡張のインストールは、認証なしで通りました。GH_TOKEN は設定せず、gh auth login もしていません。実在するトークンとアカウントは使っていません

    • ダミーのトークンは、実在しない文字列を GH_TOKEN に入れたものです。API に到達できない条件は、ネットワークなしのコンテナで作りました

    • ワークフローは、ダミーのリポジトリに 1 本から 2 本書きました。lock は手書きです。commit と ID は、公式リポジトリの actions.lock に載っている actions/checkout の v5.0.1 の値を使い、example-org のような名前は架空です

    • リリースの日付と README の記述は、2026-10-17 11:58 UTC に GitHub から取得しました。

    読者がご自身の環境で試すときの注意です。--verify-local は、README に書き込まないとあります。--verify と --no-fix は、リポジトリ内のワークフローと lock を書き換えませんが、実行のログをホームのキャッシュに保存します。通常実行(--verify も --no-fix も付けない gh actions-lock)は、help と README の記述では、lock とワークフローを書き換えうるので、使い捨てのコピーで実行してください。今回の測定で、書き換わったことを確かめたのは、lock の version の行だけです。ワークフローの書き換えは、測っていません。

    gh extension install github/gh-actions-lock

    終了コードは 0、1、2 の 3 種類

    --verify-local を、14 の状況で測りました。主なものを、終了コードと findings の種類とともに並べます。

    • lock がなく、ワークフローが actions/checkout@v5.0.1 を使うとき、終了コード 1 で、not-pinned の指摘が 1 件出ます。lock に checkout@v5.0.1 が載っているときは、終了コード 0 です

    • lock に v5.0.1 が載っていて、ワークフローを checkout@v4 に書き換えたとき、終了コード 1 で、ref-changed と stale の 2 件が出ます。ref-changed は ref が lock と違うこと、stale は lock の項目をどの uses も参照していないことを示します

    • ワークフローが、40 桁の SHA を直接指定し、lock に載っていないとき、終了コード 1 で、not-pinned と sha-as-ref(warning)が出ます

    • checkout@main と、実在しない action(no-such-owner-zzz/no-such-action@v1)が、どちらも lock にないとき、終了コード 1 で、どちらも not-pinned です。オフラインなので、実在するかどうかは見ません

    • 2 本のうち 1 本だけ lock に項目がないとき、終了コード 1 で、「1 of 2 workflows failed」と出ます。JSON の workflows に、ワークフローごとの valid が入ります

    • 削除したワークフローの項目が lock に残っているときは、終了コード 0 で通りました。古い項目は、--verify-local では指摘されませんでした。理由は調べていません

    • lock が壊れた YAML のとき、lock の version が v9.9.9 のとき、.github/workflows にワークフローがないときは、終了コード 2 です

    1 of 1 workflow failed: 1 not-pinned
    Run without --verify-local to resolve and pin missing actions and migrate `./…` refs to `$/…`.
    [exit=1]
    

    ref を書き換えた場合は、次のように出ます。

    ✗ Ref changed actions/checkout@v4
    workflow uses ref "v4" but lockfile pins "v5.0.1"
    ✗ Unused lockfile entry actions/checkout@v5.0.1
    lockfile pins actions/checkout@v5.0.1 but no uses: in this workflow references it
    1 of 1 workflow failed: 1 ref-changed, 1 stale
    

    読めない lock と、新しすぎる version は、検査の結果ではなく、実行できないものとして終了コード 2 になります。新しい version のときの出力は、次のとおりです。

    lockfile version v9.9.9 is newer than this binary supports (v0.0.3)
    [exit=2]
    

    ローカルアクション、再利用ワークフロー、docker:// の結果

    読者が既存のワークフローに入れるとき、まず当たりそうな 3 つを、手書きの lock で測りました。

    ステップの uses が ./.github/actions/hello のローカルアクションは、lock に項目があっても、--verify-local が local-action の指摘で失敗しました。

    ✗ local action .github/workflows/ci.yml
    workflow uses local path actions which are not supported; remove local path actions to continue using the lockfile
    1 of 1 workflow failed: 1 local-action
    [exit=1]
    

    ジョブの uses が ./.github/workflows/reusable.yml のローカルの再利用ワークフローは、呼び出す側と呼ばれる側の両方の項目(空の配列)を lock に書くと、終了コード 0 で通りました。項目がなければ、2 本とも not-pinned で失敗します。依存を持たない reusable.yml にも、項目が要ります。

    同じリポジトリを指す $/ の形(action と再利用ワークフロー)も、両方の項目があれば通りました。

    README は、ローカルパスの action は lock のあるワークフローでは使えないと書いています。取得した行の範囲では、./ の書き換えの記述の対象は「リポジトリ内の action ファイルに解決される参照」で、その記述は再利用ワークフローに触れていません。測定の結果は、この書き方と矛盾しませんでした。

    docker://alpine:3.20 のステップは、lock にそのワークフローの項目(空の配列)があれば、終了コード 0 で通りました。項目がなければ、not-pinned で終了コード 1 です。docker:// の参照を lock の項目に書くと、lock が読めず、終了コード 2 になりました。

    dependency "docker://alpine:3.20" is not a valid pin (expected OWNER/REPO@REF)
    [exit=2]
    

    外部の再利用ワークフロー(owner/repo/.github/workflows/x.yml@v1.2.3 の形)も、lock のキーにパスを含めると、同じエラーで終了コード 2 になりました。キーに書けるのは、owner/repo@ref の形だけでした。キーを example-org/shared@v1.2.3 にすると、workflow 側の uses と一致せず、stale の指摘で終了コード 1 になりました。

    ここまでの「通る」は、すべて、手書きの lock にその項目を書いた場合の結果です。lock を作る実行が、ローカルの再利用ワークフローや docker:// の項目をどう書くかは、測っていません。

    --verify は、API の失敗を警告にして終了コード 0 で終わる

    --verify-local は、lock の commit を見ません。lock の commit を 0 が並ぶ値に書き換えても、終了コード 0 で通りました。README の「--verify-local は、項目が信頼できるかを確かめない」という記述と一致します。

    信頼できるかの確認は、--verify の役割です。実トークンがないので、ダミーのトークンで測りました。認証がないときは、--verify も通常実行も、次のとおり終了コード 2 です。

    authentication token not found for host github.com
    [exit=2]
    

    ダミーのトークンを GH_TOKEN に入れると、lock が完全なとき、--verify は次のように出力して、終了コード 0 で終わりました。

    Scanning 1 workflow
    actions/checkout@v5.0.1: could not verify locked actions/checkout@93cb6efe1820: HTTP 401: Bad credentials (https://api.github.com/graphql)
    ↳ retry; the runner will re-check this pin at job start
    All 1 workflow valid
    [exit=0]
    

    出力の 2 行目に「ランナーがジョブの開始時に、この pin を再確認する」とあります。ランナー側の動作は、この記事では測っていないので、この警告を許容してよい根拠にはなりません。

    JSON で出すと、この 401 は、種類が reachability-unknown の警告になり、全体の valid は true です。

    "category": "reachability-unknown",
    "severity": "warning",
    "confidence": "low",
    "valid": true
    

    lock の commit を 0 が並ぶ値に書き換えた改ざんの想定でも、結果は同じでした。

    actions/checkout@v5.0.1: could not verify locked actions/checkout@000000000000: HTTP 401: Bad credentials (https://api.github.com/graphql)
    All 1 workflow valid
    [exit=0]
    

    ネットワークなしのコンテナでは、401 の代わりに、api.github.com の名前が引けないというエラー(network is unreachable)が出て、同じく警告と終了コード 0 になりました。

    同じダミーのトークンでも、ローカルで判断できる不一致は、終了コード 1 で失敗しました。ref を checkout@v4 に書き換えた場合は ref-changed と stale、lock がない場合は not-pinned です。

    README は、ランナーがジョブの開始時に、lock の各 pin を再検査し、失敗した検査は実行をブロックすると書いています。これはランナー側の動作の説明で、今回は測っていません。--verify-local の説明では、CI では --verify を使うよう勧めています。help の例には「exits 1 if invalid」とあり、help は --verify を、lock が不完全か、ランナーが pin を拒否するなら失敗する、と説明しています。取得した README と help には、API 呼び出しの失敗を警告として扱う記述と、valid が true のまま警告つきで終わる場合の説明は見つかりませんでした。

    ご自身の CI に入れるなら、終了コードと valid だけで判定すると、この警告を見落とします。JSON の findings の severity と category も読んでください。実トークンで API に届く環境で同じ警告が出るか、出るなら何のときかは、測っていません。

    gh actions-lock --no-fix --json=valid,findings

    ダミーのトークンで通常実行(--verify も --no-fix も付けず、--no-interactive を付けた)したときは、検証が 401 の警告で続いたあと、version が v0.0.2 の lock を v0.0.3 に書き換えて、終了コード 0 で終わりました。差分は version の行と末尾の空行だけです。

    -version: 'v0.0.2'
    +version: 'v0.0.3'
    

    ワークフローの書き換えは、help と README の記述によるもので、今回は測っていません。測ったのは、lock の version の行の書き換えだけです。

    同じ種類の道具との違い

    比較は、各道具の README と公式の文書を読んだ範囲です。pinact と ghasum は、動かしていません。

    • gh-actions-lock は、SHA を固定した依存を、推移的な依存も含めて、.github/workflows の actions.lock に記録します。GitHub Docs の概念のページの説明です(2026-10-17 取得)。ローカルの ./ の参照は、通常実行が $/ の形に書き換えます(README)。このページは、冒頭に preview の注記を持ちます

    • pinact は、ワークフローの uses を commit の SHA に書き換え、コメントにバージョンを書きます。README と文書に、lock ファイルを作る記述は見当たりませんでした。終了コードは公式の文書にあり、0 は全部固定済みか修正した、1 は --fix=false なのに固定が必要、2 は自動で直せない(ブランチ参照など)、3 は API エラーやフラグの不整合などです。--no-api を付けると、API を使わず、40 桁の SHA の形式だけを検査します

    • ghasum は、gha.sum にチェックサムを記録し、ghasum verify で検証します。README の制約に、Docker ベースの unpinnable actions は未対応(Issue 216)とあります

    pinact run --check
    ghasum verify

    どちらのコマンドも、この記事では実行していません。

    GitHub Docs のセキュリティ強化ガイドは、lockfile を「最良の手段」と書き、lockfile を採用するリポジトリでは SHA の固定に取って代わると説明しています。この記述は、feature フラグ(actions-workflow-lockfiles)つきの文書にあります。lockfile を採用できないリポジトリには、SHA の固定を勧めています。リポジトリの policy の Enforce SHA Pinning は、個別の文書を取得していないので、ここでは扱いません。

    ご自身のワークフローに当てはめるとき

    • ステップの uses が ./ で始まるローカルアクションがあるかを探してください。あれば、--verify-local は終了コード 1 で、local-action の指摘を出します

    • ローカルの再利用ワークフローと docker:// のステップは、ワークフローの項目が lock にあるかが、結果を左右します。lock を作る実行の結果は測っていないので、実際に作った lock で、--verify-local を実行して確かめてください

    • CI に入れる前に、--verify が API に届かないときの終了コードを、ご自身の環境で確かめてください。この記事の測定では、ダミーのトークンと到達不能で、終了コード 0 でした

    gh actions-lock --verify-local
    gh actions-lock --verify

    測っていないこと

    • 実トークンでの gh actions-lock(lock の作成、uses の書き換えの前後の差分、./ から $/ への自動の書き換え、タグの絞り込み)。認証が要るので、トークンの入力も gh auth login もしていません

    • 実トークンでの --verify(commit が ref の履歴にあるかの確認、リポジトリの rename や transfer の検出)、--relock、--accept-moved

    • 実行時に、GitHub Actions のランナーが lock を強制する動作

    • lock を作る実行が、docker:// と外部の再利用ワークフローを、lock にどう記録するか

    • 手書きの lock による測定の限界。ツールが作った lock の形式とは、一部が違う可能性があります

    • pinact と ghasum の動作。README と文書を読んだ範囲です

    • Windows と macOS。Linux の Docker コンテナの中だけで測りました

    • v0.2.0 以外のバージョン。README は、フォーマットとフラグが予告なく変わりうると書いています。lock の version は、公式リポジトリの lock が v0.0.2、v0.2.0 のバイナリが書くものは v0.0.3 でした

     
     

    タネ

     
     
    会社員エンジニア。学生時代からPythonで個人開発を続けています。PCの「困った」を自動化で解決し、主にDockerで測った結果を記事にしています。読んだ後、少しでも作業が楽になれば嬉しいです。

    あなたへのおすすめ