0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Github Actionsの実装で地味に沼ったところ7選

0
Posted at

はじめに

皆さん、GitHub Actions を使用しているでしょうか? GitHub が提供する CI/CD ツールで、YAML ファイルを書くことで、push などのトリガーに様々な処理を走らせることができます。私はここ 1 年間くらいまともに使ってみたのですが、その際に細かい仕様が勝手な予想と違った部分があってハマったところがありました。ここでは思い出せる限り、それを紹介していきたいと思います。

想定読者

  • GitHub Actions を使い始めた方
  • Workflow が期待どおりに起動・停止せず困ったことがある方
  • GitHub Actions の安全な書き方を知りたい方

記事で得られること

GitHub Actions のトリガー設計、実行制御、セキュリティでハマりやすい仕様と、その対処法を理解できます。

Tips 一覧

  • push と pull_request の挙動の違い
  • PR のベースブランチ変更をトリガーにする方法
  • bot の操作から別の Workflow を起動する方法
  • Workflow のキャンセル時にジョブを止める方法
  • matrix のジョブを最後まで実行する方法
  • schedule の遅延とタイムゾーンに関する注意点
  • inline 展開によるスクリプトインジェクションの防ぎ方

トリガー設計でハマったこと

push と pull_request の違い

GitHub Actions を実行するには、Workflow の YAML ファイルにトリガーを設定する必要があります。

on:
  pull_request:
  push:
    branches:
      - "main"
      - "releases/**"

jobs:
  jobA: ~~~~

pull_request はデフォルトだと、PR を open, reopen したとき、PR がある状態で source ブランチを push などで更新したときに実行されます。push は PR の有無にかかわらず対応したブランチ名などの条件に従ってコミットを push した後に実行されます。
そのため、PR を作成された後に push でトリガーした場合だと、push と pull_request トリガーで差がないように見えます。しかし実はちゃんと差があります。違いは主に次の二つです。

  1. pull_request トリガーだと GitHub Actions 上で PR 間の差分の資材を取得できる
  2. pull_request トリガーで actions/checkout を既定設定のまま使うと、PR の仮マージコミットが checkout される

1 つ目は、SonarQube などで新規コードだけ静的解析を行いたいなどのケースに有用です。いくつかの解析系の Workflow では pull_request トリガーの場合で実行されるとデフォルトでコード差分だけに解析を行い、push トリガーではデフォルトブランチの差分やコード全体に対して解析を行うという仕様が見られます。
2 つ目に関しては、pull_request で actions/checkout を既定設定のまま使うと、通常は base ブランチへ PR を仮マージした内容のものが checkout されます。そのため、feature branch の push だけでは見つからない、マージ時の問題を PR 段階で検知できる場合があります。
個人的にはブランチ運用がしっかりされているのであればとりあえず pull_request トリガーを使用するのがいいと思います。

PR のベースブランチ変更

開発途中で「マージするブランチが変更になった」といったケースがよくあると思います。その時に、以下のような YAML ファイルで pull_request トリガーで単体テストのジョブを実行するように定義しているとします。

on:
  - pull_request

jobs:
  unit_test:

ブランチの向き先が変わったのであれば、pull_request トリガーに従って CI が実行されてほしいところですが、上記の設定だと実行されず、edited イベントを pull_request トリガーに指定してあげる必要があります。
ただ、この edited イベントはブランチの向き先変更だけでなく、PR のタイトルや説明の変更などでも実行されてしまい、意図せず多重実行されてしまう可能性があります。
対処法としては、ジョブの実行条件で明示的にブランチの向き先変更を判定させるか、concurrency を設定して多重実行を防ぐ必要があります。ただし、concurrency を指定する場合は、古い実行をキャンセルして新しい実行をトリガーするので注意が必要です。

on:
  pull_request:
    types: [opened, reopened, synchronize, edited]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  unit_test:

bot の操作から別の Workflow を起動する場合

GitHub Actions ではデフォルトで Actions を実行しているレポジトリに対して GitHub Token が発行された状態で実行されます。そのトークンは permissions で権限を与えることができます。

  on:
    workflow_dispatch:
    push:
      branches:
        - main

  permissions:
    contents: read

  jobs:
      jobA:
      ~~~~~

権限を適切に付与すれば、このトークンを使用することで PR の自動承認やラベルの付与などを行うことができ、セキュリティ的な懸念は増えますが、便利に使用することができます。

しかしこのデフォルトで与えられているトークンによる操作で workflow_dispatch と repository_dispatch を除いて、別のワークフローのトリガー条件に引っかかった場合(例えば、PR を作成する、push するなど)では対応するワークフローは自動実行されません(参照:公式ドキュメント)。
連鎖的にワークフローを実行したい場合は、無限ループに陥らないことを確認したうえで、GitHub App や PAT を使用する必要があります。
私の場合は Dependabot によって作られた PR に対して、既存の CI チェックをさせようとしたらこの問題に当たりました。

実行制御でハマったこと

Workflow のキャンセル

Workflow 実行中に何かしらの間違いに気づいた場合は、実行をキャンセルしたい場合があると思います。その場合は CLI で実行キャンセルをしたり、UI から「Cancel Workflow」を押下して止めると思います。
image.png

罠として、これを押下したらすべてのジョブが終了するわけではありません。公式ドキュメント にもあるように、ジョブのキャンセルを判定する際に、if による実行条件があるときはその実行条件が true になる場合にはキャンセルすることができません。例えば以下のような e2e ジョブを main ブランチで起動した場合、キャンセルを押しても e2e ジョブを main ブランチで実行したという事実は変わらないため、if 文が true を返し、ジョブをキャンセルすることができずに実行し続けられます。

jobs:
  e2e:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: E2E
        run: npm run test:e2e

      - name: Upload report
        run: echo "upload"

このようなことを避けるためには、キャンセルする可能性があるジョブには !cancelled() という条件を付ける方法があります。また、cancelled をつけ忘れた場合でも、step ごとに timeout を設定することで意図しない長時間実行を避けることができます。

jobs:
  e2e:
    if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: E2E
        timeout-minutes: 50
        run: npm run test:e2e

      - name: Upload report
        if: ${{ !cancelled() }}
        timeout-minutes: 5
        run: echo "upload"

matrix の fail-fast

GitHub Actions では matrix という機能で、同一のジョブを並列実行することができます。単体テストや E2E テストを並列実行させたい場合などに使用するかと思います。

jobs:
  unit-test:
    name: Unit Test (shard ${{ matrix.shard }}/4)
    runs-on: ubuntu-latest
    timeout-minutes: 20

    strategy:
      matrix:
        shard: [1, 2, 3, 4]

    steps:
      - name: Run unit tests
        timeout-minutes: 15

デフォルトだと、この matrix で並列実行したジョブはそのうちの一つでも失敗すると、他の並列実行されているジョブも途中終了します。テスト関係で並列実行している場合は基本的にすべて実行してほしいと思います。その場合は fail-fast: false を設定することですべての並列ジョブが完了するまで待つことができます。

schedule の遅延とタイムゾーン

GitHub Actions にも決まった時間に実行する schedule トリガーがあります。実行したい時間は cron 式で制御することができます。

on:
  schedule:
    - cron: "15 4,5 * * *"
      timezone: "Asia/Tokyo"

スケジュール系ではよくあることだと思いますが、このイベントは数十分から数時間遅延することがあります。そのため、時刻きっかりで実行したい場合は別のスケジューラーからワークフロー手動実行を行う必要があります。また、実行時間帯は高負荷時間帯を避ける必要があります。実際私は 9:00 に実行させようとして、11:30 ごろに実行したことがあります。

セキュリティで注意すること

inline 展開によるスクリプトインジェクション

GitHub Actions はブランチ名、レポジトリ名、GitHub Actions の環境変数等々のいろいろな入力を与えることができます。基本的にこれらは任意入力しやすいため、インジェクションできる対象になってしまいます。
run: 内で ${{}} で展開するとスクリプトインジェクションされる可能性が発生してしまいます。例えば以下の例だと、入力のタイトルはもちろんのこと、ブランチ名ですらインジェクションに利用できてしまいます。

jobs:
  unsafe:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Unsafe interpolation
        run: |
          title="${{ inputs.title }}"
          branch="${{ github.ref_name }}"
          echo "Title: $title"
          echo "Branch: $branch"

インジェクションを避けるためには、環境変数経由で渡してあげることで回避できます。この時に ${{ env.hogehoge }} で渡してしまうとインジェクションできてしまうので、$HOGEHOGE で渡します。

jobs:
  safe:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Safe interpolation
        env:
          TITLE: ${{ inputs.title }}
          BRANCH: ${{ github.head_ref }}
        run: |
          printf 'Title: %s\n' "$TITLE"
          printf 'Branch: %s\n' "$BRANCH"

この辺のセキュリティチェックは zizmor などといった Actions 用の静的解析ツールで検知することができます。

まとめ

ここでは、この一年間で GitHub Actions を使用していた間に少しハマったところをまとめてみました。GitHub Actions は便利ですが、細かいところで意図していない仕様だったりセキュリティ的に危険になりやすいところが多いかと思っています。アップデートによって所々仕様が変わっていくため、実装時には公式ドキュメントで最新の仕様を確認し、今後も変更をキャッチアップしていこうと思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?