GitHub Actions のワナ仕様
はじめに
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
その他にも様々な記法があるようです。
中級編
composite
action のルートで定義した env
は使えない
composite
action (複合アクション) とは、一連のジョブステップを 1 つのアクションに集約して、ワークフローから 1 つのジョブステップとして呼び出せるようにしたものです。
通常のワークフローのように定義できますが、なぜかグローバルな env
は使えません。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 内全体で利用したい変数が必要な場合は、呼び出し元のワークフローから inputs
か env
で渡しましょう。先ほどの例でも呼び出し元のワークフローで 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
の例外を除き、ワークフローがフォークされたリポジトリからトリガーされた場合、シークレットはランナーに渡されません。
シークレットが再利用可能なワークフローに自動的に渡されることはありません。 詳しくは、「ワークフローの再利用」をご覧ください。
別にいいじゃんと思ってしまうのですが、そこはセキュリティ上の仕様なのでしょうか。呼び出し先で参照するには最上位のワークフローから渡す必要があります。なお、これは上述の 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 で試してみます。
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
自体が使えないので、呼び出し元のワークフローから inputs
か env
で渡してあげましょう。
Reusable workflow の場合
呼び出し元のワークフローで secrets: inherit
を指定して渡してあげましょう。
上級編
always()
などを指定しないと実行されない
連鎖的に依存するジョブの 1 つが skip されると、後続のジョブは ぶっちゃけ何言ってるか分かんないと思います。正直このケースに遭遇する方がどれだけいるかも分かりません。が、ハマると長引くので知っておいて損はないと思います。
どういうワークフロー?
以下のようなワークフローを考えます。
簡単な解説
- ジョブ 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
の次に実行するため、needs
にa
を指定します。 - job
c
:b1
,b2
の後に実行して、結果をまとめるようなことをしています。b1
,b2
のどちらが実行されても必ず実行したいため、if: ${{ always() }}
を指定します。 - job
d
: 最後に実行されるジョブでc
で設定した内容を出力しています。c
の次に実行するため、needs
にc
を指定します。
実行してみる
一見問題なく動きそうですが、このままだとジョブ d
は実行されません。以下は which_branch
に b1
を指定した時の実行結果です。
ジョブ d
がスキップされてしまっています 😢
なぜこうなる?
まず、なぜジョブ d
は実行されないのでしょうか?それは、ジョブ d
が依存するジョブ c
がさらに依存するジョブ b1
, b2
のどちらか一方が skip
されてしまっているためです。
ジョブ d
の needs
にジョブ c
しか指定していないにも関わらず、ジョブの実行結果は後続まで全て波及してしまうようです。GitHub Actions では、前段のジョブが skip
されると通常後続のジョブも skip
されてしまいます。
どうすれば良いか?
では、d
にも always()
を指定すれば良いのでしょうか?
ちょっと待ってください。もし、ジョブ c
が失敗したらジョブ d
が実行されるのは好ましくありません。もちろん always()
で良い場合もあると思いますが、ほとんどの場合は失敗した時点でエラー終了してほしいはずです。
上記を考慮してジョブ 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
が失敗した場合は実行しないようにできます。ちなみに、この指定は以下記事を参考にさせていただきました。
再度実行してみる
無事に最後まで動きました 🎉
Selected branch job is B1!
さいごに
ここまでお読みいただき、ありがとうございました。「ワナ仕様」とは言っていますが、GitHub Actions が便利であることは間違い無いので、これからも末長くお付き合いしていきたいと思います。
なお、本記事で紹介しましたワークフローは以下 GitHub にも登録していますので、興味がございましたらご参照ください。
もし、他にもっと良い方法あるよ!とか他にこんなワナ仕様あるよ!などご存じの方がいらっしゃいましたら是非コメントいただけますと幸いです。
謝辞
公式サイトをはじめ、引用させていただいたブログの方々には感謝申し上げます。

リアルタイム法人調査システム「SimpleCheck」を開発・運営するシンプルフォーム株式会社の開発チームのメンバーが、日々の開発で得た知見や試してみた技術などについて発信していきます。 Publication 運用への移行前の記事は zenn.dev/simpleform からご覧ください。
Discussion