GitHub Actionsで手動と自動のどちらでも実行できるデプロイワークフローを作る

やりたいこと

GitHub Actionsで次の要件を実現できるデプロイのワークフローを作りたい。

  • 次のトリガーでデプロイできる
    • リポジトリへのAPI呼び出し
    • 手動
    • プルリクエストのマージ
  • デプロイするブランチと環境(主にstagingもしくはproduction)を選択できる

Web APIからだと次のようにデプロイできる。

# デプロイをリポジトリへのAPI呼び出しとして実行
$ curl -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <token> \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/<owner>/<repo>/dispatches \
  --data '{ "event_type": "deploy", "client_payload": { "branch": "main", "environment": "production" }}'

Web UIだと次のような画面を通じてデプロイできる。

デプロイワークフローを実行するWeb UI

このようなワークフローを作るメリットとして次の点がある。

  • ユースケースに応じて異なるデプロイ方法を選択できる
    • ふだんの開発フローでは自動デプロイしたstagingで動作確認する
    • 他システムやSlackと連携するときはAPI呼び出しする
    • 緊急時などにブラウザから手動でデプロイできる
  • 複数環境のデプロイを同じワークフローで管理しつつ、環境ごとに異なる設定を利用できる

deployment environmentの作成

あらかじめリポジトリのdeployment environment*1としてstagingとproductionの2つの環境を作っておく。ワークフローからはこのenvironmentを使うと次のようなメリットがある。

  • 環境ごとのデプロイ履歴が残る
  • デプロイを許可するブランチを設定できる

Web UI上でenvironmentを作成すると次のようになる。

作成したenvironment

API呼び出しと手動のどちらでも実行できるワークフロー

まず、API呼び出しか手動でワークフローに対して入力を渡し、その入力に基づいてデプロイを実行するようなワークフローを作る。つまり、1枚のワークフローファイルに

  • API呼び出しか手動で渡した入力を処理
  • 入力に基づくデプロイ

の両方を書く。

name: Deploy

on:
  # API呼び出しで実行
  repository_dispatch:
    types: [deploy]

  # 手動で実行
  workflow_dispatch:
    inputs:
      environment:
        default: staging
        options:
          - staging
          - production
        required: true
        type: choice

jobs:
  setup:
    runs-on: # some runner

    # トリガーするイベントに応じてinputをoutputに変換
    steps:
      - id: setup-from-inputs
        if: github.event_name != 'repository_dispatch'
        run: |
          echo environment=${{ inputs.environment }} >> $GITHUB_OUTPUT
          echo branch=${{ github.ref_name) }} >> $GITHUB_OUTPUT
      - id: setup-from-payload
        if: github.event_name == 'repository_dispatch'
        run: |
          echo environment=${{ github.event.client_payload.environment }} >> $GITHUB_OUTPUT
          echo branch=${{ github.event.client_payload.branch) }} >> $GITHUB_OUTPUT

    # ジョブのoutputとしてenvironmentとbranchを設定
    outputs:
      environment: ${{ steps.setup-from-inputs.outputs.environment || steps.setup-from-payload.outputs.environment }}
      branch: ${{ steps.setup-from-inputs.outputs.branch || steps.setup-from-payload.outputs.branch }}

  # setup.outputs.environmentがproduction以外のときにstagingへのデプロイを実行
  staging:
    needs: setup
    if: needs.setup.outputs.environment != 'production'
    runs-on: # some runner

    # deployment environmentに関する設定
    environment:
      name: staging
      url: https://staging.example.com
    concurrency: staging

    steps:
      - uses: actions/checkout@v3
      - name: deploy
        run: # ...

  # setup.outputs.environmentがproductionのときにproductionへのデプロイを実行
  production:
    needs: setup
    if: needs.setup.outputs.environment == 'production'
    runs-on: # some runner

    # deployment environmentに関する設定
    environment:
      name: production
      url: https://example.com
    concurrency: production

    steps:
      - uses: actions/checkout@v3
      - name: deploy
        run: # ...

repository_dispatchの場合はdeployイベントでenvironmentbranchをパラメータとして渡すことを想定している。

workflow_dispatchの場合はWeb UIまたはGitHub CLIから手動で実行する*2。デプロイ先の環境を入力として受け取る。type: choiceにすると、Web UIのセレクトボックスからoptionで渡した選択肢を選択できる。

setupジョブではどのトリガーでワークフローが実行されたかを判別して、environmentbranchをジョブのoutputとして設定している。stagingジョブとproductionジョブはsetupジョブに依存しており、setupのoutputに基づいてstagingproductionのどちらか1つのジョブだけを実行する。

environmentのセクションでnameで作成済みのenvironmentの名前を指定し、さらにデプロイ先をURLとして指定する。このURLはdeploymentに関するUIで利用される。2023年3月の時点ではnameには式を渡せない*3ので、ここでは文字列を直接渡している。

また、concurrencyにenvironmentの名前を指定することで、あるenvironmentに対して同時に複数のデプロイジョブが実行されることを防ぐ。

プルリクエストをマージすると自動でデプロイするワークフロー

上のワークフローを使って、自動デプロイワークフローを作る。上のワークフローにトリガーとしてworkflow_callを追加する。

name: Deploy

on:
  repository_dispatch:
    types: [deploy]
  workflow_call: # 追加
    inputs:
      environment:
        default: staging
        type: string
  workflow_dispatch:
    # ...

workflow_callをトリガーとして追加すると、reusable workflowとして他のワークフローから呼び出せるようになる。

これで次のように自動でデプロイするワークフローを作れる。

name: Auto deploy

on:
  push:
    branches:
      - master

jobs:
  staging:
    uses: ./.github/workflows/deploy.yml
    with:
      environment: staging
    secrets: inherit

usesにdeploy.ymlへのパスを、入力としてenvironment: stagingを指定してデプロイを実行する。reusable workflowを使うときはstepではなくjob直下にusesを書くことに注意。また、リポジトリのシークレットをreusable workflowで使いたいときは、secrets: inheritと書くとreusable workflowにそのままシークレットを渡せる。第三者のreusable workflowを使うときはsecrets: inheritは使わず、必要なシークレットだけを明示的に渡すほうがよい。