Open40

GitHub Actions のベストプラクティス

概要

GitHub Actions のワークフローを書くときにこうした方がいいかな?と思ったことをメモっていくスクラップ。一般論ではなく個人的なもの。

1 フロー 1 ワークフロー

一連のフローがある場合は 1 つのワークフローにまとめる。

  • トリガーしたイベントの JSON が使える
  • needs での制御がしやすい
  • 全体を追える
  • グラフが表示される

ファイルを分割したい

ファイルを分割したい理由として以下が挙げられると思います。

  • 行数が増えて読みづらい
  • 処理を共通化したい

複合実行ステップアクションworkflow_run トリガーReusable workflow 🆕 を使うことになると思いますが、基本的には一連のフロー制御はメインのファイルに書いてその下を複合実行ステップアクションで外部ファイルへ分離するのが良さそう。

ワークフローの分割基準

トリガーの条件

条件が同じであれば対象が同じなので意味のある単位になっているはず。

  • paths
  • branches

依存関係があるか

needs で依存関係がある場合は同じワークフローにする。
依存関係があるものは下手に分けない方がよい。

workflow_dispatch でもワークフローを分割できるが、ワークフロー間の関連付けができずログが分断してしまうので避けた方がよさそう。

Reusable workflow はジョブを丸ごと外部ワークフローに依存することになる。
ワークフローがすっきりして便利ではあるが uses にコンテキストが使えないので依存先のリポジトリ名(自分自身であっても!)やブランチ名をべた書きしないといけないのがネック。
いつの間にか uses: ./.github/workflows/workflow.yml 形式で書けるようになってた。

デバッグ

ログを増やす

デバッグロギングの有効化 - GitHub Docs

ローカルで動かす

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 イベント

ワークフローファイル自身の更新イベントを登録しておくと修正後すぐに走ってくれる。

.github/workflows/example.yml
on:
  push:
    paths:
      - .github/workflows/example.yml

Bash 実行のデバッグ情報出力

- run: |
    set -x

学習

公式ドキュメントにチュートリアルがあります。
GitHub Actions について学ぶ - GitHub Docs

書籍やブログなども参考になるとは思いますが情報がすぐ古くなってしまうので初心者にはおすすめしません。
公式ドキュメントでほとんどの情報は網羅されています。

注意

公式ドキュメントも頻繁に更新されています。
特に日本語ドキュメントはページ内リンクが切れていることが多いのでページ内の特定の項目へリンクしたい場合は英語のページへリンクしておいた方が無難です。
リンク先が英語ドキュメントだった場合はページ上部で言語を切り替えられます。

翻訳が追いついていなかったりおかしかったりするページもあります。
翻訳修正の Pull request は受け付けていないようなのでページ下部の Contact support から報告してあげてください。

その他情報

個人的に役に立ちそうな記事などをまとめてます。

https://github.com/SnowCait/git-notes/blob/master/GitHubActions.md

skip ci

公式機能が実装されたのでまとめました。

https://zenn.dev/snowcait/articles/ef60401313a3fc

多くの CI サービスにはコミットメッセージに [skip ci][ci skip] と入れておくと CI 実行をスキップしてくれる機能があります。
複数のコミットを同時にプッシュした場合はどれか 1 つに含まれていたらスキップされます。
GitHub Actions では公式にこの機能がありませんので自前実装が必要です。

自前実装の前に

GitHub Actions ではイベントをフィルタすることができます。
まずはこちらで実現できないか検討しましょう。
branchespaths などがよく使われます。

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.0snow-actions/tweet@main のように指定すると思いますが、タグやブランチは指しているコミットを変更可能なのでいつの間にか悪意あるコードに書き換わっているかもしれません。
そういったことを気にする場合はコミットハッシュで指定することができます。
snow-actions/tweet@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
こうしておけばまず書き換わることはありません。

ハッシュはセキュリティの観点から省略できないようになりました。
GitHub Actions: Short SHA deprecation - GitHub Changelog

サードパーティアクションを使う場合には公開されているコードを読んでコミットハッシュでバージョンを指定しておけば安全に利用できます。

https://docs.github.com/ja/actions/learn-github-actions/security-hardening-for-github-actions
https://blog.ryotak.me/post/github-actions-supplychain/

入力値のインジェクション

run に直接 ${{ }} を書くと先に展開されてしまう。
入力値にコマンドが含まれる場合実行されてしまうので env を経由すること。
Issue や PR のタイトルはもちろん、ブランチ名も含まれるので注意。

https://securitylab.github.com/research/github-actions-untrusted-input/

GitHub ホストランナー or セルフホストランナー

GitHub ホストランナーの仕様 - GitHub Docs
セルフホストランナーについて - GitHub Docs

  • GitHub ホストランナーのスペックでは足りない
  • 環境 が GitHub の都合で更新されて動かなくなる
  • 障害のコントロール
  • 事前にソフトウェアのインストールをしたい
    • ライセンス
    • インストールに時間がかかる、コマンドからできない
  • 無料枠を大幅に超えるので課金するよりセルフホストした方が安い
  • 実行時間が GitHub ホストランナーのタイムアウトを超える
  • 並列実行数を増やしたい
  • リポジトリが大きく毎回 clone すると遅いのでインスタンスを使いまわす
  • Docker イメージを使いまわす
  • GitHub Enterprise Server を使っている
  • セキュリティ
    • アクセスキーを発行したくない

アクションの作成

https://docs.github.com/ja/actions/creating-actions

種類

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.04ubuntu-18.04 を推奨。
コストは Ubuntu < Windows <<<<< Mac なので特にこだわりがなければ Ubuntu 一択。
*-latest はバージョンが変わってワークフローが動かなくなることがあるのでバージョンまで指定しましょう。

OS 分の倍率
Linux 1
Windows 2
macOS 10

https://docs.github.com/ja/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions

複数の Docker コンテナを組み合わせたテスト

docker-compose.yml に定義した方が良さそう。
ローカルでも実行できます。

containerservices を使って組むこともできますがローカルで実行できないのでデバッグが大変。

ロジックを書きたい

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)
https://dagger.io/

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_REFbase_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 で編集する場合は補完の効くプラグインを入れておきましょう。

環境変数

環境変数は以下の3ヶ所に定義できます。どこに定義するか迷うのでルール決め。

  • ワークフロー
  • ジョブ
  • ステップ

https://docs.github.com/ja/actions/learn-github-actions/environment-variables

ワークフロー

トリガーされるイベントによらず不変なもの。 (${{ 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) を使用するかと思いますが、無限ループを防ぐためこのトークンの使用により発生したイベントは次のワークフローをトリガーしないようになっています。
https://docs.github.com/ja/actions/security-guides/automatic-token-authentication

これを回避するためには 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) になると困るのでそれを念頭に設計する必要があります。

  • SPOF になると困るところだけセルフホストランナーを使う(比較
  • (このスクラップ内でも何度か書いていますが)ローカルで実行できるようにしておく

セルフホストランナー

GitHub 自体が落ちていると Webhook がトリガーされないのでどうしようもないですがランナーの障害に比べれば頻度は低め。 GitHub ホストランナーは頻繁に落ちる。
GitHub 自体の障害にも対応したい場合は GitHub Enterprise (GHE) を使用する。

ローカルで実行

Dagger など新しい技術も出てきていますが Docker Compose や単純なシェルスクリプトでも十分なのではないかと思います。

外部サービスとの認証

GitHub ホストランナー

原則 OpenID Connect (OIDC) を使用します。
トークンを発行しなくて済むので安全です。

OIDC が使用できない場合に限りトークンを発行し Secrets に登録します。

セルフホストランナー

ランナーを同一ネットワーク内に作れるのでプラットフォームの機能(AWS であればセキュリティグループなど)を使用して通信を許可します。
トークンの発行や OIDC は不要です。

異なるプラットフォームのリソースを使いたい場合は GitHub ホストランナーに準じます。

ログインするとコメントできます