Open1

pull_request_target で skip ci を実現する

Shunsuke SuzukiShunsuke Suzuki

まとめ

  • pull_request_target では skip ci が機能しないので、同様のことがやりたければ独自に実装するしかない
  • やり方は色々あるが、特定の PR label をつけたら job を skip するのが楽では
    • 特定の PR label をつけたら特定の job を fail させることで、特別な権限なしに PR をマージできてしまうのを防ぐことができる

導入

以前 pull_request_target に関する記事を書きました。

https://zenn.dev/shunsuke_suzuki/articles/secure-github-actions-by-pull-request-target
https://dev.to/suzukishunsuke/secure-github-actions-by-pullrequesttarget-641

ところで、 GitHub Actions は [skip ci] と commit message に書くと workflow の trigger させないでおけます。

https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/skipping-workflow-runs

しかしこの機能は pull_request はサポートされているものの pull_request_target はサポートされていません。

Skip instructions only apply to the push and pull_request events. For example, adding [skip ci] to a commit message won't stop a workflow that's triggered on: pull_request_target from running.

なぜこのような仕様なのかはよくわかりませんが、 pull_request_target であっても CI を skip したくなることはあります。

そこで pull_request_target でも CI を skip する方法を考察します。

そもそも CI を skip してよいのか

勿論基本は skip すべきではありませんし、 branch protection rule (branch ruleset) で CI を pass しないとマージできないようにすべきです。
しかし大きなマイグレーションで CI が実行されると困るから CI を skip して admin 権限で force merge したいというケースはままあります。
その場合

  • audit log として残るようにして後から追跡できるようにする
  • force merge したら通知がいくようにし気づけるようにする
  • なぜ force merge したのか理由を pull request に残す
  • 他の人からの approve をもらう

といったことをすべきでしょう。

workflow run 自体を skip することはできず、 job を skip するしかない

workflow run 自体を skip するのは on filter によってしかできず、 commit message などによって skip することはできません。
branch filter で skip できるかと一瞬思いましたが、 branch filter は base branch による filtering なので難しいでしょう。
なので job の if で job を skip するしかありません。

job の skip で branch protection rule を pass するべきか?

job を skip することで required check を pass することもできます。
というか、単純に skip すると pass することになります。
こうすると force merge しなくてもマージできるということになります。
これには一長一短あるでしょう。

pros:

  • force merge しなくてよい
    • admin 権限不要
    • 一時的に branch protection rule を修正する必要がない

cons:

  • 特別な権限がなくても容易に CI を pass できてしまう
    • CI が skip されていることに reviewer が気づかずに approve, merge される可能性がある
  • audit log などが残らない可能性がある

CI を pass しないようにするにはどうしたらよいか

やり方は幾つか考えられますが、 CI を skip したい場合だけ fail し、それ以外では skip される job を用意すると良いでしょう。

skip-ci label がセットされていたら fail する job
jobs:
  skip-ci:
    if: contains(github.event.issue.labels.*.name, 'skip-ci')
    steps:
      - run: exit 1

job を skip する方法

  • commit message で skip
  • PR label で skip
  • PR title で skip

commit message で skip

これは skip ci と互換性があるため、ユーザーにとって分かりやすいでしょう。
一方で最新の head commit の commit message を取得する必要があります。

https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target

自分の理解が正しければ event の payload にコミットメッセージは含まれていないため、 API で取得する必要があります。
そのため、最初に実行する job で commit message を取得し、後続の job の if で skip する必要があり、 workflow の構成を変える必要もあるかもしれません。
尤も、以下の記事に書いてあるように、 dorny/paths-filter のような action を使って job を filtering している場合、 dorny/paths-filter を実行する job で commit message を取得すればいいので大きく構成を変える必要はありません。

https://zenn.dev/shunsuke_suzuki/articles/renovate-auto-merge-github-actions#4.-pull_request-用の-github-actions-workflow-を-1-つにまとめる

PR label or title で skip

PR の title が [skip ci] で始まったら job を skip
jobs:
  foo:
    if: |
      !startsWith(github.event.pull_request.title, '[skip ci]')   

https://docs.github.com/en/enterprise-cloud@latest/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#startswith

このやり方は skip ci とは互換性がないため、ユーザーを混乱させるかもしれません。

独自ルールになるので、きちんとドキュメントを書くのが望ましいでしょう。

また pull_request と pull_request_target が混在しているような場合 pull_request を skip できません。
尤も、混在させる必要はあまりない気はします。

PR title や label は event の payload に含まれているため API で取得する必要はありませんし、 job を追加したりする必要もありません。
尤も、後から PR title や label を修正して workflow run を retry しても payload は変わらないのでそこは注意が必要です。
その場合、新たに commit を push するなどする必要があります。

[skip ci] のみならず幾つかのパターンをサポートしようと思うとその分 if は冗長になります。
また skip ci されたものを検索したいときにクエリが複雑になるので、決め打ちで条件を絞ったほうが良いかと思います。

title と label はどちらでも良さそうですが、個人的には label が良いかなという気はします。

  • title のほうがパッとわかりやすいかもしれない
  • 検索クエリは label のほうがシンプル (例えばlabel:skip-ci)
skip-ci label がセットされていたら job を skip
jobs:
  foo:
    if: |
      !contains(github.event.issue.labels.*.name, 'skip-ci')

PoC

https://github.com/suzuki-shunsuke/poc-skip-ci-pull_request_target