GitHub Actionsの概要と、VRTをやってみる

## はじめに
最近業務で初めてGitHub Actionsを使いました。
GitHubだけで完結し、npm scriptsなどと併用すると色々できて便利だと思う反面、初めて触ったときは設定項目が多くてとっつきにくいな、と感じました。
僕のようにGitHub Actionsよくわからないけど触ってみたいという方や、GitHub ActionsでVRTやってみたいという方の助けになれば良いと思い、記事を書いてみようと思います。

## GitHub Actions とは?
GitHub上の様々なイベントをフックにして、自分で定義したジョブを実行してくれる君です。ワークフローの設定や実行するジョブの内容はYAMLファイルに定義します。
公式: https://docs.github.com/ja/actions

## VRT(Visual Regression Testing)とは?
ソースコードの変更前と変更後の画面のスナップショットを比較して、差分を確認するテストです。
ソースコード変更後、UIが崩れていないかなどを確認するために行います。

## YAMLファイルの概要
GitHub ActionsのワークフローはYAMLファイルに定義していきます。
先に全体感をお伝えすると、GitHub Actionsは大まかに、ワークフローの下にジョブがあり、ジョブの下にステップがある、という構成になっていて、

- ワークフローは、実行するイベントを定義
- ジョブは、実行環境や実行順序を定義
- ステップは、実行するコマンドや処理を定義

のような役割を持っています。

(難しい話はできないので、簡潔に概要だけメモしてます。詳しくは[公式](https://docs.github.com/ja/actions/learn-github-actions/workflow-syntax-for-github-actions)を見てもらえればと思います。)

#### ワークフローの設定
ワークフローの設定には主に `name` と `on` の２つを記述します。
`name` はワークフローの名前で、この名前がActionsページなどで表示されるようになります。
`on` はワークフローのトリガーを指定します。指定するイベントは単体でも複数でも渡すことができ、ブランチ名による制御や、特定のブランチが閉じられたとき、ラベルを付けられたとき、手動でのみ実行、などのような詳細な設定も可能です。
たくさんのイベントがあるので、[こちら](https://docs.github.com/ja/actions/learn-github-actions/events-that-trigger-workflows)を見ながら要件にあったものを選定していただくのが良いかと思います。

- `name`: ワークフローの名前。
- `on`: ワークフローをトリガーするイベントの名前。単体でも配列でも渡すことができる。 

YAMLファイルにすると下記になります。

```yml
name: workflow-name
on: pull_request
```

#### ジョブの作成
ワークフローの中で実行するジョブは`jobs`に定義していきます。
ジョブは複数定義でき、デフォルトでは各ジョブは並列で実行されます。ジョブにはそれぞれ実行環境を設定することができ(`runs-on`)、実行時に指定された仮想環境のインスタンスが立ち上がり、その中でジョブが実行されます。

```yml
jobs: 
  job-id:
    runs-on: ubuntu-latest
```

#### ステップの作成
それぞれのジョブが持つステップは`steps`に定義していきます。
`run`で実際に実行するコマンドを定義します。処理に分岐をもたせたい場合は`if`で分岐させることもできます。
また、`uses`にGitHub上のリポジトリ名を渡すことで他の開発者が公開しているGitHub Actionsを使うこともできます。

```yml
jobs: 
  job-id:
    runs-on: ubuntu-latest
    steps: 
      - run: echo "hoge"
```

僕は実際に動かしたほうが理解が進むタイプなので、GitHub Actionsを動かしてみたいと思います。

## GitHub Actions やってみる その1
#### 下準備
まず、GitHub上に適当なリポジトリを用意してください。
今回私が使ったソースコードは一番最後にまとめてあります。

その後、
1. リポジトリのルートに`.github/workflows`を作成 
2. 1で作成したディレクトリ内に適当な名前をつけたYAMLファイルを作成

もしくは下記コマンドを実行してください。

```
$ mkdir -p .github/workflows && touch .github/workflows/my-action.yml
```

このYAMLファイルの中にGitHub Actionsのワークフローを定義していきます。

#### ワークフロー定義
下記のようなワークフローを作ってみました。

```yaml:.github/workflows/my-action.yml
name: My action 
on: pull_request
jobs:
  greeting: 
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello GitHub Actions! ${{ github.actor }}"
```
My actionという名前でPR作成時にActionが実行されるはずです。
ブランチを切ってPRを作成すると、

![PR.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/c561869b-e503-b992-e90b-6bc0312fdc42.png)

無事設定したワークフローが動いているのが確認できました。

![detail action.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/cf91f441-13f9-2faf-2c3e-862c56fe9734.png)

また、この例で使っている`${{ }}`はGitHub Actionsで使える[式構文](https://docs.github.com/ja/actions/learn-github-actions/expressions)になっていて、これを使うことで演算できたり、用意されている関数を利用できたり、[コンテキスト](https://docs.github.com/ja/actions/learn-github-actions/contexts#github-context)にアクセスして、ユーザーの情報やそのPRの情報などを取得することができます。

## GitHub Actions やってみる その2
次は[storycap](https://github.com/reg-viz/storycap)と[reg-cli](https://github.com/reg-viz/reg-cli)を使ったVisual Regression Testingを組み込んでみたいと思います。

### 下準備
モックアプリケーション用に`create-react-app`します。

```
$ npx create-react-app vrt-sample
```
`yarn start`でReactアプリケーションが立ち上がるところまで確認できたら、GitHub上にリポジトリを用意し、一旦pushしておきます。

次に[こちら](https://storybook.js.org/tutorials/intro-to-storybook/react/ja/get-started/)を参考にStorybookを導入していきます。

```
$ npx -p @storybook/cli sb init
```

`.env`ファイルを作成します。

```
$  echo SKIP_PREFLIGHT_CHECK=true > .env
```

Storybookを立ち上げてみます。

```
$ yarn storybook
```

Storybookの立ち上げまで確認できました。

(CRAのときにTypeScript導入を忘れていたので、ここでinstallしておきます :bow: )
インストールして、

```
$ yarn add typescript @types/node @types/react @types/react-dom @types/jest
```

`index.js`を`index.tsx`にrenameして、`yarn start`するとTypeScriptプロジェクトになります。便利です。
一旦ここまでをpushして、いくつかのコンポーネントを使って簡単な画面を組んでみます。

#### アプリケーションの画面を組む
下記のようなボタンコンポーネントとそのstoriesを用意しました。
ここでは詳細は省かせてください。

```src/components/atoms/Button/index.tsx
import type { ReactNode } from 'react'

export type Props = {
  children: ReactNode
}

export function Button ({ children }: Props) {
  return (
    <button 
      style={{
        background: 'rgb(236,87,133)',
        color: 'white',
        padding: '8px 16px',
        borderRadius: '8px',
        appearance: 'none',
        border: 'none'
      }}>
      {children}
    </button>
  )
}

```

```src/components/atoms/Button/index.stories.tsx
import { Story } from '@storybook/react'
import { Button, Props } from '.'

export default {
  title: 'components/atoms/Button',
  component: Button,
}
const Template: Story<Props> = (arg) => <Button {...arg}>label</Button>
export const Normal = Template.bind({})
```



#### storycapの導入
storycapとpuppeteerをinstallします。

```
$ yarn add storycap puppeteer
```
GitHub Actionsで動かすにあたって事前にStorybookをビルドしておいたほうが、動作が安定していたので、今回は先にビルドしておくようにします。

```
$ yarn build-storybook
```

下のscriptを`package.json`に追加し、実行します。

```json
"snapshot": "storycap --serverCmd \"cd ./storybook-static && python3 -m http.server 9001\" http://localhost:9001"
```

```
$ yarn snapshot
```

![スクリーンショット 2021-11-30 16.49.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/1f44914d-90e9-6d8a-05e1-90b26da4795a.png)


ルートディレクトリに`__screenshots__`ディレクトリが作成され、その中にsnapshotが確認できました :tada: 

#### reg-cliの導入
reg-cliをinstallします。

```
$ yarn add reg-cli
```

reg-cliは現状のsnapshotと正とするsnapshotを比較して、そこに差分がないかをチェックしてくれるツールになります。そのため、VRTで正とするsnapshotが必要になるので、この例では先程storycapで生成された`__snapshots__`を正としたいと思います。

先程の`snapshot` npmスクリプトを修正して、`__actual__`に吐き出すように修正します。

```package.json
"snapshot": "storycap --serverCmd \"cd ./storybook-static && python3 -m http.server 9001\" http://localhost:9001 -o __actual__"
```
VRTで必要になるスクリプトもここで追記しておきます。

```package.json
"vrt": "reg-cli __actual__ __screenshots__ __diff__ -R report.html",
"update-snapshot": "reg-cli __actual__ __screenshots__ __diff__ -U"
```

VRTを実行してみます。

```
❯ yarn vrt
yarn run v1.22.10
$ reg-cli __actual__ __screenshots__ __diff__ -R report.html
✔ pass    __actual__/components/atoms/Button/Normal.png


✔ 1 file(s) passed.
✨  Done in 1.57s.
```
同じビルド済みのstorybookからのキャプチャなので、同じなのは当たり前ですが、reg-cliを導入し、比較して差分が出ないことが確認できました :tada: 

#### 差分検出
ここでボタンのサイズを変更して、差分を検出してみます。

```tsx
padding: '8px 16px',
```
のpaddingを`24px 16px`にしてみます。

もう一度、`yarn build-storybook`を実行し、`yarn snapshot`を実行してスナップショットを取り直してみます。
スナップショットが更新できたら、`yarn vrt`でvrtを実行してみます。

```
❯ yarn vrt
yarn run v1.22.10
$ reg-cli __actual__ __screenshots__ __diff__ -R report.html
✘ change  __actual__/components/atoms/Button/Normal.png


✘ 1 file(s) changed.

Inspect your code changes, re-run with `-U` to update them. 
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```
変更されていることが検知できました :tada:
reportも確認してみます。

![diff.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/802c6987-e28b-ad1f-1049-ce1f75218022.gif)

レポートでもdiffが確認できました:clap:
一旦ここまでをpushし、次はVRTをGitHub Actions上で実行してみます。

### 本題
ようやくVRTをGitHub Actionsで実行してみます。

#### ワークフローの作成
vrtという名前で、下記内容のYAMLファイルを作成します。

```yaml:.github/workflows/vrt.yml
name: VRT
on: pull_request
jobs:
  vrt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install --global yarn
      - run: yarn install --frozen-lockfile

      - name: build-storybook
        run: yarn build-storybook

      - name: snapshot
        run: yarn snapshot

      - name: VRT
        run: yarn vrt
```

このワークフローはPR作成時にトリガーされます。
ここでは2つのpublic actionをつかっています。
`actions/checkout@v2`はGitリポジトリをチェックアウトして、`actions/setup-node@v2`はNode.jsが使えるように環境を整備してくれます。
このワークフローをpushしておきます。

適当な名前のブランチを切って、先程のように、Buttonコンポーネントのpaddingを修正するPRを出すと...

![fixbuttonstyle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/0797c8ae-d601-15c4-cea1-038bb0d731c2.png)


変更を検知して、VRTが落ちてくれました:clap:
ただ、これでは意図した変更が加えられているか確認できないのと、コンポーネントが増えたときに確認が大変になってしまいます。

### reg-cliのレポートを確認できるようにする。
先程のワークフローを修正して、最後にレポートをGitHub Actionsの[Artifact](https://docs.github.com/ja/actions/advanced-guides/storing-workflow-data-as-artifacts)にアップロードできるようにします。

```yaml:.github/workflows/vrt.yml
name: VRT
on: pull_request
jobs:
  vrt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install --global yarn
      - run: yarn install --frozen-lockfile

      - name: build-storybook
        run: yarn build-storybook

      - name: snapshot
        run: yarn snapshot

      - name: VRT
        id: vrt
        run: yarn vrt
        continue-on-error: true

      - name: upload report
        uses: actions/upload-artifact@v2
        with:
          name: vrt-report
          path: |
            report.html
            __actual__
            __screenshots__
            __diff__
          retention-days: 5

      - if: ${{ steps.vrt.outcome != 'success' }}
        run: exit 1
```

`upload-artifact`を使ってアップロードすることで、GitHub ActionsのSummaryからreportをダウンロードすることができるようになります。

![スクリーンショット 2021-12-01 9.01.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/f44c482f-0a93-d9ba-c699-64545a45ec07.png)

プロダクトではs3を使って確認できるようにしているのですが、時間がなく今回は省かせてください :bow:

VRTのステップで指定している`continue-on-error`を使うと、そのステップが失敗しても続けてその先のステップを実行してくれます。
また、この設定をしているとvrtが失敗したことを開発者が気づけないので、最後のステップでVRTの結果を確認して成功していなければ、エラー終了するような作りにしています。

#### スクリーンショットのアップデート
VRTの結果落ちてしまったけど、それが意図的な変更であった場合はこれをアップデートする必要があると思うので、それ用のワークフローも作成します。
スクリーンショットのアップデートは開発者の何かしらの操作によりトリガーされる必要があります。GitHub Actionsの場合、PRのコメントや`workflow_dispatch`を使った手動での実行もできるのですが、この例では`update-snapshots`というlabelがPRに追加された際にトリガーされる作りにしています。

```yaml:.github/workflows/update-snapshot.yml
name: Update snapshot
on: 
  pull_request:
    types:
      - labeled
jobs:
  update-vrt-snapshot:
    if: contains(github.event.pull_request.labels.*.name, 'update-snapshots')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{github.event.pull_request.head.ref}}

      - uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm install --global yarn
      - run: yarn install --frozen-lockfile

      - name: build-storybook
        run: yarn build-storybook

      - name: snapshot
        run: yarn snapshot

      - name: update snapshot
        run: yarn update-snapshot

      - run: |
          git config --global user.email "email" 
          git config --global user.name "username"
          git add .
          git commit -m 'update snapshot'
          git push origin HEAD:${{github.event.pull_request.head.ref}}
```

やっている内容としては、ほぼほぼ先ほどと同じで、加えて`yarn snapshot`を実行したあとに、`package.json`に記載済みの下のスクリプトを実行して、アップデートします。

```json
"update-snapshot": "reg-cli __actual__ __screenshots__ __diff__ -U"
```

そのあと、PRのブランチにpushしてワークフロー完了になります。

## おわりに
最後まで読んでいただき、ありがとうございます！
私が今回使ったソースコードは下記にまとめてあります。
https://github.com/sakamuuy/try-actions
https://github.com/sakamuuy/vrt-sample

GitHub Actions初めて触る方や、VRTやってみたいという方の参考になれば幸いです。

## 参考
- https://docs.github.com/ja/actions
- https://github.com/actions/runner