GitHub Actionsワークフロー(YAML)の書き方入門:トリガー・ジョブ・ステップ

GitHub ActionsのワークフローYAMLの書き方を解説。on(トリガー)・jobs(ジョブ)・steps(ステップ)の基本構成に加え、ジョブの並列実行モデル、シークレットのマスキングと漏洩リスク、マトリックスビルド、actions/cacheによるキャッシュ設計、GITHUB_TOKENの権限まで、実務で必要な要点をカバーします。

GitHub Actionsとは

GitHub Actionsは、GitHubリポジトリ内で直接、ソフトウェア開発ワークフローを自動化できるCI/CD(継続的インテグレーション/継続的デリバリー)プラットフォームです。コードのビルド、テスト、デプロイなどを自動化し、開発プロセスを効率化します。

ワークフローは、リポジトリのルートディレクトリに以下の構造でYAMLファイルを配置することで定義します。

.github/workflows/{任意のファイル名}.yml

YAMLファイルの構成要素

GitHub Actionsのワークフローを定義するYAMLファイルは、主に以下の要素で構成されます。

name: { このワークフローの名前 } # ワークフローの名前。GitHub UIで表示される

on:
  # ワークフローが実行されるトリガーイベントを指定
  # 例1: pushイベント (指定したブランチへのpush時)
  push:
    branches:
      - main # または master
      - develop # 複数のブランチを指定可能

  # 例2: pull_requestイベント (指定したブランチへのプルリクエスト時)
  pull_request:
    branches:
      - main
      - develop

  # 例3: scheduleイベント (CRON形式で時間を指定)
  # UTC時間で指定するため、日本時間(JST)の場合は-9時間する
  schedule:
    - cron: "45 10 * * *"

  # その他のトリガーイベント: https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows

jobs:
  # 実行するジョブの定義
  build: # ジョブのID (任意の名前)
    name: ビルドとテスト # ジョブの表示名
    runs-on:
      ubuntu-latest # ジョブを実行する環境 (ランナー) を指定
      # ubuntu-latest, windows-latest, macos-latest など
      # または、特定のバージョン (ubuntu-20.04)

    steps:
      # ジョブ内で実行される一連のステップ
      - uses: actions/checkout@v4 # リポジトリのコードをチェックアウトするアクション
        # GitHubが提供するアクションや、コミュニティのアクションを利用できる

      - name: 依存関係のインストール # ステップの名前
        run: |
          npm install # 例: Node.jsプロジェクトの依存関係をインストール
          pip install -r requirements.txt # 例: Pythonプロジェクトの依存関係をインストール

      - name: コードのビルド # ステップの名前
        run: |
          npm run build # 例: プロジェクトをビルド
          make # 例: makeコマンドを実行

      - name: テストの実行 # ステップの名前
        run: |
          npm test # 例: テストを実行
          pytest # 例: Pythonのテストを実行

主要な要素の解説

  • name: ワークフロー全体の名前です。GitHubのActionsタブで表示されます。
  • on: ワークフローがいつ実行されるかを定義します。push, pull_request, schedule など、様々なイベントを指定できます。
  • jobs: ワークフロー内で実行される一連のジョブを定義します。各ジョブは独立して実行することも、依存関係を設定して順次実行することも可能です。
    • runs-on: ジョブを実行する仮想環境(ランナー)を指定します。Ubuntu, Windows, macOSの最新バージョンや特定のバージョンを選択できます。
    • steps: ジョブ内で実行される個々のタスクのシーケンスです。
      • uses: 既存のアクション(再利用可能なタスクの単位)を使用します。actions/checkout@v4 は、リポジトリのコードをランナーにチェックアウトする公式アクションです。
      • name: ステップの表示名です。
      • run: シェルコマンドを実行します。複数行のコマンドは | を使って記述できます。

ここまでが最小限の構成要素です。ここから先は、実際にワークフローを設計・運用する際に必ずぶつかる「ジョブはどう実行されるのか」「シークレットは安全に扱えているか」「ビルドを速くするには」「権限はどこまで絞るべきか」という実務上の論点を、具体的なYAMLとともに掘り下げます。

実行モデル:ジョブは並列、ステップは逐次

GitHub Actionsの挙動を正しく予測するには、次の3つのルールを押さえる必要があります。

  1. 同じワークフロー内の複数の jobs は、既定では並列に実行されるneeds キーで依存関係を指定しない限り、ジョブ同士は互いを待たない。
  2. 1つのジョブの中の steps は、常に上から順番(逐次)に実行される。前のステップが完了するまで次のステップは始まらず、あるステップが失敗すると、既定ではそのジョブの残りのステップはスキップされる。
  3. 各ジョブは、それぞれ独立した新品(クリーン)の仮想マシン(ランナー)上で実行される。同じワークフロー内であっても、ジョブが変わればファイルシステムも環境変数もプロセスも一切引き継がれない。

この3つ目のルールが初学者にとって最も見落としやすい点です。「同じワークフローなのだから、ジョブAでビルドした成果物をジョブBでそのまま使えるはず」と考えてしまいがちですが、実際にはジョブBは全く別のVMで起動するため、ジョブAのファイルシステムには一切アクセスできません。ジョブ間で状態(ビルド成果物、生成したファイル、計算結果など)を受け渡すには、明示的に次のいずれかを使う必要があります。

  • actions/upload-artifact でジョブAの成果物をアップロードし、actions/download-artifact でジョブBがダウンロードする
  • actions/cache で内容に基づくキーのキャッシュとして保存し、後続ジョブ(や後続のワークフロー実行)で復元する
  • ジョブの outputs で小さな文字列値(バージョン番号や判定フラグなど)だけを受け渡す

下の図は、push/pull_request トリガーから2つのジョブ(buildlint-and-test)が並列に起動し、それぞれのジョブ内ではステップが上から順に実行される様子を示したものです。ジョブ間を結ぶ点線は、成果物の受け渡しには upload-artifact/download-artifact が必要であり、既定では何も共有されないことを表しています。

GitHub Actionsのジョブ・ステップ実行モデル。2つのジョブは並列に開始し、各ジョブ内のステップは逐次実行される。ジョブ間の状態共有にはupload-artifact/download-artifactが必要

実際のワークフローで確認する

このブログ自身も含むこのリポジトリ(web-source)の .github/workflows/deploy.yml は、次のような構成になっています(実際のファイルから抜粋)。

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      # ...(中略:npm ci、型チェック、Hugo/Next.jsビルド、デプロイ、E2Eテストと続く)

このワークフローは build-and-deploy という単一のジョブだけで完結しています。つまり、Hugoのビルド・Next.jsのビルド・デプロイ・E2Eテストはすべて同じランナー上で順番に実行され、ジョブ間の受け渡しを考える必要がありません。もしこれを「ビルドするジョブ」と「デプロイするジョブ」に分割していたら、ビルド成果物(dist/ ディレクトリなど)を upload-artifact/download-artifact で明示的に橋渡ししない限り、デプロイ側のジョブはビルド成果物を見つけられずに失敗していたはずです。単一ジョブに留めているのは、まさにこの「クリーンな環境が毎回用意される」という制約を回避するための、実務上よくある選択です。

エッジケース(a): シークレットの取り扱いとマスキング

secrets コンテキスト

APIキーやデプロイ鍵のような機密情報は、リポジトリの Settings > Secrets and variables > Actions に登録し、ワークフロー内では secrets.<名前> コンテキストで参照します。

- name: Deploy to yuhi-sa.github.io
  uses: peaceiris/actions-gh-pages@v4
  with:
    deploy_key: ${{ secrets.DEPLOY_KEY }}
    external_repository: yuhi-sa/yuhi-sa.github.io
    publish_branch: main
    publish_dir: ./dist

(これも deploy.yml からの実際の抜粋です。SSH Deploy Keyを secrets.DEPLOY_KEY として渡し、yuhi-sa/yuhi-sa.github.io に直接プッシュしています。)

ログ上でのマスキング

GitHub Actionsは、ワークフローの実行ログに登録済みシークレットの値がそのまま出現すると、自動的に *** へ置き換えて表示します。例えば次のようなステップを書くと、

- name: デバッグ出力(本来は避けるべき)
  run: echo "token is ${{ secrets.DEPLOY_KEY }}"

実際のログには次のように表示され、値そのものは読めません。

token is ***

このマスキングは「ログの文字列に、登録済みシークレットの値と完全一致する部分文字列が含まれているかどうか」を機械的にチェックして置換する仕組みです。裏を返すと、シークレットの値そのままではなく加工した形でログに出力すると、マスキングは効きません。

実際に起きたセキュリティインシデント:マスキングのバイパス

これは理論上の懸念ではなく、実際に大規模なインシデントとして発生しています。2025年3月、23,000以上のリポジトリで利用されていたサードパーティアクション tj-actions/changed-files が改ざんされ、悪意あるコードが混入しました( CVE-2025-30066 )。この攻撃コードは、ランナーのワーカープロセスのメモリをダンプしてシークレットらしき文字列を探し出し、それを2重にBase64エンコードしてからログに出力することで、GitHubの自動マスキングを回避しました。マスキングは「シークレットの生の値」と完全一致する文字列しか検出できないため、エンコードして値の見た目を変えてしまえば素通りしてしまいます。公開リポジトリではワークフローのログも公開されているため、AWSアクセスキー、GitHub PAT、npmトークン、秘密鍵などが誰でも閲覧できる状態で漏洩しました。

ここから得られる実務上の教訓は次の2つです。

  • サードパーティアクションは可変なタグ(@v4 など)ではなく、コミットSHAで固定するuses: tj-actions/changed-files@11052...のように)。タグは後から差し替えられる可能性があるが、SHAは特定のコミット内容に紐づくため改ざんの影響を受けない。
  • マスキングは万能ではない。シークレットをBase64エンコードする、文字列を分割する、他の値と連結する、あるいは4文字未満の短いシークレットを扱う、といった操作をすると、マスキングをすり抜けてログに残る可能性がある。

もう1つの見落としがちな穴:アーティファクトとキャッシュ

マスキングはログの標準出力にのみ適用され、actions/upload-artifact でアップロードしたファイルや、actions/cache に保存した内容までは検査してくれません。例えば、シークレットを含む一時ファイル(.env の生成結果やビルドログなど)をうっかりアーティファクトとしてアップロードすると、ログ上は安全に見えても、そのアーティファクトをダウンロードした人には値がそのまま見えてしまいます。「シークレットを直接 echo しない」だけでなく、「シークレットを含みうるファイルをアーティファクト/キャッシュとして外部に出さない」という視点も必要です。

エッジケース(b): マトリックスビルド

複数のOSや言語バージョンの組み合わせでテストしたい場合、strategy.matrix を使うと、1つのジョブ定義から複数のジョブが自動生成されます。

jobs:
  test:
    strategy:
      fail-fast: false # 既定はtrue。1つが失敗しても他を止めたくない場合はfalseにする
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

この例では os に3種類、node-version に3種類指定しているため、組み合わせの数だけ(3 × 3 = 9個の)ジョブが自動的に生成され、すべて並列に実行されます。ジョブごとに matrix.osmatrix.node-version の値が異なるコンテキストとして注入されます。

fail-fast の挙動

strategy.fail-fast の既定値は true です。この場合、マトリックス内のいずれか1つのジョブが失敗すると、GitHubはまだ実行中の他のマトリックスジョブをすべてキャンセルします。これは「Node.js 18で早々に失敗したのだから、19や20の結果を待つ意味がない」という高速フィードバックを狙った設計です。

一方で、次のような場面では fail-fast: false にして無効化すべきです。

  • リリース前の互換性マトリックス確認:「Node 18ではダメだが20と22では動く」という情報こそが知りたいので、1つの失敗で残りをキャンセルされては困る。
  • 並列で独立した検証を行いたい場合:例えばOS別のE2Eテストで、Windows特有のバグとmacOS特有のバグを同じ実行の中で両方拾いたいとき。
  • フレーキー(不安定)なテストの調査:どの組み合わせで再現し、どの組み合わせでは再現しないかを比較したいとき。

エッジケース(c): キャッシュ設計

依存関係のインストール(npm cipip install など)は、変更がなければ実行のたびに同じ結果を再構築しているだけの無駄な処理です。actions/cache を使うと、指定したパスをキーに紐づけて保存し、次回以降のジョブで再利用できます。

- name: Cache npm dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

Pythonであれば同様に次のように書けます。

- name: Cache pip dependencies
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

キーの設計

key には hashFiles('**/package-lock.json') のように、ロックファイルの内容から計算したハッシュ値を含めるのが定石です。こうすることで、

  • ロックファイルの内容が変わらない限り、同じキーでキャッシュヒットし、依存関係の再ダウンロード・再インストールをスキップできる
  • ロックファイルが更新された(依存関係が変わった)瞬間に、キーが変わって自動的にキャッシュミスになり、新しい依存関係一式が正しくインストールされる

という2つの要求を同時に満たせます。もしハッシュを含めず固定文字列だけをキーにすると、依存関係を更新してもキャッシュがヒットし続けてしまい、古いバージョンのまま気づかないという事故につながります。restore-keys はキーが完全一致しない場合のフォールバックで、「OSだけ一致する直近のキャッシュ」を部分的に再利用しつつ、差分だけを更新する形で高速化を図ります。

このリポジトリの deploy.yml でも、Next.jsのビルドキャッシュとPlaywrightのブラウザキャッシュの両方で actions/cache@v4 を使っています。

- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      apps/calcbox/.next/cache
      apps/devtoolbox/.next/cache
      apps/pomodoro/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('apps/**/*.ts', 'apps/**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      ${{ runner.os }}-nextjs-
- name: Cache Playwright browsers
  uses: actions/cache@v4
  id: playwright-cache
  with:
    path: ~/.cache/ms-playwright
    key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}

- name: Install Playwright Browsers
  if: steps.playwright-cache.outputs.cache-hit != 'true'
  run: npx playwright install --with-deps chromium

- name: Install Playwright system deps (cached browser)
  if: steps.playwright-cache.outputs.cache-hit == 'true'
  run: npx playwright install-deps chromium

このNext.jsキャッシュのキーは、package-lock.json のハッシュと、apps/ 配下の .ts/.tsx ファイル全体のハッシュの両方を連結しています。依存関係が変わった場合はもちろん、TypeScriptのソースコード自体が変わった場合にもキーが変わり、古いビルドキャッシュを引きずらないようにする設計です。また、Playwrightのキャッシュ例では id: playwright-cache を付け、steps.playwright-cache.outputs.cache-hit の値によって「キャッシュがヒットした場合」と「ミスした場合」で異なるステップを条件分岐(if:)させています。キャッシュがヒットすればブラウザバイナリのダウンロードをまるごとスキップし、システム依存パッケージのインストールだけを行います。

実測タイミング:このリポジトリ自身のCI実行履歴から

gh run view <run-id> --json jobs でこのリポジトリの実際のワークフロー実行履歴を取得し、Playwrightキャッシュのヒット/ミスを比較したところ、次のような実測値が得られました。

ケース該当ステップ所要時間実行日時(Run ID)
キャッシュミスInstall Playwright Browsers(ブラウザ本体のダウンロード+インストール)約25秒2026-07-02 (Run 28593370712)
キャッシュヒットInstall Playwright system deps (cached browser)(システム依存パッケージのみ)約15〜20秒2026-07-18 (Run 29636104857 ほか複数回)

このケースでは差が約6〜10秒程度と、それほど劇的ではありません。理由は、時間のほとんどを占めているのが apt によるシステム依存パッケージのインストールであり、これはキャッシュのヒット・ミスに関わらず毎回必要になるためです。ブラウザバイナリ自体のダウンロード・展開だけを見れば、キャッシュヒットにより数秒〜十数秒が短縮されています。

一方で、node_modules 全体やPythonの仮想環境のように、パッケージ数が多く1つ1つのダウンロード・ビルドに時間がかかる依存関係キャッシュでは、ヒット時とミス時の差がもっと大きくなるのが一般的です(この点は本リポジトリの実測データではなく、典型的な傾向としての目安です)。キャッシュの効果はキャッシュ対象の性質に強く依存するため、「導入すれば必ず劇的に速くなる」と決めつけず、実際に自分のワークフローで計測することをおすすめします。

エッジケース(d): 権限(GITHUB_TOKEN)の最小化

すべてのワークフロー実行には、GitHubが自動的に発行する一時的なトークン secrets.GITHUB_TOKEN が渡されます。これはリポジトリへのAPIアクセス(チェックアウト、Issueへのコメント、Releaseの作成など)に使われますが、既定でどこまでの権限を持つかはリポジトリの設定に依存します。

  • リポジトリの Settings > Actions > General > Workflow permissions で、「Read and write permissions」(多くのスコープに対する読み書き権限)と「Read repository contents permission」(読み取り専用)のいずれかを選べます。
  • 2023年2月以降に新規作成されたリポジトリでは、既定値が読み取り専用側に変更されましたが、それ以前から存在するリポジトリでは読み書き権限が既定のままになっていることがあります。

しかし、リポジトリ全体の既定設定に依存するのではなく、ワークフローファイル自体に明示的な permissions: ブロックを書くのが最小権限の原則(Principle of Least Privilege)に沿ったベストプラクティスです。

permissions:
  contents: read # コードの読み取りだけ許可
  pull-requests: write # PRへのコメント投稿だけ追加で許可
  # 明示していないスコープは自動的に「アクセスなし」になる

permissions はワークフロー全体にも、個々のジョブ単位にも設定できます。ジョブによって必要な権限が異なる場合(例:ビルドジョブは contents: read だけでよいが、デプロイジョブだけ contents: write が必要)は、ジョブ単位で分けて指定するとより厳格になります。

なお、このリポジトリ自身の deploy.yml を確認すると、明示的な permissions: ブロックは設定されておらず、リポジトリ側の既定権限に依存しています。デプロイ自体はSSH Deploy Key(secrets.DEPLOY_KEY)で外部リポジトリへプッシュしており、GITHUB_TOKEN の権限に依存する処理はしていないため実害はありませんが、最小権限の原則を厳密に適用するなら permissions: { contents: read } を明示するとより堅牢になる、という良い実例です。

2024〜2025年の重要なアップデート

GitHub Actionsは活発に更新が続いているプラットフォームです。この記事の内容に直接関わる、比較的新しい変更を2つ紹介します。

  • actions/cache のバックエンド刷新(2025年2月):GitHubは2025年2月1日に、キャッシュサービスの旧バックエンド(v1/v2 API)を廃止し、新しいキャッシュサービスに全面移行しました。新バックエンドはGitHubホストランナー上でのキャッシュのアップロード時間を最大80%短縮するとされています。この移行に伴い、actions/cachev4.2.0 以降(または v3.4.0 以降)を使う必要があり、古いバージョンのまま放置されたワークフローはキャッシュのアップロード/ダウンロードが失敗するようになりました。まだ古いバージョンをコミットSHA指定などで固定している場合は、v4 系の最新版に更新してください。
  • tj-actions/changed-files のサプライチェーン侵害(2025年3月、CVE-2025-30066):前述の通り、広く使われていたサードパーティアクションが改ざんされ、シークレットがログに漏洩する事件が発生しました。この事件を受けて、サードパーティアクションを利用する際は「タグではなくコミットSHAで固定する」「 GitHubのセキュリティアドバイザリ を定期的に確認する」といった対策の重要性が改めて認識されています。

これらはいずれも実際に確認された変更・インシデントであり、本記事執筆時点(2026年7月)の公開情報に基づいています。GitHub Actionsのアクションやキャッシュサービスは今後も変わっていくため、実運用では公式ドキュメントとセキュリティアドバイザリを定期的に確認することをおすすめします。

まとめ

GitHub Actionsのワークフローは、on/jobs/steps という3つの要素で表面上はシンプルに書けますが、実務で事故なく運用するには次のような実行モデル・セキュリティ面の理解が欠かせません。

  • ジョブは既定で並列、ステップは逐次。ジョブ間の状態共有には upload-artifact/download-artifact かキャッシュが必要
  • シークレットはログ上で *** にマスキングされるが、加工(エンコードなど)すると素通りする。サードパーティアクションはタグではなくコミットSHAで固定する
  • マトリックスビルドは組み合わせの数だけジョブを自動生成する。fail-fast: false で「途中で打ち切らず全結果を見る」挙動に切り替えられる
  • actions/cache のキー設計にはロックファイルのハッシュを含め、依存関係が変わった瞬間に確実にキャッシュミスさせる
  • GITHUB_TOKEN の権限はリポジトリの既定設定に頼らず、ワークフローファイルに permissions: を明示して最小化する

GitHub Actionsは非常に柔軟性が高く、様々なツールやサービスと連携して複雑なワークフローを構築できます。まずは小さなワークフローから始め、上記のポイントを1つずつ意識しながら育てていくのがおすすめです。