GitHub Actionsワークフローを1行ずつ解剖
コピペで動かしてきたCIを、自分で読み書きできる状態にする。テストとビルドの実例をonからpermissionsまで1行ずつ解剖し、cronのUTC解釈やフォークとsecretsの関係まで、事故を防ぐ要点を押さえる。
- onは実行契機で、push・pull_requestのほかworkflow_dispatchで手動、scheduleで定期実行を定義する。cronはUTCなので、日本時間の予定から9時間引いて書く。
- usesの版はタグよりコミットSHA固定が堅い。タグは付け替えられるがSHAは内容と1対1だからだ。なおフォークからのpull_requestにはsecretsが渡らない。
- permissionsでGITHUB_TOKENを最小権限にし、needsでジョブ依存、strategy.matrixで組み合わせを宣言する。actions/cacheはkeyを誤ると毎回フルインストールになる。
この設定ファイルは何者か
.github/workflows/ ディレクトリに置いたYAMLファイルは、GitHub Actionsのワークフロー定義だ。リポジトリへのpushやプルリクエストといったイベントを合図に、GitHubが用意する仮想マシン(ランナー)の上でテスト・ビルド・デプロイを自動実行する。CI/CDパイプラインの本体であり(全体像はDevOpsを参照)、1ファイルが1本のワークフローに対応し、複数置けばそれぞれ独立に動く。
このファイルの厄介さは、理解していなくても動いてしまう点にある。コピペした定義でもテストは通り、バッジは緑になる。しかしその実体は、リポジトリの権限を持って動く小さなプログラムであり、secretsの扱いや権限設定の甘さは事故に直結する。だからこそ1行ずつ、何がなぜ書かれているのかを読み解く価値がある。
横にスクロール
1行ずつ解剖
題材として、2つのNode.jsバージョンでテストを走らせ、すべて成功したらビルドする、という現実的なワークフローを用意した。
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
schedule:
- cron: "0 21 * * *" # UTCで解釈される(日本時間の翌朝6時)
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
name と on——いつ動くか
nameはActionsタブに表示されるワークフロー名で、省略するとファイルパスがそのまま表示される。
onはトリガーの宣言だ。この例では4種類を束ねている。pushはbranchesで対象をmainに絞り、pull_requestは絞り込みなしなので、どのブランチ宛てのプルリクエストでも動く。workflow_dispatchを書いておくと、WebのUIやAPIから手動で実行できる。手動実行のボタンは、このファイルがデフォルトブランチに存在して初めて現れる。
scheduleはcron書式による定期実行で、時刻は常にUTCとして解釈される。コメントのとおり、UTCの21時は日本時間では翌朝6時にあたる。またscheduleが動くのはデフォルトブランチ上の定義だけで、公開リポジトリでは60日間活動がないと自動的に無効化される。
日本の朝はUTCでは前日の夜。時刻を9時間引くだけでなく、曜日指定も1日ずれる。平日の朝のつもりで月〜金を指定すると、日本時間では火〜土の朝に動く。
permissions——GITHUB_TOKENを最小にする
ワークフローの実行ごとに、GitHubはGITHUB_TOKENという一時トークンを自動発行する。permissionsは、このトークンに与える権限スコープの宣言だ。既定でどこまで許されるかはリポジトリや組織の設定に依存するため、ファイル側で明示するのが確実で、この例のようにcontentsの読み取りだけへ絞れば、チェックアウトはできるが書き込みは一切できないトークンになる。リリース作成やプルリクエストへのコメントが必要なジョブにだけ、ジョブ単位のpermissionsで個別に権限を足すのが定石だ。呼び出したアクションが悪意ある挙動をしたとしても、この宣言が被害範囲の上限になる。
jobs と runs-on——どこで動くか
jobsの下に並ぶtestとbuildがジョブだ。ジョブごとにまっさらな仮想マシンが割り当てられ、runs-onでその種類を選ぶ。ubuntu-latestのほか、windows-latestやmacos-latestが指定できる。重要なのはジョブ間で環境が共有されないことで、testで入れたnode_modulesはbuildには存在しない。だからbuildでもnpm ciをやり直している。ジョブは既定で並列に走り、順序が必要ならneedsで宣言する。
strategy.matrix——組み合わせの展開
matrixは、同じジョブ定義を変数の組み合わせの数だけ複製する仕組みだ。この例ではnode-versionに20と22を与えたので、testは2つのジョブに展開されて並列に走る。ステップ側からは、冒頭の例のnode-version指定にあるような式構文(ドル記号と二重波かっこで囲む記法)で値を参照する。軸を複数書くと直積で展開される点には注意がいる。3軸に各4値なら64ジョブ、1回の実行でmatrixが生成できるジョブは最大256だ。excludeで特定の組み合わせを間引き、includeで例外的な組み合わせを追加できる。
steps——uses と run
stepsは、ジョブ内で上から順に実行される手順のリストで、書き方は2系統ある。usesは公開されたアクション(再利用可能な処理部品)の呼び出し、runはシェルコマンドの実行だ。runの各ステップは独立したシェルプロセスとして動くため、cdや環境変数の設定は次のステップへ持ち越されない。なおnpm ciはpackage-lock.jsonの記載どおりに依存を入れ直すコマンドで、CIでnpm installを使わない理由も含めてpackage.jsonの解剖で扱った。
usesのバージョン固定——タグは動かせる
usesの実体はGitHub上のリポジトリで、アットマーク以降がバージョン指定だ。v4のようなタグ指定は読みやすいが、タグは配布元の管理者が後から別のコミットへ付け替えられる。同じv4でも昨日と今日で実行される中身が変わり得るということで、2025年3月には、広く使われていたアクションの既存タグ群が悪意あるコミットへ付け替えられる事件が実際に起きた。コミットSHAはコミット内容から計算される識別子なので、SHAで指定すれば実行される中身が一意に固定される。
| 指定方法 | 挙動 | 堅牢性 |
|---|---|---|
| ブランチ(main) | 常にそのブランチの最新を実行 | 低い。中身が毎回変わり得る |
| タグ(v4) | リリースに追従 | 中。タグは付け替えできる |
| コミットSHA | そのコミットの内容に固定 | 高い。中身とSHAが1対1に対応する |
# タグ指定:読みやすいが、中身は可変
- uses: actions/checkout@v4
# フルSHA指定:中身が固定される。人間向けにバージョンをコメントで残す
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
actions/checkout と actions/cache
actions/checkoutは、リポジトリのコードをランナー上へ取得する定番の最初のステップだ。既定では対象コミット1つだけの浅いクローンになるため、タグ一覧や全履歴が必要な処理ではfetch-depthに0を指定して全履歴を取る。
依存取得の高速化を担うのがキャッシュで、この例ではsetup-nodeのcacheオプション(npmを指定)が保存と復元を肩代わりしている。汎用のactions/cacheを直接使う場合は、キーの設計が自分の仕事になる。
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
keyにロックファイルのハッシュを含めるのが基本形だ。依存が変われば自動的に別キーになって作り直され、restore-keysの前方一致によって、完全一致がなくても近い内容のキャッシュから部分復元できる。キャッシュの保存はリポジトリあたり10GBが上限で、7日間アクセスされないものから削除される。
env と secrets——フォークには渡らない
envは環境変数の定義で、ワークフロー全体・ジョブ・ステップの3階層に書け、内側の定義が優先される。APIトークンのような秘密情報はYAMLに直書きせず、リポジトリ設定のSecretsへ登録して参照する。値はログ上で自動的にマスクされる。
- run: npx deploy-tool
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
重要な仕様が一つある。フォークされたリポジトリからのpull_requestで動くワークフローには、secretsが渡されない(GITHUB_TOKENも読み取り専用に落ちる)。外部の書いたコードが秘密情報へ届かないようにする防御であり、フォークからのプルリクエストでデプロイ系のステップだけが失敗するのは、この仕様が正しく働いている証拠だ。
needs——ジョブをつなぐ
needsは他ジョブの完了を待つ宣言だ。buildにはneedsとしてtestが指定されているので、buildはmatrix展開された2つのtestがすべて成功してから始まり、どれか1つでも失敗すればスキップされる。テストを通ったものだけをビルドし、その先のデプロイへ渡す。パイプラインの骨格を作るのがこのキーで、リスト形式で書けば複数ジョブの合流点も表現できる。
つまずきやすい点
最大の危険はpull_request_targetの安易な使用だ。名前は似ているが、こちらはsecretsへアクセスできる文脈でフォーク由来のプルリクエストに反応する。この文脈でプルリクエスト側のコードをチェックアウトして実行すると、外部の書いたコードに秘密情報と権限を差し出すことになる。ラベル付けのようなメタデータ操作以外では使わず、CIは通常のpull_requestで行うのが安全だ。この境界の考え方はセキュリティの各記事に通じる。
cronのUTC解釈も定番の罠だ。時刻の9時間差に加えて曜日が1日ずれる。さらにscheduleはデフォルトブランチの定義しか見ないため、作業ブランチで実験しても動かない。pushでは動くのにscheduleだけ沈黙する場合、まずこの2点を疑う。
matrixは便利さゆえに膨張しやすい。OSが3種、言語バージョンが3つ、データベースが2つで既に18ジョブになり、プルリクエストのたびに全部が走れば、待ち時間も、プライベートリポジトリなら課金もかさむ。プルリクエストでは主要な組み合わせだけを回し、全組み合わせは夜間のscheduleに逃がすのが現実的だ。
キャッシュはキー設計を誤ると静かに壊れる。keyに実行日時のような毎回変わる値を混ぜると永遠にヒットせず、毎回フルインストールに逆戻りする。逆にkeyを固定文字列にすると、依存を更新しても古いキャッシュを使い続ける。ロックファイルのハッシュをkeyに、その前方一致をrestore-keysに、という基本形から離れるときほど慎重に設計したい。
まとめ
ワークフローファイルは、いつ動くか(on)、どこで動くか(runs-on)、何をするか(steps)、何を許すか(permissions)の宣言を束ねた、リポジトリの権限で動く小さなプログラムだ。usesのSHA固定とpermissionsの最小化という2つの習慣だけでも、事故の芽は大きく減らせる。次は自分のリポジトリのワークフローを開き、すべての行の意味を説明できるか確かめてほしい。説明できない1行が、たいてい次の障害の入り口になる。
設定ファイル解剖の記事ガイド
GitHub Actionsワークフローを1行ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
設定ファイル
比較で見る軸
難易度: advanced / カテゴリ: 設定ファイル解剖 / タグ数: 4
導入後に効く点
usesの版はタグよりコミットSHA固定が堅い。タグは付け替えられるがSHAは内容と1対1だからだ。なおフォークからのpull_requestにはsecretsが渡らない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 設定ファイル解剖
- タグ数
- 4
判断チェックリスト
- 自社の用途が「設定ファイル / GitHub Actions」に近いか確認する。
- 強みである「onは実行契機で、push・pull_requestのほかworkflow_dispatchで手動、scheduleで定期実行を定義する。cronはUTCなので、日本時間の予定から9時間引いて書く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。