GitHub Actions のワークフローがキャンセルできなくなったら

GitHub Actions のワークフローがキャンセルできなくなったら

GitHub Actions のワークフローがずっと「queued」ステータスのままになっていたので、GitHub のページ上からキャンセルボタンを押したところ、success と表示されるものの実際にはワークフローがキャンセルせずに queued のままになってしまう事象が発生しました。

ちなみにそのときのワークフロー定義ファイルは以下のような感じでした。

steps:
  ...
  - name: The job that runs always
    if: ${{ always() }}

この always() が曲者でした 🫠

強制的にワークフローをキャンセルする方法 #

always() の詳細は後回しにするとして、このような状態に陥ったときにも強制的にワークフローをキャンセルするには GitHub CLI を使うと良いです。

gh run list -R {リポジトリ名} でワークフローの一覧を取得し、キャンセルしたいワークフローの ID を確認します。STATUS が * がら queued のワークフローです。

gh run list -R my-organization/my-repository

STATUS  TITLE               WORKFLOW       BRANCH           EVENT         ID           ELAPSED   AGE
*       new-mypage          my-validate    feat/new-mypage  pull_request  19259302212  42m56s    about 42 minutes ago
*       new-mypage          my-validate    feat/new-mypage  pull_request  19255928662  3h31m31s  about 3 hours ago
X       new-mypage          my-validate    feat/new-mypage  pull_request  19250659802  3m41s     about 8 hours ago
X       my-cron-validation  cron-validate  main             schedule      19207217714  1s        about 1 day ago

ワークフロー ID が分かったら gh run cancel --force コマンドで強制キャンセルできます。以下の例では 2 つのワークフローを強制キャンセルしています。

gh run cancel --force 19259302212 -R my-organization/my-repository
> ✓ Request to force cancel workflow 19259302212 submitted.

gh run cancel --force 19255928662 -R my-organization/my-repository
> ✓ Request to force cancel workflow 19255928662 submitted.

このあとでブラウザでワークフローのページをリロードすると、ちゃんとキャンセル表示に変わっているはずです。

if: ${{ always() }} について #

always() は本当にオールウェイズです。たとえジョブがキャンセルされた場合でも true を返します。つまり always() が指定されたジョブやステップは、ワークフローがキャンセルされた場合でも実行され続けます。そういう意味で always() はキャンセル不可能です。

Causes the step to always execute, and returns true, even when canceled. The always expression is best used at the step level or on tasks that you expect to run even when a job is canceled. For example, you can use always to send logs even when a job is canceled.

https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#always

ほとんどの場合において、always() を使う意図は「依存するジョブ/ステップが失敗した場合であっても実行させたい」というものだと思います。その場合は if: ${{ always() }} ではなく if: ${{ !cancelled() }} を使いましょう。

steps:
  ...
  - name: The job that runs always but can be cancelled
    if: ${{ !cancelled() }}

if: ${{ !cancelled() }} はどういう意味かというと、「キャンセルされない限り実行する」です。さらに書き加えるならば「(依存するジョブが成功していようが失敗していようが)自身がキャンセルされない限りは実行する」です。要するにキャンセル可能ではありつつも、前のジョブにかかわらず常に実行するという挙動です。

GitHub Docs の always() にも以下の記載があります。

Warning

Avoid using always for any task that could suffer from a critical failure, for example: getting sources, otherwise the workflow may hang until it times out. If you want to run a job or step regardless of its success or failure, use the recommended alternative: if: ${{ !cancelled() }}

https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#always