【GitHub Actions】ワークフローはなんとなく書けるけど構造や仕組みの理解が曖昧な方々向けの記事

最近、現場でGitHub Actionsを使い始めました。CI/CDってなんやねんというレベルだったので、基礎から学んで簡単なワークフローをいろいろつくって勉強してみることにしました。

#GitHub Actionsって何?
GitHub上のリポジトリやイシューに対するさまざまな操作(Push, Pull Request, 外部イベントなど)や指定したスケジュールをトリガとして、あらかじめ定義した処理(Workflow)を実行する機能です。今までは自動テストや自動ビルドを実施するのにCircle CIやTravis CIなどの外部サービスとの連携が必要でしたが、GitHub Actionsを使うことでGitHubだけで実現することができます。

WorkflowはJobという単位にわけられ、仮想マシンのインスタンス上で実行されます。JobはさらにStepという単位にわけられ、コマンドや既存の処理(アクション)が実行されます。
![スクリーンショット 2021-03-24 7.57.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/a76d56d0-5071-45a4-3eeb-27fae36621e1.png)

Jobごとに仮想マシンをたてることができるので、それぞれの環境に適した処理を実行することができます。
![スクリーンショット 2021-03-24 20.59.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c935d707-c7f9-6f79-a712-36dbaf4481ad.png)

##Runnerって何?
Runnerはトリガとなるイベントが起こったときにJobを実行するためのアプリケーションです。GitHubに標準装備されているもの(GitHub-hosted Runner)と、自分でカスタマイズして実装するもの(Self-hosted Runner)があります。それぞれ以下の特徴をもっています。

###GitHub-hosted Runner
- ソフトウェアがプリインストールされたLinux, Windows, MacOSの仮想環境が提供される
- GitHubで管理される(ユーザによるハードウェアのカスタマイズは不可)

プリインストールされているソフトウェアには以下のものがあります。

- curl, git, npm, yarn, pipなどのツール
- Python, Ruby, Node.jsなどの言語
- Android SDK, XCodeなどの開発キット

###Self-hosted Runner
- Runnerアプリケーションがインストールされたマシンをユーザが管理する
- ハードウェア、OS、ソフトウェアツールをユーザが自由にカスタマイズできる(ex.メモリ増設、hosted-Runnerで使えないOSを使用)

#ワークフローの作成
簡単なワークフローをいろいろつくって挙動やオプションを学んでいきます。

##Hello World
実際にHello Worldを出力(Step1)して、nodeとnpmのバージョンを表示(Step2)するような簡単なワークフローをつくってみます。ワークフローはyml形式で以下のように記述することができます。

```simple.yml
name: Shell Commands #ワークフローの名前

on: [push] #ワークフロー実行のトリガーとなるイベント

jobs: #ワークフローで実行するジョブ(複数記述可能)
  run-shell-command: #ジョブの名前
    runs-on: ubuntu-latest #jobを実行するVMのOSを指定
    steps: #jobの実行内容
      - name: echo a string
        run: echo "Hello World"
      - name: multiline script 
        run: |
          node -v 
          npm -v
```

ワークフローのファイルは`プロジェクト/.github/workflow`の直下に保存します。リモートリポジトリをつくって`git push`すると、Actionタブでワークフローの状態を確認することができます。緑のチェックがついていれば成功です。
![スクリーンショット 2021-03-21 15.12.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/99629560-d7de-700a-e580-0b9be7aed484.png)

###Runner診断ログ
Settingsタブのsecretsに`ACTIONS_RUNNER_DEBUG`, `ACTIONS_STEP_DEBUG`をtrueで保存すると、Jobの実行状態に関する情報を含む追加のログファイルが2つ提供されます。
![スクリーンショット 2021-03-21 15.18.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/b3d17150-248b-ab24-4af5-b0c9eda95574.png)

##シェルコマンドの実行
`shell`を追加することで、runステップに対するデフォルトのシェルを指定することができます。記述場所によってWorkflow単位, Job単位, Step単位で影響範囲を変えることができます。

以下ではPythonシェルを指定して、runステップでコマンドを実行しています。

```yml
      - name: python Command
        run: |
          import platform
          print(platform.processor())
        shell: python
```
![スクリーンショット 2021-03-21 15.34.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/640da407-a27c-dc94-7b5d-04d3bfbe9970.png)

次にwinOSの仮想マシン(VM)で新たなジョブを作って、PowerShellとbashを実行してみます。

```yml
  run-windows-commands:
    runs-on: windows-latest #ジョブを実行するVMのOSをwindowsに指定
    needs: ['run-shell-command'] #run-shell-commandのジョブが成功したときだけ実行
    steps:
      - name: Directory PowerShell
        run: Get-Location #winOSなのでPowerShellを実行
      - name: Directory Bash
        run: pwd #bashのpwdコマンドを実行
        shell: bash #Directory Bashのshellをbashに指定
```
winOSのデフォルトシェルがPowerShellなので、Directory PowerShellのステップでは`D:\a\github-actions-test\github-actions-test`が出力されます。Directory Bashのステップではbashを指定しているので、`/d/a/github-actions-test/github-actions-test`が出力されます。

また、複数のジョブはデフォルトで並列に実行されますが、needsを指定することで別のジョブが成功したときだけ実行することができます。

##アクションの実行
ステップでは指定のコマンドを実行するだけではなく、`uses`でパッケージ化された処理(アクション)を読み込んで実行することもできます。
まずはHello Worldを行うアクション(`hello-world-javascript-action`)を読み込んで実行してみます。

https://github.com/actions/hello-world-javascript-action

```actions.yml
name: Actions Workflow

on: [push]

jobs: 
  run-github-actions:
    runs-on: ubuntu-latest
    steps:
      - name: Simple JS Action
        id: greet #別のstepで読み込むためのid
        uses: actions/hello-world-javascript-action@v1 #使用したい外部のアクション([ユーザ名/リポジトリ名]@[ブランチ名])
        with: 
          who-to-greet: John
      - name: Log Greeting Time
        run: echo "${{ steps.greet.outputs.time }}" #greetのstepを実行した時間を呼出
```

Simple JS Actionのステップでアクションを読み込んでHello Worldを実行し、Log Greeting Timeのステップで前ステップを実行した時間を呼び出しています。

![スクリーンショット 2021-03-21 19.29.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/df885e5f-322c-d62d-aeba-287e5ef24791.png)

![スクリーンショット 2021-03-21 19.29.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/4bb1c50c-91ad-1781-c0a0-2e6d5f4215ed.png)

#Checkoutって何?
簡単なワークフローをいろいろつくりましたが、実際にワークフローは仮想マシンのどのディレクトリで実行されているのでしょうか。
確認のために以下のステップを追加して実行してみます。

```yml
      - name: List Files
        run: |
          pwd
          ls
```

pwdの結果は`/home/runner/work/github-actions-test/github-actions-test`となり、ワークフローを実行するためのフォルダ`github-actions-test`が作成されていることがわかります。

一方、lsの結果は空で`github-actions-test`フォルダにはファイルが存在していません。これはデフォルトでファイルを仮想マシンのディレクトリにクローンしないようになっているためです。

ただ、このままではワークフローでテストやビルドを実施することができません。
このときに使用するのが、`actions/checkout`というアクションです。これを使うことにより、ワークフローが実行されたときにリポジトリがトリガとなったコミットにチェックアウトされて、仮想マシンのディレクトリにクローンされます。

https://github.com/actions/checkout

```yml
      - name: List Files
        run: |
          pwd
          ls -a
      - name: Checkout
        uses: actions/checkout@v2
      - name: List Files After Checkout
        run: |
          pwd
          ls -a
```

Checkoutステップ後に仮想マシン上のディレクトリを確認すると、リポジトリがちゃんとクローンされています。
![スクリーンショット 2021-03-21 19.57.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/a680e69b-2f78-2349-0cc2-4fa62d3580b2.png)

また、このアクションをGitコマンドに置き換えると以下のようになります。

```
          git clone git@github:$GITHUB_REPOSITORY
          git checkout $GITHUB_SHA
```

`$GITHUB_REPOSITORY`や`$GITHUB_SHA`はGitHubでデフォルトで用意されている環境変数です。それぞれ、ワークフローが実行されるリポジトリとワークフローをトリガしたコミットSHA(ID)を表しています。

変数の中身はそれぞれ以下のように出力されます。

```yml
      - name: List Files
        run: |
          pwd
          ls -a
          echo $GITHUB_SHA #最新のコミットID
          echo $GITHUB_REPOSITORY #ユーザ名/リポジトリ名
          echo $GITHUB_WORKSPACE #VMのディレクトリ
          echo "${{ github.token }}" #GitHubのトークン
```
![スクリーンショット 2021-03-21 20.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/6630a5f3-09a2-11bc-f9be-85c7517b97d1.png)


#ワークフローのトリガの種類
ここまでは`git push`をトリガとしたワークフローを取り扱ってきましたが、push以外のトリガとなるイベントについてまとめました。


##Git操作のイベント
以下はプルリクエストをトリガとするイベントの記述です。typesの中でプルリクがcloseしたときやopenしたときのイベントを指定することができます。

```yml
  pull_request:
    types: [closed, assigned, opened, reopened]
```

##スケジュール
1日ごと1ヶ月ごとなど指定したスケジュールでワークフローを実行することができます。Crontab.guruというエディタを使うと簡単にcronの記述を行うことができます。

```yml
  schedule:
    - cron: "0/5 * * * *"
```

https://crontab.guru/

##dispatch event
dispatch eventは手動のトリガによってワークフローを実行させる手段です。

以下の記述によって、buildというイベントのリクエストがリポジトリに送られると、ワークフローが実行されるようになります。

```yml
  repository_dispatch:
    types: [build]
```
試しにPostmanでリクエストを送ってみます。

Bodyに`event_type`を記述します。
![スクリーンショット 2021-03-22 21.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/69a51cdc-ce12-dad1-f2d1-286fb79eefd9.png)

リクエスト投げるにはトークンが必要なので、GitHubのDeveloper settingsから新しく作成します。
![スクリーンショット 2021-03-22 21.43.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/633157a8-c017-5362-54c9-e6ef1071d38a.png)

PostmanのAuthorizationタブでPasswordにトークンを入力してSendを押し、何も表示されなければリクエストの送信は成功です。
![スクリーンショット 2021-03-22 21.45.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/195c2999-f6dc-920f-f9c3-4c69db15086d.png)

GitHubのActionsタブをみると、ワークフローが確かに実行されています。
![スクリーンショット 2021-03-22 21.49.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/97bccc18-6d0b-ec4a-b45b-aeb0d3054fa5.png)


#ワークフローのフィルタリング
pushやpull_requestのイベントが起こった際、特定のブランチ、タグ(コミット)、パスの条件に一致したときだけワークフローを実行するように設定することができます。

パスを例にすると、1つ以上の変更されたファイルがpaths-ignore にマッチしない場合や、1つ以上の変更されたファイルが設定されたpathsにマッチする場合にワークフローを実行することができます。 

```yml
  push:
    branches:
      - master
      - "feature/*" #feature/featA, feature/featBなど
      - "feature/**" #feature/feat/a
      - "!feature/featC"
    tags:
      - v1.*
    paths:
      - "**.js"
      - "!filename.js"
```

また、ifを使ってジョブやステップの条件実行を行うこともできます。

```yml
jobs:
  one:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' #pushイベントのときだけジョブを実行
```

#環境変数
ワークフローでは、GitHubでデフォルトで用意されている環境変数と自分で作成した環境変数の両方を使うことができます。

`env`を設定することで環境変数を自分で作成することができ、記述する位置によって、ワークフロー内、ジョブ内、ステップ内と変数を使用できる範囲を変えることができます。

デフォルトの環境変数については、`${HOME}`や`${GITHUB_SHA}`と記述することで読み込むことができます。

```yml
name: ENV Variable
on: push
env: #すべてのジョブから利用できる環境変数
  WF_ENV: Available to all jobs

jobs:
  log-env:
    runs-on: ubuntu-latest
    env: #ジョブ内で利用できる環境変数
      JOB_ENV: Available to all steps in log-env job
    steps:
      - name: Log ENV Variables
        env: #ステップ内で利用できる環境変数
          STEP_ENV: Available to only this step
        run: |
          echo "WF_ENV: ${WF_ENV}"
          echo "JOB_ENV: ${JOB_ENV}"
          echo "STEP_ENV: ${STEP_ENV}"
  log-default-env:
    runs-on: ubuntu-latest
    steps:
      - name: Default ENV Variables
        run: |
          echo "HOME: ${HOME}"
          echo "GITHUB_WORKFLOW: ${GITHUB_WORKFLOW}"
          echo "GITHUB_SHA: ${GITHUB_SHA}"
```
自分でカスタマイズした環境変数はGitHub上で管理することができます。Actions secretsに保存することで、`WF_ENV: ${{ secrets.WF_ENV }}`のようにしてワークフロー内で読み込むことができます。
![スクリーンショット 2021-03-22 22.44.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/03a98e3d-111d-f1de-f1c5-8cd8ef8ec689.png)

##環境変数を使用するジョブ
環境変数について理解したところで、2つのジョブをつくってみようと思います。

###issueを作成するジョブ(create-issue)
適当なファイルをリポジトリにpushして、同時にissueを作成するようなジョブを作成します。これらの流れを2つのステップにわけて実装します。

####ファイルプッシュ(Push a random file)
`run`にgitコマンドがいろいろ書いてありますが、ここでは単に、仮想マシンのディレクトリでリポジトリの最新のmasterブランチをプルして、ファイルを追加して、リポジトリにプッシュしているだけです。

```yml
      - name: Push a random file
        run: |
          pwd
          ls -a
          git init
          git remote add origin "https://$GITHUB_ACTOR:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git"
          git config --global user.email "my-bot@bot.com"
          git config --global user.name "my-bot"
          git fetch
          git checkout master
          git branch --set-upstream-to=origin/master
          git pull
          ls -a
          echo $RANDOM >> random.txt
          ls -a
          git add -A
          git commit -m "Random file"
          git push
```

####REST APIでissue作成(Creating issue using REST API)
curlコマンドでGitHub REST APIをたたき、Issueを作成します。APIをコールするためにGitHubトークンが必要となるので、`${{ secrets.GITHUB_TOKEN }}`のようにして読み込んでいます。

```yml
  create-issue:
    runs-on: ubuntu-latest
    steps:
    - name: Create issue using REST API
      run: |
        curl --request POST \
        --url https://api.github.com/repos/${{ github.repository }}/issues \
        --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
        --header 'content-type: application/json' \
        --data '{
          "title": "Automated issue for commit: ${{ github.sha }}",
          "body": "This issue was automatically created by the GitHub Action workflow **${{ github.workflow }}**. \n\n The commit hash was: _${{ github.sha }}_."
          }'
```

ワークフローを実行すると、issueが確かに作成されています。
![スクリーンショット 2021-03-23 7.35.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/070c8a8c-6333-c4e5-fd75-d5b33bea66b8.png)


###暗号化ファイルの復号するジョブ
gpgで暗号化されたファイル`secret.json.gpg`を復号化して中身を確認するジョブを作成します。
ここではGitHubのsecretsに保存したPASSPHRASEを使用して、復号化のコマンドを実行しています。

```yml
  decrypt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Decrypt file
        run: gpg --quiet --batch --yes --decrypt --passphrase="$PASSPHRASE" --output $HOME/secret.json secret.json.gpg 
        env:
          PASSPHRASE: ${{ secrets.PASSPHRASE }}
      - name: Print our file content
        run: cat $HOME/secret.json
```

#その他オプション

##continue-on-error, timeout-minutes
ジョブの実行や停止を制御するオプションです。

```yml
    steps: #ジョブの実行内容
      - name: echo a string
        run: echo "Hello World"
        continue-on-error: true #前ステップでエラーが発生しても実行する
        timeout-minutes: 0 #指定時間以上経過したらジョブを停止する
```

##matrix
複数のOSで同じジョブを実行したい場合、同様の記述を繰り返せば実現することができますが、あまり綺麗なやり方ではありません。
このようなときにmatrixを使用します。

以下の例では、`os: [macos-latest, ubuntu-latest, windows-latest]`の3つのOS、`node_version: [6, 8, 10]`で3つのnodeバージョンを指定しています。

この場合、以下の記述だけで3×3=9パターンのジョブを実行することができます。

```matrix.yml
name: Matrix
on: push

jobs:
  node-version:
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest] #3パターンのOS
        node_version: [6, 8, 10] #3パターンのnode_version
        max-parallel: 2 #ジョブの最大並列実行数
        fail-fast: true #3つのジョブのどれかがfailだったらstop
    runs-on: ${{ matrix.os }} #3パターン
    steps:
      - name: Log node version
        run: node -v
      - uses: actions/setup-node@v2 #nodeのダウンロード
        with:
          node-version: ${{ matrix.node_version }} #3パターン
      - name: Log node version
        run: node -v    
```

また、include, excludeで、実行する(しない)matrixの組合せを指定することができます。
includeでは特定の組合せのときだけ使える変数(以下の例の`is_ubuntu_8`)を設定することができます。
excludeに2パターン記述しているので、以下で実行されるジョブ数は9-2=7つとなります。

```matrix.yml
name: Matrix
on: push

jobs:
  node-version:
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]
        node_version: [6, 8, 10] #3パターンのnode_version
        include: #指定したmatrixの組合せを実行する
          - os: ubuntu-latest
            node_version: 8
            is_ubuntu_8: "true" #この組合せのときだけ使用できる変数
        exclude: #指定したmatrixの組合せを実行しない
          - os: ubuntu-latest
            node_version: 6
          - os: macos-latest
            node_version: 8
    runs-on: ${{ matrix.os }}
    env:
      IS_UBUNTU_8: ${{ matrix.is_ubuntu_8 }}
    steps:
      - name: Log node version
        run: node -v
      - uses: actions/setup-node@v2 #nodeのダウンロード
        with:
          node-version: ${{ matrix.node_version }} #3パターン
      - name: Log node version
        run: |
          node -v
          echo $IS_UBUNTU_8
```

#おわりに
1ヶ月前に挫折したDockerコンテナの自動ビルドに再度挑戦してみたいと思います。

#参考資料
https://www.udemy.com/share/102DqFCEEZd11VRno=/

https://knowledge.sakura.ad.jp/23478/

https://docs.github.com/en/actions/reference