GitHub Actions のベストプラクティス
概要
GitHub Actions のワークフローを書くときにこうした方がいいかな?と思ったことをメモっていくスクラップ。一般論ではなく個人的なもの。
1 フロー 1 ワークフロー
一連のフローがある場合は 1 つのワークフローにまとめる。
- トリガーしたイベントの JSON が使える
-
needs
での制御がしやすい - 全体を追える
- グラフが表示される
ファイルを分割したい
ファイルを分割したい理由として以下が挙げられると思います。
- 行数が増えて読みづらい
- 処理を共通化したい
複合実行ステップアクション や workflow_run トリガー や Reusable workflow 🆕 を使うことになると思いますが、基本的には一連のフロー制御はメインのファイルに書いてその下を Reusable workflow や複合実行ステップアクションで外部ファイルへ分離するのが良さそう。
workflow_run はログが分断するのでおすすめしません。
ワークフローの分割基準
トリガーの条件
条件が同じであれば対象が同じなので意味のある単位になっているはず。
- paths
- branches
依存関係があるか
needs
で依存関係がある場合は同じワークフローにする。
依存関係があるものは下手に分けない方がよい。
workflow_dispatch
でもワークフローを分割できるが、ワークフロー間の関連付けができずログが分断してしまうので避けた方がよさそう。
Reusable workflow はジョブを丸ごと外部ワークフローに依存することになる。
ワークフローがすっきりして便利ではあるが uses
にコンテキストが使えないので依存先のリポジトリ名(自分自身であっても!)やブランチ名をべた書きしないといけないのがネック。
いつの間にか uses: ./.github/workflows/workflow.yml
形式で書けるようになってた。
デバッグ
ログを増やす
ローカルで動かす
nektos/act: Run your GitHub Actions locally 🚀
SSH する
GitHub Actions で SSH デバッグ! (debugging-with-tmate)
動いている最中の処理を中断してインスタンスへ SSH 接続することができます。
GitHub からすると意図しない使い方と思われるので怒られることはあるかもしれません。自己責任でお願いします。
試行錯誤用リポジトリを作る
サンドボックスとして利用できる試行錯誤用のリポジトリを作っておくと便利。
public なリポジトリだと無料。
ペイロードを出力しておく
$GITHUB_EVENT_PATH
または ${{ github.event_path }}
にペイロードが保存されているので出力しておくと使いたいプロパティを確認できます。
- run: cat $GITHUB_EVENT_PATH
追加のトリガーを設定する
イベントによって github.event
に送られてくる情報が異なるので使えるケースは限られますが追加で以下のイベントを設定しておくと便利。
workflow_dispatch イベント
手動実行イベント on.workflow_dispatch
を設定しておきます。
push イベント
ワークフローファイル自身の更新イベントを登録しておくと修正後すぐに走ってくれる。
on:
push:
paths:
- .github/workflows/example.yml
Bash 実行のデバッグ情報出力
- run: |
set -x
学習
公式ドキュメントにチュートリアルがあります。
GitHub Actions について学ぶ - GitHub Docs
書籍やブログなども参考になるとは思いますが情報がすぐ古くなってしまうので初心者にはおすすめしません。
公式ドキュメントでほとんどの情報は網羅されています。
注意
公式ドキュメントも頻繁に更新されています。
特に日本語ドキュメントはページ内リンクが切れていることが多いのでページ内の特定の項目へリンクしたい場合は英語のページへリンクしておいた方が無難です。
リンク先が英語ドキュメントだった場合はページ上部で言語を切り替えられます。
翻訳が追いついていなかったりおかしかったりするページもあります。
翻訳修正の Pull request は受け付けていないようなのでページ下部の Contact support
から報告してあげてください。
その他情報
個人的に役に立ちそうな記事などをまとめてます。
記事にしました。
skip ci
公式機能が実装されたのでまとめました。
多くの CI サービスにはコミットメッセージに [skip ci]
や [ci skip]
と入れておくと CI 実行をスキップしてくれる機能があります。
複数のコミットを同時にプッシュした場合はどれか 1 つに含まれていたらスキップされます。
GitHub Actions では公式にこの機能がありませんので自前実装が必要です。
自前実装の前に
GitHub Actions ではイベントをフィルタすることができます。
まずはこちらで実現できないか検討しましょう。
branches
や paths
などがよく使われます。
GitHub Actionsのワークフロー構文 - GitHub Docs
アクションを使って実装
アクション | 備考 |
---|---|
Skip CI action | HEAD のみ |
CI-SKIP-ACTION | |
Skip Workflow |
自前実装
イベントによって異なる。
コミットメッセージを見る方法は on.push
では使えるが、 on.pull_request
では使えない。
コミットメッセージ(HEAD のみ)
jobs:
build:
runs-on: ubuntu-latest
if: !contains(github.event.head_commit.message, '[skip ci]')
コミットメッセージ(全てのコミット)
jobs:
build:
runs-on: ubuntu-latest
if: !contains(github.event.commits.*.message, '[skip ci]')
Pull request タイトル
jobs:
build:
runs-on: ubuntu-latest
if: !contains(github.event.pull_request.title, '[skip ci]')
Pull request 本文
jobs:
build:
runs-on: ubuntu-latest
if: !contains(github.event.pull_request.body, '[skip ci]')
Pull request ラベル
jobs:
build:
runs-on: ubuntu-latest
if: !contains(github.event.pull_request.labels.*.name, 'skip ci')
結論
HEAD だけを見たいか、全てのメッセージを見たいか、 Pull request 単位で見たいか、あるいは対象としたいメッセージはケースバイケースなので自前実装して on.push
はコミットメッセージを、 on.pull_request
はタイトルや本文を見るのがいいのではないかと思います。
cron として使いたい
GitHub ホストランナーでやるのはやめましょう。
サーバーが混みあっているときはかなり遅延します。
落ちていることもあります。
不安定でも問題ないものは可。
使う場合は UTC なので注意。
セルフホストランナーでも遅延するかは要検証。
セキュリティ
GitHub Actions はサードパーティのアクションを作れる/使えるのが魅力ですが、セキュリティが気になることもあります。
使用するアクションを制限
使用を許可するアクションをリポジトリの [Settings] > [Actions] で制御できます。
バージョンの指定
通常は snow-actions/tweet@v1.0.0
や snow-actions/tweet@main
のように指定すると思いますが、タグやブランチは指しているコミットを変更可能なのでいつの間にか悪意あるコードに書き換わっているかもしれません。
そういったことを気にする場合はコミットハッシュで指定することができます。
snow-actions/tweet@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
こうしておけばまず書き換わることはありません。
ハッシュはセキュリティの観点から省略できないようになりました。
GitHub Actions: Short SHA deprecation - GitHub Changelog
サードパーティアクションを使う場合には公開されているコードを読んでコミットハッシュでバージョンを指定しておけば安全に利用できます。
GitHub ホストランナー or セルフホストランナー
GitHub ホストランナーの仕様 - GitHub Docs
セルフホストランナーについて - GitHub Docs
- GitHub ホストランナーのスペックでは足りない
- 環境 が GitHub の都合で更新されて動かなくなる
- 障害のコントロール
- 事前にソフトウェアのインストールをしたい
- ライセンス
- インストールに時間がかかる、コマンドからできない
- 無料枠を大幅に超えるので課金するよりセルフホストした方が安い
- 実行時間が GitHub ホストランナーのタイムアウトを超える
- 並列実行数を増やしたい
- リポジトリが大きく毎回 clone すると遅いのでインスタンスを使いまわす
- Docker イメージを使いまわす
- GitHub Enterprise Server を使っている
- セキュリティ
- アクセスキーを発行したくない
アクションの作成
種類
Docker コンテナアクション、 JavaScript アクション、複合実行ステップアクションの3種類がある。
1から作るなら JavaScript アクション。
Docker 資産があるなら Docker コンテナアクション。JS 以外で書く場合もこれ。
複数のステップをまとめたいなら複合実行ステップアクション。
Docker コンテナ | JavaScript | 複合実行ステップ | |
---|---|---|---|
OS | Linux | Linux, MacOS, Windows | Linux, MacOS, Windows |
速度 | 遅い | 速い | - |
言語 | 何でも | JavaScript(TS, AltJS) | Bash, PowerShell |
備考 |
uses は使えないuses も使えるようになりました
|
API アクセス
GitHub Enterprise Server との互換性のため GITHUB_API_URL
, GITHUB_GRAPHQL_URL
環境変数を使う。
パス
セルフホストランナーでも動かせるよう GitHub ホストランナーに依存したパスを使用しない。
リリース
ユーザーがアクションを利用する際に @
でバージョンを指定できる。
ここにはタグ、ブランチ、コミットの SHA が利用できる。
ブランチやメジャーバージョンのみのタグでの運用はバグでユーザーの環境を壊しかねない。
基本的には セマンティック バージョニング されたタグ指定かコミットの SHA を推奨。
steps:
- uses: actions/javascript-action@v1.0.0 # タグ
- uses: actions/javascript-action@v1-beta # ブランチ
- uses: actions/javascript-action@172239021f7ba04fe7327647b213799853a9eb89 # SHA
複合実行ステップアクションでusesは今はもう使えるようになってるみたいです。
ありがとうございます、情報を更新しました。
どれがいい?
新規作成するなら複合アクションがおすすめ。(複合実行ステップアクションから名前変わったっぽい)
actions/github-script を使えばちょっとした JS も書けるし、カスタムシェルを使えばどんな言語でも使える。
GitHub ホストランナーでの OS 選択
runs-on
の指定は ubuntu-20.04
か ubuntu-18.04
を推奨。
コストは Ubuntu < Windows <<<<< Mac なので特にこだわりがなければ Ubuntu 一択。
*-latest
はバージョンが変わってワークフローが動かなくなることがあるのでバージョンまで指定しましょう。
OS | 分の倍率 |
---|---|
Linux | 1 |
Windows | 2 |
macOS | 10 |
ロジックを書きたい
Jenkins だと Groovy を使ってロジックが書けたりします。
しかし GitHub Actions は YAML で書くため最低限の関数や if
くらいしか用意されていません。
ロジックを書くためには actions/github-script や自作アクションを作る(リポジトリ内/外)必要があります。
ですが、YAML にロジックを前に考えてほしいのがそのロジックは GitHub Actions でしか実行できないということです。
例えばテストを docker-compose
を使ってコマンド1つで実行できるようにしておけば YAML で素直に書けますしローカルでも実行ができます。
この 「ローカルで実行できる」 というのは大きなメリットです。デバッグも容易にできます。
YAML にロジックを書き始める前にそれは本当に必要か再考してみてください。
ローカルでも実行したい
テスト(デプロイスクリプトも?)のようにローカルと CI の両方で実行したいものに関しては Docker や Gradle などマルチ OS で動くものを使ってラップしておく。
ワークフローにべた書きしてしまっている(↑が出来ていない)プロジェクトでは Dagger も選択肢になりそうです。
Dagger (A Portable Devkit for CI/CD Pipelines)
CUE + BuildKit による CI/CD ラッパー。
PR の CI トリガー
on: pull_request
PR の CI をする場合は on: pull_request
を使いましょう。
デフォルトは types: [ opened, synchronize, reopened ]
なので PR 作成時だけでなく追加でプッシュしたときにも走ってくれます。
また、マージ先のブランチをマージした上で CI が実行されます。(GitHub が用意してくれる pull/:prNumber/merge
ブランチで走ります)
on: push
PR の CI として on: push
を使ってしまうと以下のような問題が発生します。
- マージ先のブランチが変更されていた場合に適用されない
- 余計なリソースを食わないように PR 以外で走らないようフィルタしないといけない
-
branches
でフィルタするためにはブランチ名のルールを決めないといけない
-
-
paths
を指定したときに追加でプッシュしたコミットに対象ファイルが含まれていないと CI が実行されない https://qiita.com/ham0215/items/9599facddcba7b912358
マージされた際にも実行したい
PR の CI が走った後にマージ先に変更が入った場合は当然 CI はトリガーされません。
最新の状態で CI が通ることを確認するためには保護ブランチの設定で Require branches to be up to date before merging
を有効にして最新の状態ではないとマージしないようにします。
ただ、更新頻度の高いリポジトリだとかなりコストが高いのでマージされた際にも再度 CI を走らせてエラーを早い段階で検知できるようにします。
下書き代わりに WIP (未検証)
on: push の branches
マージ先のブランチは大体決まっていると思うのでシンプルに branches
でフィルタします。
PR を通さずプッシュされたものも検知できます。
on:
push:
branches:
- main
- develop
- milestone/**
on: pull_request の types: closed
マージされたときも types: closed
でトリガーすることができます。
ペイロードが merged:true
になっているものがマージされた PR です。
必要あれば if: github.action != 'closed' || github.event.merged
などでフィルタしてあげてください。
on:
pull_request:
types: [ opened, synchronize, reopened, closed ]
競合していると pull_request
はトリガーされない。
pull_request_target
はトリガーされるようだが
- デフォルトの
GITHUB_REF
がbase_ref
だったり - デフォルトブランチに存在してないとトリガーされなかったり
- 競合を自動解決(コミット)したいケースでは
pull/:prNumber/merge
だと都合が悪かったり
するので思案中。
一般的な CI/CD におけるトリガー選択フローチャート
迷いがちなものを中心に。
pull_request_review
など自明なものは含めていません。
API から呼べる repository_dispatch
もありますが workflow_dispatch
がほとんど上位互換なので記載していません。
ディレクトリ構成
.github/
workflows/*.yml
actions/action-name/*.* # private なアクション
scripts/*.[sh|js|php] # ワークフローから呼び出すスクリプト
Problem Matchers の JSON を .github 直下に置いてるけどディレクトリにまとめたいような気もする。
セルフホストランナー
コスト管理や保守の観点からは Organization か Enterprise 単位で紐付けるのがよさそう。
モノリポな巨大リポジトリがある場合は独立させて Repository に紐付けてもいいかもしれない。
※ /runner/_work/<repository>/<repository>
に展開されるので複数のリポジトリがあるとディスク容量を食う。
ワークフローに必ず設定しておく項目
タイムアウト
リソース(特にプライベートなリポジトリで時間)を浪費しないように。
jobs:
<job_id>:
timeout-minutes: 1
ペイロード
デバッグや調査に必要。
steps:
- run: cat $GITHUB_EVENT_PATH
バージョン
使用しているツールのバージョン。
普段動いていても VM 環境が更新されて急に動かなくなったりするので調査に必要。
steps:
- run: gh --version
- run: docker --version
- run: docker-compose --version
ワークフローの編集
オンラインエディタが便利です。
Actions タブから New workflow
で新規作成。
テンプレートを選んで、右側のドキュメントを読みながら編集できます。
VS Code で編集する場合は補完の効くプラグインを入れておきましょう。
Secrets
登録すべき値
- GitHub Personal Access Token (PAT)
- Slack Webhook URL, BOT Token
- Twitter Consumer API Key / Secret, Access Token / Secret
登録すべきではない値
- AWS Access Key, Credentials
- Google 認証情報
- Azure 認証情報
- HashiCorp 認証情報
環境変数
環境変数は以下の3ヶ所に定義できます。どこに定義するか迷うのでルール決め。
- ワークフロー
- ジョブ
- ステップ
ワークフロー
トリガーされるイベントによらず不変なもの。 (${{ github.* }}
)
env:
GH_TOKEN: ${{ github.token }}
ジョブ
トリガーされるイベント毎に定義されるもの。 (${{ github.event.* }}
)
複数のイベントをトリガーにした場合にジョブ単位で if: github.event_name == '*'
を書くことが多そうなので。
jobs:
<job_id>:
if: github.event_name == 'pull_request'
env:
PR_NUMBER: ${{ github.event.number }}
ステップ
個別に上書きしたいもの。
または他のステップに渡したくないもの。
jobs:
<job_id>:
steps:
- run: gh pr create
env:
GH_TOKEN: ${{ secrets.PAT }}
FAQ
ハマりポイントとその対応。
ワークフローがトリガーされない
ブランチでの開発時
一部のトリガーはワークフローがデフォルトブランチに入っている必要があります。
cron が指定した時刻に起動しない
こちら を参考にしてください。
次のワークフローがトリガーされない
通常認証には GITHUB_TOKEN
(github.token
, secrets.GITHUB_TOKEN
) を使用するかと思いますが、無限ループを防ぐためこのトークンの使用により発生したイベントは次のワークフローをトリガーしないようになっています。
これを回避するためには Personal Access Token (PAT) や GitHub Apps で発行されたトークンを使用します。
ただし無限ループしないように十分注意してください。
セキュリティ的には GitHub Apps がおすすめです。
Git の履歴やブランチにアクセスできない
actions/checkout
を使用すると Git の履歴、ブランチやタグにアクセスできません。
これは無用なデータをダウンロードしないようデフォルトではシャロークローン (fetch-depth: 1
) になっているためです。
チェックアウトするブランチを指定したい場合は ref
を指定します。
履歴やすべてのブランチを取得したい場合は fetch-depth: 0
を指定します。
README には他にも様々なサンプルが記載されているので一読することをおすすめします。
PR のブランチにプッシュできない
actions/checkout
はデフォルトで GITHUB_SHA
にチェックアウトするようになっています。
pull_request
イベントの場合は PR マージブランチ refs/pull/:prNumber/merge
になります。
PR のブランチにチェックアウトしたい場合は ref: ${{ github.head_ref }}
または ref: ${{ github.event.pull_request.head.sha }}
を指定しましょう。
ワークフローの設計
- スキップ用途で
exit 1
しない(ログに❌が並んでしまい本当に fail してるものが見つけづらくなる)
GitHub ホストランナーの障害対策
GitHub ホストランナーは頻繁に落ちます。
単一障害点 (SPOF: Single Point Of Failure) になると困るのでそれを念頭に設計する必要があります。
セルフホストランナー
GitHub 自体が落ちていると Webhook がトリガーされないのでどうしようもないですがランナーの障害に比べれば頻度は低め。 GitHub ホストランナーは頻繁に落ちる。
GitHub 自体の障害にも対応したい場合は GitHub Enterprise (GHE) を使用する。
ローカルで実行
Dagger など新しい技術も出てきていますが Docker Compose や単純なシェルスクリプトでも十分なのではないかと思います。
外部サービスとの認証
GitHub ホストランナー
原則 OpenID Connect (OIDC) を使用します。
トークンを発行しなくて済むので安全です。
OIDC が使用できない場合に限りトークンを発行し Secrets に登録します。
セルフホストランナー
ランナーを同一ネットワーク内に作れるのでプラットフォームの機能(AWS であればセキュリティグループなど)を使用して通信を許可します。
トークンの発行や OIDC は不要です。
異なるプラットフォームのリソースを使いたい場合は GitHub ホストランナーに準じます。
ワークフローをすっきり書く Tips
サードパーティアクションは不要なことが多いです。
Bash
変数展開で変数を加工したり、Bash の機能で様々なことができます。
GitHub CLI
デフォルトで使用できて GitHub の操作が一通りできます。
情報の参照先
基本的には github コンテキストを参照することが多い。
github.event
にペイロードが入っている。
ただしトリガーされて実際にジョブが実行されるまでには時間が空くので PR のステータスやラベルなど最新情報を見た方がいい場合は github.event
を参照しない方が良さそう。
github.event
にはトリガーされた時点の情報が入っており Re-run しても変更されない。
PR の情報であれば gh pr view
で取得できる。
モニタリング