😹

GitHub Actions のワナ仕様

2025/02/21に公開

はじめに

GitHub Actions (GHA)、便利ですよね。

便利なんですが、たまに「え、そんな仕様だっけ?」みたいなワナに遭遇します。そして毎回忘れてワナにハマってしまいます。

今後そんなワナにハマって時間を無駄にしないよう、ワナ仕様について網羅していきます。

対象読者

  • GitHub Actions を使い倒したい方
  • 割と大きめな規模の GitHub Actions を構成している方

初級編

run を複数行で書くときの記法

GHA というか YAML の記法です。いきなり GHA から外れてすみません 💦

が、よく忘れるので覚えておくと助かります。以下の 2 種類を覚えておけば、ほとんどの場合事足りると思います。

| は改行を改行として実行する

run: |
  echo "hoge"
  echo "fuga"
実行結果
hoge
fuga

> は改行を無視して 1 コマンドとして実行する

run: >
  echo "hoge"
  echo "fuga"
実行結果
hoge echo fuga

その他にも様々な記法があるようです。

https://qiita.com/jerrywdlee/items/d5d31c10617ec7342d56

中級編

composite action のルートで定義した env は使えない

composite action (複合アクション) とは、一連のジョブステップを 1 つのアクションに集約して、ワークフローから 1 つのジョブステップとして呼び出せるようにしたものです。

https://docs.github.com/ja/actions/sharing-automations/creating-actions/creating-a-composite-action

通常のワークフローのように定義できますが、なぜかグローバルな env は使えませんcomposite action の中でのみ有効な環境変数を定義できても良さそうですが、定義しても参照できません(ただし、エラーが起きないので気付きにくい)。

簡単な composite action を使って例を示します。

composite action
name: Test Action

inputs:
  name:
    required: true
    type: string

env:
  GLOBAL_ENV: "global environment"

runs:
  using: composite
  steps:
    - name: Test step
      id: test
      shell: bash
      env:
        LOCAL_ENV: "local environment"
      run: |
        echo "Hello, ${{ inputs.name }}! at ${{ env.GLOBAL_ENV }}"
        echo "Hello, ${{ inputs.name }}! at $LOCAL_ENV"
呼び出し元のワークフロー
name: Test Workflow

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Hello
        uses: ./.github/actions/test_action # ここで呼び出している
        with:
          name: "World"

実行結果は以下の通りで、グローバルに定義して GLOBAL_ENV が参照できていません。

実行結果
Hello, World! at
Hello, World! at local environment

なお、composite action 内のジョブステップではワークフローと同様に env が定義できます。

もし、composite action 内全体で利用したい変数が必要な場合は、呼び出し元のワークフローから inputsenv で渡しましょう。先ほどの例でも呼び出し元のワークフローで env を定義すれば使えるようになります。

呼び出し元のワークフロー (修正版抜粋)
      - name: Hello
        uses: ./.github/actions/test_action # ここで呼び出している
        env:
          GLOBAL_ENV: "global environment" # 追加
        with:
          name: "World"
実行結果
Hello, World! at global environment
Hello, World! at local environment

最上位のワークフロー以外では secret を参照できない (ただし GITHUB_TOKEN を除く)

secret はパスワードやトークンなどの機密情報を扱うための特別な変数です。リポジトリなどに設定しておくと GitHub Actions 内で読み取ることができます。

非常に便利な secret ですが、最上位のワークフロー以外では参照することができません

GITHUB_TOKEN の例外を除き、ワークフローがフォークされたリポジトリからトリガーされた場合、シークレットはランナーに渡されません。
シークレットが再利用可能なワークフローに自動的に渡されることはありません。 詳しくは、「ワークフローの再利用」をご覧ください。

https://docs.github.com/ja/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#using-secrets-in-a-workflow

別にいいじゃんと思ってしまうのですが、そこはセキュリティ上の仕様なのでしょうか。呼び出し先で参照するには最上位のワークフローから渡す必要があります。なお、これは上述の composite action だけではなく、Reusable workflow (ワークフローから呼び出されるワークフロー) も同様です。

composite action で試してみる

先ほどの composite action を少し修正して試してみます。

name: Test Action

inputs:
  name:
    required: true
    type: string

runs:
  using: composite
  steps:
    - name: Test step
      id: test
      shell: bash
      env:
        LOCAL_ENV: ${{ secrets.TEST_SECRET }}
      run: |
        echo "Hello, ${{ inputs.name }}! at ${{ env.GLOBAL_ENV }}"
        echo "Hello, ${{ inputs.name }}! at $LOCAL_ENV"

残念ながらエラーになってしまいました。composite action では secrets というコンテキストがそもそも無いようです。

実行結果
Error: ..././.github/actions/test_action/action.yml (Line: 15, Col: 20): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.TEST_SECRET

Reusable workflow で試してみる

次のような Reusable workflow で試してみます。

Reusable workflow
name: Test Reusable Workflow

on:
  workflow_call:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Test secret
        id: test
        shell: bash
        run: |
          echo "Hello, secret: ${{ secrets.TEST_SECRET }}"
呼び出し元のワークフロー
name: Test Workflow

on:
  workflow_dispatch:

jobs:
  test_reusable_workflow:
    uses: ./.github/workflows/_test_reusable_workflow.yaml  # ここで呼び出している

エラーにはなりませんが、secret の参照はできていないようです。

実行結果
Hello, secret:

一方で、GITHUB_TOKEN だけは例外で Reusable workflow でも参照ができます。GITHUB_TOKEN が参照できるんだから他の secret も参照できるはずと思い込んでしまうとハマります。自分はハマりました... 😓

どうすれば良いか?

composite action の場合

secrets 自体が使えないので、呼び出し元のワークフローから inputsenv で渡してあげましょう

Reusable workflow の場合

呼び出し元のワークフローで secrets: inherit を指定して渡してあげましょう

https://docs.github.com/ja/actions/sharing-automations/reusing-workflows#passing-inputs-and-secrets-to-a-reusable-workflow

上級編

連鎖的に依存するジョブの 1 つが skip されると、後続のジョブは always() などを指定しないと実行されない

ぶっちゃけ何言ってるか分かんないと思います。正直このケースに遭遇する方がどれだけいるかも分かりません。が、ハマると長引くので知っておいて損はないと思います。

どういうワークフロー?

以下のようなワークフローを考えます。

簡単な解説

  • ジョブ B1, B2 は inputs.which_branch の値に応じてどちらか一方だけを実行します。
  • ジョブ C, D は B1, B2 どちらが実行されても実行するようにします。

どういう実装?

上記のフローを実装してみます。中身は簡単ですが、少し長いです。

ワークフロー
name: Test Branch Workflow

on:
  workflow_dispatch:
    inputs:
      which_branch:
        required: true
        type: choice
        options:
          - b1
          - b2

jobs:
  a:
    runs-on: ubuntu-latest
    steps:
      - name: Init
        shell: bash
        run: |
          echo "This is initial job!"

  b1:
    runs-on: ubuntu-latest
    needs:
      - a
    if: ${{ inputs.which_branch == 'b1' }}
    outputs:
      branch_job: ${{ steps.select.outputs.branch_job }}
    steps:
      - name: Select
        id: select
        shell: bash
        run: |
          echo "branch_job=B1" >> $GITHUB_OUTPUT

  b2:
    runs-on: ubuntu-latest
    needs:
      - a
    if: ${{ inputs.which_branch == 'b2' }}
    outputs:
      branch_job: ${{ steps.select.outputs.branch_job }}
    steps:
      - name: Select
        id: select
        shell: bash
        run: |
          echo "branch_job=B2" >> $GITHUB_OUTPUT

  c:
    runs-on: ubuntu-latest
    needs:
      - b1
      - b2
    if: ${{ always() }}
    outputs:
      branch_job: ${{ steps.set.outputs.executed_branch_job }}
    steps:
      - name: Set
        id: set
        shell: bash
        run: |
          if [ "${{ needs.b1.result }}" = "success" ]; then
            echo "executed_branch_job=${{ needs.b1.outputs.branch_job }}" >> $GITHUB_OUTPUT
          elif [ "${{ needs.b2.result }}" = "success" ]; then
            echo "executed_branch_job=${{ needs.b2.outputs.branch_job }}" >> $GITHUB_OUTPUT
          else
            echo "executed_branch_job=none" >> $GITHUB_OUTPUT
          fi

  d:
    runs-on: ubuntu-latest
    needs:
      - c
    steps:
      - name: Show
        shell: bash
        run: |
          echo "Selected branch job is ${{ needs.c.outputs.branch_job }}!"

簡単な解説

  • job a: 何らかの初期設定を行うジョブです。必ず一番最初に実行します。
  • job b1, b2: inputs.which_branch に応じてどちらかが実行されるようにします。a の次に実行するため、needsa を指定します。
  • job c: b1, b2 の後に実行して、結果をまとめるようなことをしています。b1, b2 のどちらが実行されても必ず実行したいため、if: ${{ always() }} を指定します。
  • job d: 最後に実行されるジョブで c で設定した内容を出力しています。c の次に実行するため、needsc を指定します。

実行してみる

一見問題なく動きそうですが、このままだとジョブ d は実行されません。以下は which_branchb1 を指定した時の実行結果です。

実行結果1

ジョブ d がスキップされてしまっています 😢

なぜこうなる?

まず、なぜジョブ d は実行されないのでしょうか?それは、ジョブ d が依存するジョブ c がさらに依存するジョブ b1, b2 のどちらか一方が skip されてしまっているためです。

ジョブ dneeds にジョブ c しか指定していないにも関わらず、ジョブの実行結果は後続まで全て波及してしまうようです。GitHub Actions では、前段のジョブが skip されると通常後続のジョブも skip されてしまいます。

どうすれば良いか?

では、d にも always() を指定すれば良いのでしょうか?

ちょっと待ってください。もし、ジョブ c が失敗したらジョブ d が実行されるのは好ましくありません。もちろん always() で良い場合もあると思いますが、ほとんどの場合は失敗した時点でエラー終了してほしいはずです。

上記を考慮してジョブ d に条件式を追加します。

ジョブ d の修正
  d:
    runs-on: ubuntu-latest
    needs:
      - c
    if: ${{ !cancelled() && !failure() }} # 条件式追加
    steps:
      - name: Show
        shell: bash
        run: |
          echo "Selected branch job is ${{ needs.c.outputs.branch_job }}!"

always() ではなく !cancelled() && !failure() とすることで、ジョブ c が失敗した場合は実行しないようにできます。ちなみに、この指定は以下記事を参考にさせていただきました。

https://qiita.com/abetomo/items/d9ede7dbeeb24f723fc5

再度実行してみる

無事に最後まで動きました 🎉

実行結果2

ジョブ d の実行結果
Selected branch job is B1!

さいごに

ここまでお読みいただき、ありがとうございました。「ワナ仕様」とは言っていますが、GitHub Actions が便利であることは間違い無いので、これからも末長くお付き合いしていきたいと思います。

なお、本記事で紹介しましたワークフローは以下 GitHub にも登録していますので、興味がございましたらご参照ください。

https://github.com/jirtosterone/github-actions-playground

もし、他にもっと良い方法あるよ!とか他にこんなワナ仕様あるよ!などご存じの方がいらっしゃいましたら是非コメントいただけますと幸いです。

謝辞

公式サイトをはじめ、引用させていただいたブログの方々には感謝申し上げます。

SimpleForm Tech Blog

Discussion