Github Actionsの作り方（Docker編）

以前、[checkovをreviewdogに対応させた話](https://qiita.com/ishii1648/items/4878b01823113b50128d)で`terraform`のコードリポジトリで静的解析を実行する仕組みを作りましたが、それを`Github Actions`として世に公開していこうと思います。

## Github Actionsの種類
`Github Actions`には`Private Action`と`Public Action`の2種類があります。

`Private Action`はCIを実行するリポジトリのなかに直接埋め込まれているタイプで、以下のようなディレクトリ構成になっています。

```
├ .github/
　├ actions/
    └ action.yml
    └ Dockerfile
    └ entrypoint.sh
　├ workflows/
　 　└ main.yml
... 以下略
```

`Private Action`は`./github/workflows/main.yaml`で以下のように相対パスで呼び出すことができます。

```
- uses: ./.github/actions/tflint-reviewdog
  with:
    github_token: ${{ secrets.github_token }}
    reporter: github-pr-check
    fail_on_error: "true"
    filter_mode: "nofilter"
```

一方で`Public Action`は専用のリポジトリとして作成され、別のリポジトリから呼び出すことができます。

`Public Action`は一度作ってしまえば複数のリポジトリで使いまわしが出来きるという点で便利です。今回は`Public Action`の作り方を説明していきます。

## ディレクトリ構成

今回作成する`Public Action`のディレクトリ構成は以下の通りです。

```
├ .github/
　├ workflows/
　 　└ test.yml
　├ testdata/
　 　└ example.tf
└ action.yml
└ Dockerfile
└ entrypoint.sh
└ parse.py
└ requirements.txt
```

https://github.com/ishii1648/action-checkov-reviewdog

## action.yml

`Github Action`を作るえうで必ず必要になるファイルです。とは言え設定はとても簡単です。
設定項目は主に引数と動作環境の指定です。

以下の内容ですと、引数としてリポジトリを操作するために必要な`github_token`に加えて、`checkov`と`reviewdog`というツールのオプション値を引数として受け取れるように設定しています。

また、`using: 'docker'`でコンテナとして動作するように指定しています（コンテナ以外にも`Javascript`などで動作させることもできます）。

```yaml
name: 'Checkov with Reviewdog GitHub Action'
author: 'Ishii1648'
description: 'Run Checkov with Reviewdog against Terraform/CloudFormation infrastructure code, as a pre-packaged GitHub Action.'
inputs:
  github_token:
    description: 'GITHUB_TOKEN'
    required: true
  reporter:
    description: |
      Reporter of reviewdog command [github-pr-check,github-pr-review].
      Default is github-pr-check.
    default: 'github-pr-check'
  filter_mode:
    description: |
      Filtering for the reviewdog command [added,diff_context,file,nofilter].
      Default is added.
    default: 'added'
  fail_on_error:
    description: |
      Exit code for reviewdog when errors are found [true,false]
      Default is `false`.
    default: 'false'
  working_directory:
    description: |
      Directory to run the action on, from the repo root.
      Default is . ( root of the repository)
    default: '.'
  skip_check:
    description: 'Run scan on all checks but a specific check identifier (comma separated)'
    required: false

runs:
  using: 'docker'
  image: 'Dockerfile'
  env:
    INPUT_GITHUB_TOKEN: ${{ inputs.github_token }}
    INPUT_REPORTER: ${{ inputs.reporter }}
    INPUT_FILTER_MODE: ${{ inputs.filter_mode }}
    INPUT_FAIL_ON_ERROR: ${{ inputs.fail_on_error }}
    INPUT_WORKING_DIRECTORY: ${{ inputs.working_directory }}
    INPUT_SKIP_CHECK: ${{ inputs.skip_check }}
```

## Dockerfile

Dockerfileでは`reviewdog`と`checkov`をインストールしています。加えて`checkov`の出力結果を`reviewdog`で受取可能な形式に変換する`parse.py`という自作スクリプトも入れています。

```dockerfile
FROM python:slim

ARG REVIEWDOG_VERSION="v0.11.0"
ENV PATH $PATH:/usr/local/bin

RUN apt-get update && \
    apt install -y git curl

RUN curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s -- -b "/usr/local/bin/" "${REVIEWDOG_VERSION}" 2>&1

COPY requirements.txt /requirements.txt
COPY parse.py /parse.py
COPY entrypoint.sh /entrypoint.sh

RUN pip install -r /requirements.txt

ENTRYPOINT ["/entrypoint.sh"]
```

## entrypoint.sh

コンテナ起動時に実行されるスクリプトです。特徴としてはパイプで連結している3つのスクリプト（checkov, parse.py, reviewdog）の返り値をそれぞれ変数に格納している点です。

何故わざわざこのような事をしているのかと言うと、後述するテストで各スクリプトの返り値を元にテスト結果の成否を判定するためです。

```sh
#!/bin/bash

[[ ! -z "$INPUT_SKIP_CHECK" ]] && SKIP_CHECK_FLAG="--skip-check $INPUT_SKIP_CHECK"

export REVIEWDOG_GITHUB_API_TOKEN="${INPUT_GITHUB_TOKEN}"

checkov -d $INPUT_WORKING_DIRECTORY $SKIP_CHECK_FLAG -o json \
    | python3 /parse.py \
    | reviewdog -efm="%f:%l: %m" -name="checkov" -reporter="${INPUT_REPORTER}" -fail-on-error="${INPUT_FAIL_ON_ERROR}" -filter-mode="${INPUT_FILTER_MODE}"

checkov_return="${PIPESTATUS[0]}" reviewdog_return="${PIPESTATUS[2]}" exit_code=$?
echo ::set-output name=checkov-return-code::"${checkov_return}"
echo ::set-output name=reviewdog-return-code::"${reviewdog_return}"

exit $exit_code
```

## .github/workflows/test.yml

テストのためにPR作成時とmainブランチへのマージ時にアクションが実行されるよう設定しています。
ここでは`reviewdog`の3つの出力パターンをそれぞれ実行するよう設定しており、`name: Check return codes`で成否を判定しています。

```yaml
name: Test
on:
  push:
    branches:
      - main
  pull_request:
jobs:
  test-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./
        id: test
        continue-on-error: true
        with:
          github_token: ${{ secrets.github_token }}
          reporter: github-pr-check
          working_directory: testdata
      # The check is expected to fail on the test data
      - name: Check return codes
        if: success() || failure ()
        run: |
          checkov_return="${{ steps.test.outputs.checkov-return-code }}"
          reviewdog_return="${{ steps.test.outputs.reviewdog-return-code }}"
          if [ "$checkov_return" -eq 1 ]; then
            echo "checkov correctly returned failure ${checkov_return}"
          else
            echo "checkov returned ${checkov_return}, expected '1'. Failing..."
            exit 1
          fi
          if [ "$reviewdog_return" -eq 0 ]; then
            echo "reviewdog correctly returned success: ${reviewdog_return}"
          else
            echo "reviewdog returned ${reviewdog_return}, expected '0'. Failing..."
            exit 1
          fi
  test-pr-check:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./
        continue-on-error: true
        id: test
        with:
          github_token: ${{ secrets.github_token }}
          reporter: github-pr-check
          working_directory: testdata
      # The check is expected to fail on the test data
      - name: Check return codes
        if: success() || failure ()
        run: |
          checkov_return="${{ steps.test.outputs.checkov-return-code }}"
          reviewdog_return="${{ steps.test.outputs.reviewdog-return-code }}"
          if [ "$checkov_return" -eq 1 ]; then
            echo "checkov correctly returned failure ${checkov_return}"
          else
            echo "checkov returned ${checkov_return}, expected '1'. Failing..."
            exit 1
          fi
          if [ "$reviewdog_return" -eq 0 ]; then
            echo "reviewdog correctly returned success: ${reviewdog_return}"
          else
            echo "reviewdog returned ${reviewdog_return}, expected '0'. Failing..."
            exit 1
          fi
  test-pr-review:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ./
        continue-on-error: true
        id: test
        with:
          github_token: ${{ secrets.github_token }}
          reporter: github-pr-review
          working_directory: testdata
      # The check is expected to fail on the test data
      - name: Check return codes
        if: success() || failure ()
        run: |
          checkov_return="${{ steps.test.outputs.checkov-return-code }}"
          reviewdog_return="${{ steps.test.outputs.reviewdog-return-code }}"
          if [ "$checkov_return" -eq 1 ]; then
            echo "checkov correctly returned failure ${checkov_return}"
          else
            echo "checkov returned ${checkov_return}, expected '1'. Failing..."
            exit 1
          fi
          if [ "$reviewdog_return" -eq 0 ]; then
            echo "reviewdog correctly returned success: ${reviewdog_return}"
          else
            echo "reviewdog returned ${reviewdog_return}, expected '0'. Failing..."
            exit 1
          fi
```

以上が`Github Actionsの作り方（Docker編）`になります。

`Github Actions`はとても簡単に作成することができ、かつCIとして必要な機能も充分に備えているのでこれからどんどん使っていこうと思います。