GitHub Actions(v2;yaml)でプルリクエスト毎に確認用アプリ(Review Apps)を立ち上げる(PaaS: Zeit Now)

## この記事で説明・提供していること

- GitHub Actionsの基礎知識：かんたんな用語整理(Actions, Workflow, Job, Step)
- Pull Requestのオープン(`opened`)と追加コミット・プッシュ(`synchronize`)時だけ実行されるWorkflowの書き方
- CI(test)が成功したときだけDeployが実行されるWorkflow (Job)の書き方
- 実行するアクションの中でPull Request(PR)のNumber(`#99`)やRepository名を取得する方法
- [Zeit Now](https://zeit.co/docs "ZEIT Now is a cloud platform for static sites and serverless functions.")にPR毎のアプリ(Review Apps)をPR NumberをURLに含むかたちでDeployする方法
- ↑でDeployされたReview AppのURLをPRにコメントとして書き込む方法
- これらが書かれたGitHub Actions(v2)設定yamlの完成コード

### Change Log

- 2019/09/10 13:00：初稿として全体コードだけ載せました。詳細は後で書きます（ストックしておくと更新通知が来るので良かったらどうぞ）
- 2019/12/04 20:00：全体コードに加えて各パートの解説を書きました

## GitHub Actionsの基礎知識

- `Actions`： あるRepositoryに存在する`Workflow`の集まり
- `Workflow`： Repositoryに対する特定の行動(Push, Pull Request, etc.)に対して実行される`Job`の集まり
- `Job`： 複数（または単一）の`Step`を含む`Workflow`の実行単位。
  各`Job`は並列で実行されるが依存関係を設定することもできる。
  実行されるプラットフォーム(Ubuntu etc.)の指定もここで行う
- `Step`: `Job`内で順番に実行される一つ一つの処理。前の処理の成功・失敗に応じて実行有無を変えることも可能

## 設定yamlの完成コード

パーツ毎の説明は後述するので、まずは最初に設定ファイル全体を載せます。（ ~~説明は後で書きます~~ 書きました）

また、完成コードを含むExampleをGitHubに置きました。
[noriaki/gh-actions-example-with-zeit-nextjs-and-now: Example of GitHub Actions v2 with Zeit Next.js and Now](https://github.com/noriaki/gh-actions-example-with-zeit-nextjs-and-now)
設定yamlコードの場所は[~/.github/workflows/pull_request.yml](https://github.com/noriaki/gh-actions-example-with-zeit-nextjs-and-now/blob/master/.github/workflows/pull_request.yml)です。

```yaml
on:
  pull_request:
    types: [opened, synchronize]

name: Test and Deploy with PR number alias and comment apps URL

jobs:
  test:
    name: CI Test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1
        with:
          fetch-depth: 1

      - name: "[INFO] Versions"
        run: |
          echo "node: `node --version`"
          echo "npm:  v`npm --version`"
          echo "yarn: v`yarn --version`"

      - name: Install packages
        run: >-
          yarn install
          --frozen-lockfile
          --ignore-optional
          --no-progress
          --non-interactive

      - name: Test
        run: yarn test

  deploy_to_zeit_now_staging:
    name: Deploy and Alias with PR number
    runs-on: ubuntu-latest
    needs: test

    steps:
      - uses: actions/checkout@v1
        with:
          fetch-depth: 1

      - name: "[INFO] Versions"
        run: |
          echo "node: `node --version`"
          echo "npm:  v`npm --version`"
          echo "yarn: v`yarn --version`"
          echo "now:  v`now --version`"

      - name: Deploy
        env:
          ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }}
        run: >
          now deploy
          --build-env NODE_ENV=production
          --env NODE_ENV=production
          --public
          --no-clipboard
          --token ${ZEIT_TOKEN}
          > ${HOME}/deployment-url.txt

      - name: Alias with PR number
        env:
          ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }}
        run: >
          now alias
          --token ${ZEIT_TOKEN}
          `cat ${HOME}/deployment-url.txt`
          ${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh

  post_comment_review_app_url:
    name: Post comment that review app URL when PR opened
    runs-on: ubuntu-latest
    needs: deploy_to_zeit_now_staging

    steps:
      - name: Post comment review app URL by GitHub API
        if: github.event_name == 'pull_request' && github.event.action == 'opened'
        run: >
          curl
          -X POST
          -H "Authorization: token $GITHUB_TOKEN"
          -H "Accept: application/vnd.github.v3+json"
          --data "{ \"body\": \"Review-App was deployed.\nhttps://${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh\" }"
          ${{ github.event.pull_request.comments_url }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

以下、完成コードから一部抜粋するかたちで解説していきます。

## Pull Requestのオープン(`opened`)と追加コミット・プッシュ(`synchronize`)時だけ実行されるWorkflowの書き方

```yaml
on:
  pull_request:
    types: [opened, synchronize]
```

冒頭のこの部分で`pull_request`の「`opened`：プルリクオープン時」と「`synchronize`：オープン済みプルリクに追加プッシュ時」を指定しています。

## CI(test)が成功したときだけDeployが実行されるWorkflow (Job)の書き方

```yaml
jobs:
  test: # <- (a)
    name: CI Test
    runs-on: ubuntu-latest

# ...中略...

  deploy_to_zeit_now_staging: # <- (b)
    name: Deploy and Alias with PR number
    runs-on: ubuntu-latest
    needs: test
#   ↑`needs`で指定した値（この場合`test`）をIDに持つJob(a)が
#    正常終了した場合だけこのJob（b）が実行される
```

コメントに書いたとおりですが、`needs:`に指定したJob IDが正常に終了したら実行されます。
ちなみにJob IDは同じWorkflow内で重複しなければ任意のものを指定できます。また、[配列で指定すれば複数のJobの正常終了を条件にすることも可能(GitHub Help)](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds)です。

## 実行するアクションの中でPull Request(PR)のNumber(`#99`)やRepository名を取得する方法

`${{ github.event.repository.name }}`にリポジトリの名前が、`${{ github.event.number }}`にプルリクNumberが入っていて、以下のようにRunコマンドの中に書くと実行時に展開されます。

```yaml
      - name: Alias with PR number
        env:
          ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }}
        run: >
          now alias
          --token ${ZEIT_TOKEN}
          `cat ${HOME}/deployment-url.txt`
          ${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh
```

その他に取得できる情報は以下のページに書いてあります。
[Contexts and expression syntax for GitHub Actions - GitHub Help](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context)

ちなみに、上記コードの`run: >`というのは、それ以降のヒアドキュメント文字列を半角空白(` `)で連結する`YAML`の記法です。

## Zeit NowにPR毎のアプリ(Review Apps)をPR NumberをURLに含むかたちでDeployする方法

プルリク内容を本番に近い状態で確認できるようにすると、コードに加えて動作もレビューすることができます。これは、個人的にHerokuのキラー機能と思っている[Heroku Review Apps](https://qiita.com/G-awa/items/c1286fed0575aaa05b34)で実現されているものですが、同じことを`Zeit Now`で実現するためのJobです。

```yaml
  deploy_to_zeit_now_staging:
    name: Deploy and Alias with PR number
    runs-on: ubuntu-latest
    needs: test

    steps:
      - uses: actions/checkout@v1
        with:
          fetch-depth: 1

      # (1)
      - name: "[INFO] Versions"
        run: |
          echo "node: `node --version`"
          echo "npm:  v`npm --version`"
          echo "yarn: v`yarn --version`"
          echo "now:  v`now --version`"

      - name: Deploy
        env: # (2)
          ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }}
        run: > # (3)
          now deploy
          --build-env NODE_ENV=production
          --env NODE_ENV=production
          --public
          --no-clipboard
          --token ${ZEIT_TOKEN}
          > ${HOME}/deployment-url.txt

      - name: Alias with PR number
        env:
          ZEIT_TOKEN: ${{ secrets.ZEIT_TOKEN }}
        run: >
          now alias
          --token ${ZEIT_TOKEN}
          `cat ${HOME}/deployment-url.txt`
          ${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh
```

この設定コードのポイントとしては、以下3点です。

1. Zeit Nowのデプロイ等のコマンドラインツール`now`は、`node`や`yarn`と同じく(1)でバージョン表示しているように`ubuntu-latest`に最初からインストールされています[^1]
2. (2)の部分で指定している`secrets.ZEIT_TOKEN`は[ZeitのTokensページ](https://zeit.co/account/tokens)で発行したものを、対象のリポジトリ設定(settings/secrets)に`ZEIT_TOKEN`という名前で保存すれば使えます
3. 同一Job内のStep間で値を受け渡ししたいときは、(3)`> ${HOME}/deployment-url.txt`のように`${HOME}`ディレクトリ以下にファイルとして保存し、以降のStepで`` `cat ${HOME}/deployment-url.txt` ``のように読み出すことが可能です

## DeployされたReview AppのURLをPRにコメントとして書き込む方法

```yaml
  post_comment_review_app_url:
    name: Post comment that review app URL when PR opened
    runs-on: ubuntu-latest
    needs: deploy_to_zeit_now_staging

    steps:
      - name: Post comment review app URL by GitHub API
        if: github.event_name == 'pull_request' && github.event.action == 'opened'
        run: >
          curl
          -X POST
          -H "Authorization: token $GITHUB_TOKEN"
          -H "Accept: application/vnd.github.v3+json"
          --data "{ \"body\": \"Review-App was deployed.\nhttps://${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh\" }"
          ${{ github.event.pull_request.comments_url }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### プルリクのオープン時のみ実行されるJob

冒頭でプルリクのオープン時・追加Push時に実行されるWorkflowについて説明しましたが、以下の部分ではjob毎に実行条件を指定しています。以下の場合、プルリクでオープン時のみ実行されます。[^2]

```yaml
        if: github.event_name == 'pull_request' && github.event.action == 'opened'
```

### GitHub Actionsの中からGitHub APIを叩く

GitHub Actions実行時に自動でセットされている`secrets.GITHUB_TOKEN`には書き込み権限があるため、`curl`で[GitHub APIのコメント投稿(Issue Comments)](https://developer.github.com/v3/issues/comments/#create-a-comment)のエンドポイント`${{ github.event.pull_request.comments_url }}`にPOSTしてあげればOKです。

```yaml
        run: >
          curl
          -X POST
          -H "Authorization: token $GITHUB_TOKEN"
          -H "Accept: application/vnd.github.v3+json"
          --data "{ \"body\": \"Review-App was deployed.\nhttps://${{ github.event.repository.name }}-pr-${{ github.event.number }}.now.sh\" }"
          ${{ github.event.pull_request.comments_url }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

[^1]: その他に使えるSoftwareの一覧：[Software installed on GitHub-hosted runners - GitHub Help](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts)
[^2]: その他`if:`に指定できる条件：[Workflow syntax for GitHub Actions - GitHub Help](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#jobsjob_idif)