お手軽に Github Actions を解ったつもりになるための Tips

[mediba Advent Calendar 2020](https://qiita.com/advent-calendar/2020/mediba) の18日目担当の @h-sugawara です。
担当している各種プロジェクトでは、CI/CD に CircleCI というサービスを使わせて頂いています。
しかし、最近、諸事情により CircleCI から Github Actions への移行を検討していて、Github Actions の導入検証を行いました。
今回は、Github Actions で頻出して使うことが多くて調べたものを、毎度調べ直すのが面倒だったため、情報を集約する Tips を作成しました。

## Workflow Tips

### コミットにソースコードを含む開発用ブランチをプッシュした時に実行する

開発用ブランチ`feature/hogehoge`にソースコード`batch/foobar/main.go`を含むファイルをコミットして、Github へプッシュした時に、テストや構文チェックを行う Workflow を実行させたいとします。
このような場合、次の例のようにプッシュイベントのトリガーを設定します。

```yaml
on:
  push:
    branches:
      - 'feature/*'
    paths:
      - '**.go'
```

`on.push.branches`の`'feature/*'`は、`feature/`から始まるブランチが対象です。
しかし、__`*`は`/`を除く0文字以上の文字列を示すフィルタ__であるため、`feature/issue-1234/hogehoge`のようなブランチは対象外となることに注意が必要です。
これらの対象外となるブランチも含める場合は、0文字以上のすべての文字列を対象とする`**`を用いて`'feature/**'`とします。

`on.push.paths`も同様のルールで、上述の例の場合はすべての`.go`ファイルがコミットに含まれているかが対象となります。
これを、`'*.go'`と書き換えた場合は、レポジトリのルートにある`.go`ファイルだけが対象となります。

### 指定ブランチ以外をプッシュした時に実行する

特定のブランチのプッシュを無視する場合は、`on.push.branches-ignore`で指定します。
ただし、これをトリガーに設定すると Github Actions の仕様上、__タグのプッシュはすべて無視される__ことに留意しておきましょう。
タグのプッシュも含めるには、次のように`on.push.tags`に`'*'`または`'**'`を、トリガーに設定します。

```yaml
on:
  push:
    branches-ignore:
      - 'develop'
      - 'release/**'
    tags:
      - '*'
```

このようにすると、例えば、`feature/`から始まるブランチをプッシュしたり、`v1.0.0`のようなタグをプッシュしたりすると Workflow が実行されます。
反対に、`develop`ブランチや`release/`から始まるブランチをプッシュしても Workflow は実行されません。
なお、タグのプッシュが無視されても構わない場合は、`on.push.tags`の指定は不要です。

### 指定タグ以外をプッシュした時に実行する

特定のタグのプッシュを無視する場合は、`on.push.tags-ignore`で指定します。
前述の`branches-ignore`と同様に GitHub Actions の仕様により、__ブランチのプッシュはすべて無視__されます。
ブランチのプッシュも含めるには、下記のように`on.push.branches`に`'**'`を、トリガーに設定します。
※`'*'`を指定した場合、ブランチ名に`/`が含まれるとうまく動かないため、`'**'`の利用を推奨。

```yaml
on:
  push:
    branches:
      - '**'
    tags-ignore:
      - 'dev-*'
      - 'stg-*'
```

この設定例では、`develop`ブランチや、`feature/`または`release/`などから始まるブランチをプッシュしたり、`v1.0.0`のようなタグをプッシュしたりすると Workflow が実行されます。
反対に、`dev-`や`stg-`で始まるタグがプッシュされた場合は、Workflow は実行されません。
もちろん、特定のタグ以外のタグのプッシュを対象とするだけなら、`on.push.branches`は不要です。

### プルリクエストがマージされた時に実行する

プルリクエストのマージをトリガーとする Workflow を作成する時は、次の2点に注意が必要です。

1. compare ブランチ（マージ元）ではなく、base ブランチ（マージ先）が対象である
2. プルリクエストイベントのアクティビティタイプにはマージされたことを示すものはない

そのため、以下の例のように、`on.pull_request.types`が`closed`であり、`job`などで`github.event.pull_request.merged`が`true`であることを確かめる必要があります。

```yaml
on:
  pull_request:
    branches:
      - my_base_branch
    types:
      - closed

jobs:
  my_job_name:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    steps:
      - name: my_step_name
        run: echo "Hello world!"
```

### renovate などのツールやプルリクエストの作成などを定期的に実行する

```yaml
on:
  schedule:
    - cron: '*/15 * * * *'
```

## Job Tips

### 2つ以上前に実行された Job の出力値を参照する

例えば、`first_job`、`second_job`、`last_job`の順で Job を実行するとします。
この時、`last_job`の`needs`に、直前に実行されることを意図して`second_job`だけを指定すると、Job の実行順はその通りになります。
しかし、`second_job`の前に実行された`first_job`の出力値は参照できません。
なぜなら、Job が参照できる出力値は、依存関係によって以前に実行したすべての Job ではなく、`needs`で指定した Job のみだからです。
そのため、実行する Job から見て依存関係にあるすべての Jobを `needs`に指定する必要があります。

```yaml
jobs:
  first_job:
    runs-on: ubuntu-latest
    outputs:
      first_val: ${{ steps.echo_step.outputs.first }}
    steps:
      name: set output
      id: echo_step
      run: echo "::set-output name=first::hogehoge"

  second_job:
    runs-on: ubuntu-latest
    needs: [first_job]
    outputs:
      second_val: ${{ steps.echo_step.outputs.second }}
    steps:
      name: set output
      id: echo_step
      run: echo "::set-output name=second::foobar"

  last_job:
    runs-on: ubuntu-latest
    needs: [first_job, second_job]
    steps:
      name: echo before job output
      run: |
        echo "${{ needs.first_job.outputs.first_val }}"
        echo "${{ needs.second_job.outputs.second_val }}"   
```

### 直前の Job の結果が success 以外でも必ず実行する

Job の実行条件は、`if: success()`（成功時のみ実行）がデフォルトで設定されています。
そのため、`jobs.<job_id>.needs`で指定した Job のいずれか一つでも実行結果が`success`以外になった場合、`<job_id>`の実行はスキップされます。
必ず実行させる必要がある Job には、下記のように`if: always()`を指定しましょう。

```yaml
jobs:
  first_job:
    runs-on: ubuntu-latest
    steps:
      name: failure job
      run: exit 1

  second_job:
    runs-on: ubuntu-latest
    steps:
      name: success job
      run: exit 0

  last_job:
    runs-on: ubuntu-latest
    needs: [first_job, second_job]
    if: always()
    steps:
      name: echo
      run: echo "running second_job."   
```

この方法は、テストやビルド、デプロイなどの Job の後に、Slack などでそれらの実行結果を通知する Job を実行する場合に活用できます。