•12 min read

GitHub ActionsでCI/CDをセットアップする方法

GitHub ActionsでCI/CDをセットアップする方法

私はCI/CDへの導入が遅れていました。何年もの間、私はローカルでテストを実行し、指を交差させて(うまくいくことを祈って)プッシュしていました。それは問題なく機能していましたが、ある日、ビルドを壊すコミットをプッシュしてしまい、どの依存関係の変更が原因だったのかを突き止めるのに1時間も費やしました。

GitHub Actionsは、パブリックリポジトリでは無料で、プライベートリポジトリでは私が知る限り最も安価な選択肢です。Next.jsアプリからPython API、Goサービスまで、さまざまなプロジェクトで実行した結果、私がたどり着いたセットアップがこれです。

Audio Briefing
0:00 / 0:00

なぜ他の選択肢ではなくGitHub Actionsなのか?

CIツールを選ぶ前に:GitHub Actionsはリポジトリ上で直接実行され、接続するサードパーティサービスは不要です。Jenkinsはサーバーが必要です。CircleCIは、一部のユースケースではより良い無料枠を提供しています。Travis CIは有料のみになりました。GitLabをすでに使用しているなら、GitLab CIは素晴らしい選択肢です。

GitHubでホストされているプロジェクトの場合、Actionsは利便性で優れています。Webhookの設定も、トークンの管理も不要で、トリガーはコードの隣で定義されます。


Advertisement

初めてのワークフロー

GitHub Actionsのワークフローは、YAMLファイルとして.github/workflows/に配置されます。.github/workflows/ci.ymlを作成してください。

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm run build

これをプッシュすると、次のコミットでワークフローがトリガーされます。すべてのPRに緑色のチェックマークまたは赤色のXが付き、壊れたコードをマージすることは二度とありません。

npm ci vs npm install

CIではnpm installではなくnpm ciを使用してください。npm ciはより厳格です。package-lock.jsonに記載されているものを正確にインストールし、不一致があれば失敗し、ロックファイルを更新することはありません。これにより、CI環境がローカルでテストしたものと一致します。


キャッシュで高速化

上記のcache: 'npm'行は、package-lock.jsonのハッシュに基づいて、実行間でnode_modulesをキャッシュします。これがないと、ワークフローが実行されるたびに、すべての依存関係がゼロから再インストールされます。これは、依存関係の数に応じて、実行ごとに60〜120秒かかります。

キャッシュを使用すると、キャッシュヒットにより5〜10秒に短縮されます。

他のエコシステムの場合:

# Python (pip)
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Python (uv)
- uses: astral-sh/setup-uv@v3
  with:
    enable-cache: true

# Go
- uses: actions/setup-go@v5
  with:
    go-version: '1.22'
    cache: true

# Rust
- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target/
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

キャッシュはロックファイルのハッシュに基づいてキーが設定されます。依存関係が変更されると、キャッシュはミスして再構築されます。変更されない場合、キャッシュはヒットし、CIは高速になります。


並行テスト

大規模なテストスイートの場合、テストを複数のランナーに分割します。

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/4

これにより、4つのジョブが同時に実行され、それぞれがテストの4分の1を処理します。合計テスト時間は4倍から約1倍に短縮されます。

Playwrightは--shardでこれをネイティブにサポートしています。Jestの場合は--testPathPatternまたはjest-runner-groupsを使用してください。pytestの場合はpytest-splitを使用してください。


Advertisement

自動デプロイ

CI(テスト)とCD(デプロイ)を分離します。これらは異なる失敗セマンティクスを持っています。テストの失敗はPRをブロックすべきですが、デプロイの失敗は他のPRをブロックすべきではありません。

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy to Vercel
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
        run: npx vercel --prod --token $VERCEL_TOKEN
ハードコードされたトークンではなくシークレットを使用する

APIキーやデプロイトークンをYAMLファイルに直接記述しないでください。GitHub → Settings → Secrets and variables → Actionsに保存し、${{ secrets.MY_TOKEN }}として参照してください。シークレットは保存時に暗号化され、ログではマスクされ、フォークされたPRには決して公開されません。

テスト成功時のみデプロイ

needsキーワードを使用してCIとCDを連結します。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npx vercel --prod --token ${{ secrets.VERCEL_TOKEN }}

deployジョブは、testが成功し、かつmainブランチにいる場合にのみ実行されます。


環境変数とシークレット

GitHub Actionsで設定を扱うための3つの階層:

1. YAMLにハードコード — 機密性のない、普遍的な値のみ:

env:
  NODE_ENV: production
  PORT: 3000

2. リポジトリシークレット — 機密性の高い値(APIキー、トークン):

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}

3. 環境シークレット — デプロイ環境ごとに異なる値(ステージング vs プロダクション):

jobs:
  deploy:
    environment: production  # uses the "production" environment's secrets
    steps:
      - run: deploy.sh
        env:
          API_URL: ${{ secrets.API_URL }}  # production-specific value

環境シークレットには承認ゲートが必要です。Settings → Environments → protection rulesで必要なレビュー担当者を設定します。誤った本番デプロイを防ぐのに役立ちます。


苦労して学んだこと

メインブランチだけでなく、すべてのPRでCIを実行する。 PRブランチでテストの失敗を検出する方が、マージ後に検出するよりもはるかに安価です。すべてのワークフローにpull_request: branches: [main]を追加してください。

CIとCDを分離する。 CIワークフローはプッシュごとに実行され、5分以内に完了すべきです。CDワークフローはデプロイを行い、より時間がかかる場合があります。これらを混同すると、遅いデプロイがPRのフィードバックループをブロックしてしまいます。

マトリックスビルドは請求額を増やす。 マトリックス戦略で複数のNodeバージョンやOSにわたってテストすると、ランナーの時間が何倍にもなります。私は、クロスバージョン互換性が実際に重要なライブラリの場合にのみ使用します。アプリの場合、1つのターゲット環境で十分です。

# Only do this for libraries, not apps
strategy:
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest, macos-latest]

これはプッシュごとに9つのランナーです。それぞれ2分とすると、合計18分の計算時間になります。

利用時間を監視する。 GitHub Actionsは、無料プランのプライベートリポジトリに対して月2,000分の無料枠を提供しています。マトリックスビルドを使用し、1日10回プッシュするプロジェクトでは、1週間で使い切ってしまう可能性があります。

fail-fast: trueで高速に失敗する(マトリックスビルドのデフォルト)。1つのマトリックスジョブが失敗すると、GitHubは他のジョブをキャンセルします。根本的なエラーがある場合に時間を節約できます。

Dockerレイヤーには明示的にactions/cacheを使用する。

- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

これがないと、Dockerは実行ごとにイメージ全体をゼロから再構築します。


完全な本番環境対応ワークフロー

以下は、私がNext.jsプロジェクトで使用している完全なワークフローです。

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: .next/
          retention-days: 1

主な機能:

  • concurrencyは、新しいプッシュが到着したときに進行中の実行をキャンセルします。これにより、活発な開発中のキューの蓄積を防ぎます。
  • lintとtestは並行して実行され、buildは両方の完了を待ちます。
  • カバレッジレポートはプッシュごとにCodecovに送信されます。
  • ビルド成果物は1日間アップロードされます(再構築せずにデプロイジョブが利用するのに便利です)。

失敗したワークフローのデバッグ

ワークフローが失敗し、ログが不明瞭な場合:

デバッグログを有効にするには、リポジトリシークレットACTIONS_STEP_DEBUG=trueを設定します。これにより、すべてのステップから詳細な出力がダンプされます。

tmateを使用してランナーにSSH接続する:

- uses: mxschmitt/action-tmate@v3
  if: ${{ failure() }}

これにより、ワークフローが一時停止し、ステップが失敗したときに正確なランナー環境へのSSH接続が提供されます。環境固有の問題をデバッグするのに非常に貴重です。

環境の詳細を出力する:

- run: node --version && npm --version && env | sort

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement