CircleCIのワークフローを書き換えながら学ぶGitHub Actions入門

## はじめに

最近、CircleCIのワークフローの一部をGitHub Actionsに書き換えました。

同様の作業をこれから行う人に向けて、本記事では

「最初にこのあたりを押さえておけば割とスムースに書き換えができるのでは？」

と思われるポイントをまとめました。

本記事では、以下の観点でCircleCIとGitHub Actionsを比較しながら、GitHub Actionsのワークフローファイルの書き方を簡単に解説していきます。

- ワークフローファイルのパス
- ジョブの構成
- ソースコードのチェックアウト
- 任意のコマンドの実行
- キャッシュ
- 環境変数

本記事で触れられていない点に関しては、[公式のリファレンス](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions)を見るか、Boothで販売されている

- [GitHub Actions 実践入門](https://booth.pm/ja/items/1865906)

が非常におすすめですので、そちらを読んでいただければと思います。

(私個人の力量ではActionsを理解するのに公式リファレンスだけでは辛かったので、上記の本には非常にお世話になりました。CircleCIの方は、公式リファレンスだけでとても分かりやすいんですけどね。。。)

## 1. ワークフローファイルのパス

CircleCI, GitHub Actionsそれぞれのワークフローファイルのパスは以下になります。

|CircleCI|GitHub Actions|
|:---|:---|
|.circleci/config.yml|.github/workflows/test.yml<br>.github/workflows/deploy.yml<br>など任意の名前|

## 2. ジョブの構成

### 2.1. CircleCIの場合 

CircleCIでは、`jobs`でジョブを定義し、`workflows`でジョブの実行順や条件を定義しています。

```yaml:.circleci/config.yml
version: 2.1
jobs:
  build:
    # 略
  test:
    # 略
  deploy:
    # 略

workflows:
  version: 2
  build-test-deploy:
    jobs:
      - build
      - test:
          requires:
            - build
      - deploy:
          requires:
            - test
          filters:
            branches:
              only:
                - master          
```

`requires`で、先行ジョブを定義します。

その他、今回のサンプルでは`filters`, `branches`, `only`を使って、`master`ブランチでのみ`deploy`ジョブが動くようにしています。

- [requires - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#requires)
- [filters - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#filters-1)

### 2.2. GitHub Actionsの場合 その1

Actionsでは、CircleCIでいうところの`workflows`にあたるものは無いようなので`jobs`だけでジョブの実行順などを定義していきます。

```yaml:.github/workflows/build-test.yml
name: build-test

on: [pull_request] # on: pull_request でも良い（指定したいイベントがひとつだけなら）

jobs:
  build:
    # 略
  test:
    needs: [build] # needs: build でも良い(指定したいジョブがひとつだけなら)
    # 略
```

```yaml:.github/workflows/build-test-deploy.yml
name: build-test-deploy

on:
  push: # イベントに対して更に条件を追加する場合は、[]を使った指定はできない
    branches:
      - master

jobs:
  build:
    # 略
  test:
    needs: [build]
    # 略
  deploy:
    needs: [test]
    # 略
```

#### 2.2.1. on 

`on`では、Actionsを動かすトリガーとなるイベントを定義できるので、今回のサンプルでは

- PRを立てた時に`build`, `test`ジョブを動かす(このPRのブランチにpushした時にも動きます)

- `master`ブランチにpushした時に、`build`, `test`, `deploy`ジョブを動かす(PRを`master`ブランチにマージした時も動きます)

としました。

- [on - Workflow syntax for GitHub Actions](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#on)

#### 2.2.2. needs

`needs`で先行ジョブを定義しています。

- [needs - Workflow syntax for GitHub Actions](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idneeds)


### 2.3. GitHub Actionsの場合 その2

先ほどの例ではワークフローを2つに分けていますが、`build`, `test`ジョブの内容が同じであれば記述として冗長になってしまいます。

ワークフローを分けないのであれば、以下の構成が考えられるかと思います。

```yaml:.github/workflows/build-test-deploy.yml
name: build-test-deploy

on: 
  pull_request:
  push:
    branches:
      - master # masterにマージした時にもワークフローを動かすため

jobs:
  build:
    # 略
  test:
    needs: [build]
    # 略
  deploy:
    needs: [test]
    if: github.ref == 'refs/heads/master' # masterにマージした時のみdeployジョブを動かすため
    # 略
```

#### 2.3.1. if

GitHub Actionsでは、ジョブやステップに対し、`if`で実行条件を定義できます。

この`if`を使って、`deploy`ジョブが動くのを、masterにマージした時のみとしています。

- [if - Workflow syntax for GitHub Actions](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idif)


#### 2.3.2. github.ref / github.head_ref / github.base_ref

今回は、`github.ref`というコンテキストを利用して、masterマージ時のみ`deploy`ジョブが動くようにしました。

似たようなコンテキストとして、`github.head_ref`, `github.base_ref`があります。

その値については`refs/...`が先頭に付いたり付かなかったりするので、状況別にまとめました。参考にしてください。

|プロパティ|プルリクエスト時の値|masterへマージした時の値
|:---|:---|:---|
|github.ref|refs/pull/PR番号/merge|refs/heads/master|
|github.head_ref|PRのブランチ名<br>(feature/hoge など。refs/...は付かない。)|無し|
|github.base_ref|PRでマージする予定のブランチ名<br>(master など。refs/...は付かない。)|無し|

- [Context and expression syntax for GitHub Actions](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#contexts)

## 3. ソースコードのチェックアウト

### 3.1. CircleCIの場合 

CircleCIでは、`checkout`コマンドでGitHubのソースコードをチェックアウトします。

```yaml:.circleci/config.yml
version: 2.1
executors:
  default:
    docker: # 略
jobs:
  build:
   executor: default
   steps:
     - checkout
   # 略
```

- [checkout - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#checkout)

### 3.2. GitHub Actionsの場合 

GitHub Actionsでは、`actions/checkout`を使用してGitHubのソースコードをチェックアウトします。

```.github/workflows/xxx.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
```

- https://github.com/actions/checkout

### 3.3. git branchおよびgit logの結果の違い

CircleCIの`checkout`コマンドと、GitHub Actionsの`actions/checkout`では、チェックアウトのされ方に違いがあります。

CircleCIのチェックアウト後に、`git branch`と`git log`を実施した結果の例は以下になります。

```
$ git branch
* feature/hogeなどPRのブランチ名
  master
```

```
$ git log --oneline -5
xxxxxxx (HEAD -> feature/hoge, origin/feature/hoge, master) コミットメッセージ
xxxxxxx コミットメッセージ
xxxxxxx コミットメッセージ
xxxxxxx コミットメッセージ
xxxxxxx コミットメッセージ
```

対して、GitHub Actionsでのチェックアウト後に、`git branch`と`git log`を実施した結果の例は以下になります。

```
$ git branch
* (HEAD detached at pull/PR番号/merge)
```

```
$ git log --oneline -5
xxxxxxx Merge プルリクエストのHEADのコミットハッシュ into masterブランチのコミットハッシュ
```

GitHub Actionsでのチェックアウトではデフォルトで`--depth=1`が指定されているので、CircleCIのチェックアウトのように過去のコミット履歴は取得できません。

そのため、例えば

```
$ git diff --name-only origin/master...HEAD
```

で、masterとプルリクエスト間で差分のあるファイル名を出力しようとしても、以下のエラーになります。

```
fatal: ambiguous argument 'origin/master...HEAD': unknown revision or path not in the working tree.
```

もし、GitHub Actionsでも過去のコミット履歴を利用して何らかの処理をしたい場合は、チェックアウト後に追加のステップで`git fetch --prune --unshallow`を行えば良いかと思います。

```.github/workflows/xxx.yml
    steps:
      - uses: actions/checkout@v2
      - run: git fetch --prune --unshallow
```

- [Fetch all history for all tags and branches - actions/checkout](https://github.com/actions/checkout#fetch-all-history-for-all-tags-and-branches)

## 4. 任意のコマンドの実行

### 4.1. CircleCIの場合 

`run`でコマンドを実行します。

```yaml:.circleci/config.yml
   steps:
     - run: echo "Hello World"       
```

`name`などを定義する場合は、以下の記述になります。

```yaml:.circleci/config.yml
   steps:
     - run:
         name: Greeting # CircleCIの実行結果の画面にステップ名として表示される
         command: echo "Hello World"       
```

複数コマンドを実行する場合は、以下の記述になります。

```yaml:.circleci/config.yml
   steps:
     - run: |
        echo "One"       
        echo "Two"
```

```yaml:.circleci/config.yml
   steps:
     - run:
         name: Count
         command: |      
           echo "One"       
           echo "Two"
```

- [run - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#run)

### 4.2. GitHub Actionsの場合 

GitHub Actionsも`run`でコマンドを実行します。

```.github/workflows/xxx.yml
   steps:
     - run: echo "Hello World"       
```

`name`などを定義する場合は、以下の記述になります。

```.github/workflows/xxx.yml
   steps:
     - name: Greeting # GitHub Actionsの実行結果の画面にステップ名として表示される
       run : echo "Hello World"       
```

複数コマンドを実行する場合は、以下の記述になります。

```.github/workflows/xxx.yml
   steps:
     - name: Count
       run: |      
         echo "One"       
         echo "Two"
```


## 5. キャッシュ

### 5.1. CircleCIの場合 

CircleCIでは、`restore_cache`と`save_cache`コマンドでキャッシュのリストアと保存を行います。

```yaml:.circleci/config.yml
# 略
    steps:
      # 略
      - restore_cache:
          key: npm-v1-{{ checksum "package-lock.json" }}
      - run:
          name: if there is no node_modules, npm ci
          command: |
            if [ ! -d node_modules ]; then
              npm ci
            fi
      - save_cache:
          key: npm-v1-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
 ```

- [restore_cache - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#restore_cache)
- [save_cache - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#save_cache)


### 5.2. GitHub Actionsの場合 

GitHub Actionsでは、`actions/cache`でキャッシュのリストアと保存を行います。

```.github/workflows/xxx.yml
# 略
    steps:
      # 略
      - name: cache npm
        uses: actions/cache@v1.1.2
        with:
          path: node_modules
          key: npm-v1-${{ hashFiles('package-lock.json') }}
      - name: if there is no node_modules, npm ci
        run: |
          if [ ! -d node_modules ]; then
            npm ci
          fi
```

なお、上記のサンプルでは、キャッシュから`node_modules`を復元できたかどうかを`node_modules`の有無で判定し、復元できている場合は`npm ci`を実行しないようにしています。

`actions/cache`ではキャッシュヒットした場合に、`cache-hit`に`'true'`を出力するので、これを利用する場合は以下のように書き直せます。

```.github/workflows/xxx.yml
# 略
    steps:
      # 略
      - name: cache npm
        id: cache-npm # ステップを特定するためのidを定義
        uses: actions/cache@v1.1.2
        with:
          path: node_modules
          key: npm-v1-${{ hashFiles('package-lock.json') }}
      - name: npm ci
        if: steps.cache-npm.outputs.cache-hit != 'true' # キャッシュヒットした時のみnpm ciを実行する
        run: npm ci
```

- https://github.com/actions/cache

#### 5.2.1. キャッシュの保存のタイミングについて

CircleCIではキャッシュのリストア/保存のステップをそれぞれ記述しましたが、GitHub Actionsではキャッシュをリストアしたいタイミングに`actions/cache`を使用するステップを記述するのみとなり、保存のステップは記述しません。

キャッシュの保存は、`actions/cache`ステップが存在するジョブの最後に`Post + actions/cacheのステップ名`というステップ名で実行されます。

ただし、**ジョブが正常終了しなかった場合は、キャッシュを保存するステップは実行されません。**

そのため、ビルドのためのジョブを存在させず、テストジョブ内で

- `actions/cache`の利用とビルド
- テスト

を行なっていると、テストが通らない限り、キャッシュが保存されません。

```.github/workflows/xxx.yml
# 略
jobs:
  # build: # ビルドジョブ無し
  test:
    steps:
      # 略
      - name: cache npm # テストが通らないと、ジョブの最後のキャッシュ保存を行なってくれない
        # 略
      - name: npm ci
        # 略
      - name: npm test
        run: npm test
```

もし、これを防ぎたい場合は、ビルドのためのジョブを定義し、ビルドジョブとテストジョブそれぞれで、キャッシュの`key`は同じにした上で、`actions/cache`の利用とビルドを行えば良いかと思います。

```.github/workflows/xxx.yml
# 略
jobs:
  build:
    steps:
      # 略
      - name: cache npm
        id: cache-npm
        uses: actions/cache@v1.1.2
        with:
          path: node_modules
          key: npm-v1-${{ hashFiles('package-lock.json') }}
      - name: npm ci
        # 略
  test:
    needs: [build]
    steps:
      # 略
      - name: cache npm
        id: cache-npm
        uses: actions/cache@v1.1.2
        with:
          path: node_modules
          key: npm-v1-${{ hashFiles('package-lock.json') }}
      - name: npm ci
        # 略
      - name: npm test
        run: npm test
```

なお、キャッシュの保存のステップは以下のような処理結果になります。

```
Cache saved successfully
Post job cleanup.
/bin/tar -cz -f /home/runner/work/_temp/9fa1f088-a867-4efe-80eb- 
132944ceba50/cache.tgz -C /home/runner/work/リポジトリ名/リポジトリ名/node_modules .
Cache saved successfully
```

リストア時にキャッシュヒットした場合は、キャッシュは上書き保存されません。

```
Post job cleanup.
Cache hit occurred on the primary key npm-v1-433b1cb5f6b93173ee6214d276272b0b7fc0c4c8b3e0e3770d29c98035416d4e, not saving cache.
```

#### 5.2.2. 単一ファイルのキャッシュ

CircleCIでは、`paths`にディレクトリだけでなくファイル名を指定し、それをキャッシュすることができますが、`actions/cache`ではディレクトリしかキャッシュできません。

GitHub Actionsで単一のファイルをキャッシュする場合は、退避用のディレクトリを作って、そこにファイルをコピーした上でディレクトリごとキャッシュし、リストアされた時にはそのディレクトリから本来のパスにコピーする、といった作業が必要になるかと思います。

## 6. 環境変数

### 6.1. CircleCIの場合 

CircleCIでは、`Project Settings`の`Environment Variables`画面で環境変数を設定します。

![CircleCI Project Settings Environment Variables](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/47700096-7875-8abf-340b-80835698acbf.png)

ワークフローファイル上で、環境変数を新規に設定する場合は、`environment`に記述します。

```yaml:.circleci/config.yml
version: 2.1
jobs:
  # 略
  deploy:
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
    # 略
```

前述の画面で設定した環境変数の値を上書きする場合も同様の方法となります。

- [environment - CircleCI](https://circleci.com/docs/ja/2.0/configuration-reference/#environment)

### 6.2. GitHub Actionsの場合

GitHub Actionsでは、GitHubの`Settings`の`Secrets`画面で、いったん秘密情報として設定します。

![GitHub Settings Secrets](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/cb197fdf-c01b-cc5b-9efd-80a9533ad491.png)

その上で、ワークフローファイルで、`env`に改めて環境変数として設定します。

秘密情報は、`${{ secrets.秘密情報のキー }}`と記述することで取り出せます。

```yaml:.github/workflows/xxx.yml
# 略
jobs:
  # 略
  deploy:
    needs: [test]
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: ap-northeast-1    
```

なお、`env`はワークフロー全体やステップ単位に適用することもできます。

```yaml:.github/workflows/xxx.yml
# 略
env:
  # 略

jobs:
  # 略
```

```yaml:.github/workflows/xxx.yml
# 略
jobs:
  steps:
    - name: s3 upload
      env:
        # 略
      run: # 略
```

- [Creating and storing encrypted secrets - GitHub Help](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets)

## おまけ(SSH)

GitHub Actionsの実行環境にSSHで入って調査したりデバッグをしたい場合は、以下の記事を参考にしてください。

- [GitHub ActionsにSSHで入る(簡単3ステップ) - Qiita](https://qiita.com/shonansurvivors/items/cb8902acfe5c3a1b3ca0)