最近、現場でGitHub Actionsを使い始めました。CI/CDってなんやねんというレベルだったので、基礎から学んで簡単なワークフローをいろいろつくって勉強してみることにしました。
GitHub Actionsって何?
GitHub上のリポジトリやイシューに対するさまざまな操作(Push, Pull Request, 外部イベントなど)や指定したスケジュールをトリガとして、あらかじめ定義した処理(Workflow)を実行する機能です。今までは自動テストや自動ビルドを実施するのにCircle CIやTravis CIなどの外部サービスとの連携が必要でしたが、GitHub Actionsを使うことでGitHubだけで実現することができます。
WorkflowはJobという単位にわけられ、仮想マシンのインスタンス上で実行されます。JobはさらにStepという単位にわけられ、コマンドや既存の処理(アクション)が実行されます。
Jobごとに仮想マシンをたてることができるので、それぞれの環境に適した処理を実行することができます。
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形式で以下のように記述することができます。
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タブでワークフローの状態を確認することができます。緑のチェックがついていれば成功です。
Runner診断ログ
SettingsタブのsecretsにACTIONS_RUNNER_DEBUG
, ACTIONS_STEP_DEBUG
をtrueで保存すると、Jobの実行状態に関する情報を含む追加のログファイルが2つ提供されます。
シェルコマンドの実行
shell
を追加することで、runステップに対するデフォルトのシェルを指定することができます。記述場所によってWorkflow単位, Job単位, Step単位で影響範囲を変えることができます。
以下ではPythonシェルを指定して、runステップでコマンドを実行しています。
- name: python Command
run: |
import platform
print(platform.processor())
shell: python
次にwinOSの仮想マシン(VM)で新たなジョブを作って、PowerShellとbashを実行してみます。
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
)を読み込んで実行してみます。
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のステップで前ステップを実行した時間を呼び出しています。
Checkoutって何?
簡単なワークフローをいろいろつくりましたが、実際にワークフローは仮想マシンのどのディレクトリで実行されているのでしょうか。
確認のために以下のステップを追加して実行してみます。
- 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
というアクションです。これを使うことにより、ワークフローが実行されたときにリポジトリがトリガとなったコミットにチェックアウトされて、仮想マシンのディレクトリにクローンされます。
- name: List Files
run: |
pwd
ls -a
- name: Checkout
uses: actions/checkout@v2
- name: List Files After Checkout
run: |
pwd
ls -a
Checkoutステップ後に仮想マシン上のディレクトリを確認すると、リポジトリがちゃんとクローンされています。
また、このアクションをGitコマンドに置き換えると以下のようになります。
git clone git@github:$GITHUB_REPOSITORY
git checkout $GITHUB_SHA
$GITHUB_REPOSITORY
や$GITHUB_SHA
はGitHubでデフォルトで用意されている環境変数です。それぞれ、ワークフローが実行されるリポジトリとワークフローをトリガしたコミットSHA(ID)を表しています。
変数の中身はそれぞれ以下のように出力されます。
- name: List Files
run: |
pwd
ls -a
echo $GITHUB_SHA #最新のコミットID
echo $GITHUB_REPOSITORY #ユーザ名/リポジトリ名
echo $GITHUB_WORKSPACE #VMのディレクトリ
echo "${{ github.token }}" #GitHubのトークン
ワークフローのトリガの種類
ここまではgit push
をトリガとしたワークフローを取り扱ってきましたが、push以外のトリガとなるイベントについてまとめました。
Git操作のイベント
以下はプルリクエストをトリガとするイベントの記述です。typesの中でプルリクがcloseしたときやopenしたときのイベントを指定することができます。
pull_request:
types: [closed, assigned, opened, reopened]
スケジュール
1日ごと1ヶ月ごとなど指定したスケジュールでワークフローを実行することができます。Crontab.guruというエディタを使うと簡単にcronの記述を行うことができます。
schedule:
- cron: "0/5 * * * *"
dispatch event
dispatch eventは手動のトリガによってワークフローを実行させる手段です。
以下の記述によって、buildというイベントのリクエストがリポジトリに送られると、ワークフローが実行されるようになります。
repository_dispatch:
types: [build]
試しにPostmanでリクエストを送ってみます。
リクエスト投げるにはトークンが必要なので、GitHubのDeveloper settingsから新しく作成します。
PostmanのAuthorizationタブでPasswordにトークンを入力してSendを押し、何も表示されなければリクエストの送信は成功です。
GitHubのActionsタブをみると、ワークフローが確かに実行されています。
ワークフローのフィルタリング
pushやpull_requestのイベントが起こった際、特定のブランチ、タグ(コミット)、パスの条件に一致したときだけワークフローを実行するように設定することができます。
パスを例にすると、1つ以上の変更されたファイルがpaths-ignore にマッチしない場合や、1つ以上の変更されたファイルが設定されたpathsにマッチする場合にワークフローを実行することができます。
push:
branches:
- master
- "feature/*" #feature/featA, feature/featBなど
- "feature/**" #feature/feat/a
- "!feature/featC"
tags:
- v1.*
paths:
- "**.js"
- "!filename.js"
また、ifを使ってジョブやステップの条件実行を行うこともできます。
jobs:
one:
runs-on: ubuntu-latest
if: github.event_name == 'push' #pushイベントのときだけジョブを実行
環境変数
ワークフローでは、GitHubでデフォルトで用意されている環境変数と自分で作成した環境変数の両方を使うことができます。
env
を設定することで環境変数を自分で作成することができ、記述する位置によって、ワークフロー内、ジョブ内、ステップ内と変数を使用できる範囲を変えることができます。
デフォルトの環境変数については、${HOME}
や${GITHUB_SHA}
と記述することで読み込むことができます。
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 }}
のようにしてワークフロー内で読み込むことができます。
環境変数を使用するジョブ
環境変数について理解したところで、2つのジョブをつくってみようと思います。
issueを作成するジョブ(create-issue)
適当なファイルをリポジトリにpushして、同時にissueを作成するようなジョブを作成します。これらの流れを2つのステップにわけて実装します。
ファイルプッシュ(Push a random file)
run
にgitコマンドがいろいろ書いてありますが、ここでは単に、仮想マシンのディレクトリでリポジトリの最新のmasterブランチをプルして、ファイルを追加して、リポジトリにプッシュしているだけです。
- 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 }}
のようにして読み込んでいます。
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が確かに作成されています。
暗号化ファイルの復号するジョブ
gpgで暗号化されたファイルsecret.json.gpg
を復号化して中身を確認するジョブを作成します。
ここではGitHubのsecretsに保存したPASSPHRASEを使用して、復号化のコマンドを実行しています。
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
ジョブの実行や停止を制御するオプションです。
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パターンのジョブを実行することができます。
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つとなります。
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コンテナの自動ビルドに再度挑戦してみたいと思います。
参考資料