親ワークフローでは ${{ secrets.DEPLOY_TOKEN }} を使えるのに、再利用ワークフローへ移すと空になる場合、原因の多くは秘密情報が呼び出し先へ渡されていないことです。
GitHub Actionsのカスタムシークレットは、再利用ワークフローへ自動的には引き継がれません。呼び出し先で on.workflow_call.secrets を定義し、呼び出し元のジョブから secrets で明示的に渡すか、条件を満たす場合に secrets: inherit を指定します。未設定のシークレットを参照すると式の結果は空文字列になるため、「親では使えるのに呼び出し先では空」という現象が起こります。([GitHub Docs][1])
再利用ワークフローへsecretsが渡らないときのGitHub Actions設定
最初に、呼び出し元と呼び出し先の呼び方を整理します。
- 呼び出し元:親ワークフロー、caller workflow
- 呼び出し先:再利用ワークフロー、called workflow
再利用ワークフローへ秘密情報を渡すには、次の3点がそろっている必要があります。
| 確認箇所 | 必要な設定 |
|---|---|
| 呼び出し方法 | jobs.<job_id>.uses で再利用ワークフローを呼ぶ |
| 呼び出し先 | on.workflow_call.secrets で受け取る名前を定義する |
| 呼び出し元 | jobs.<job_id>.secrets で実際のシークレットを対応付ける |
どれか一つでも欠けると、シークレットが空になったり、ワークフローの構文検証でエラーになったりします。
再利用ワークフローはstepsではなくjobsから呼び出す
再利用ワークフローは、通常のActionとは呼び出し方が異なります。
次のように steps の中へ書くのは誤りです。
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: ./.github/workflows/reusable-deploy.yml
steps[*].uses は、ActionやComposite Actionを実行する場所です。再利用ワークフローはジョブそのものとして呼び出します。
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
GitHub Actionsでは、再利用ワークフローを jobs.<job_id>.uses で直接呼び出し、steps の中には配置しません。呼び出し先のファイルには on: workflow_call が必要です。([GitHub Docs][2])
secretsを明示的に渡す最小構成
同じリポジトリ内の再利用ワークフローへ、PROD_DEPLOY_TOKEN を渡す例を見てみましょう。
呼び出し元ワークフロー
.github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
jobs:
call-deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
target: production
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
呼び出し先の再利用ワークフロー
.github/workflows/reusable-deploy.yml
name: Reusable deploy
on:
workflow_call:
inputs:
target:
description: Deployment target
required: true
type: string
secrets:
deploy_token:
description: Token used for deployment
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Verify secret
shell: bash
env:
DEPLOY_TOKEN: ${{ secrets.deploy_token }}
run: |
if [ -z "${DEPLOY_TOKEN:-}" ]; then
echo "::error::deploy_token is empty"
exit 1
fi
echo "deploy_token is available"
echo "Deployment target: ${{ inputs.target }}"
この構成では、次の対応関係が重要です。
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
左側の deploy_token は、再利用ワークフローが受け取るインターフェース名です。
右側の PROD_DEPLOY_TOKEN は、呼び出し元リポジトリなどに登録されている実際のシークレット名です。
呼び出し先では、元の名前ではなく、左側の名前を使います。
${{ secrets.deploy_token }}
次のように元のシークレット名を参照すると、明示的なマッピングを行った構成では空になる可能性があります。
${{ secrets.PROD_DEPLOY_TOKEN }}
on.workflow_call.secrets で定義した名前、呼び出し元の secrets の左辺、呼び出し先で参照する名前の3か所を照合してください。明示的に渡すシークレット名は、呼び出し先の定義と一致している必要があります。([GitHub Docs][3])
inputsではなくsecretsで渡す
トークンや秘密鍵を、通常の入力値として渡すのは避けます。
with:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
機密値は、次のように secrets を使って渡します。
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
with は環境名、デプロイ先、ビルド方式などの通常パラメーターに使い、トークンやパスワードは secrets へ分けると、ワークフローの意図も明確になります。
| 種類 | 渡し方 | 呼び出し先での参照 |
|---|---|---|
| 通常の設定値 | with | ${{ inputs.名前 }} |
| 秘密情報 | secrets | ${{ secrets.名前 }} |
on.workflow_call.inputs で定義できる型は、文字列、数値、真偽値です。シークレットは別に on.workflow_call.secrets で定義します。([GitHub Docs][3])
secrets: inheritを使う場合の条件
個別にシークレットを列挙する代わりに、呼び出し元が利用できるシークレットをまとめて引き継ぐ方法もあります。
jobs:
call-deploy:
uses: ./.github/workflows/reusable-deploy.yml
secrets: inherit
別リポジトリの例は次のようになります。
jobs:
call-deploy:
uses: example-org/automation/.github/workflows/deploy.yml@v1
secrets: inherit
secrets: inherit は、同一Organization内のリポジトリ間、または同一Enterprise配下のOrganization間で利用できる構成です。OrganizationやEnterpriseの境界を越える場合は、inherit を前提にせず、必要なシークレットを明示的に渡します。([GitHub Docs][2])
inherit を使った場合、呼び出し先では、継承されたシークレットを on.workflow_call.secrets に個別定義していなくても参照できます。([GitHub Docs][2])
ただし、実務では次の基準で使い分けると安全です。
| 方法 | 向いているケース | 注意点 |
|---|---|---|
明示的な secrets | デプロイ、公開処理、別チーム共有 | 必要な秘密情報だけ渡せる |
secrets: inherit | 同一管理下の共通処理 | 呼び出し先から参照できる範囲が広くなる |
| A→B→Cの明示渡し | 多段の共通ワークフロー | 各段階で再度渡す必要がある |
利便性だけで inherit を選ぶのではなく、再利用ワークフローが本当にすべてのシークレットを必要とするかを確認してください。秘密情報を一つしか使わないなら、明示的な受け渡しの方がレビューしやすく、権限も絞れます。
A→B→Cでは各段階でsecretsを渡す
再利用ワークフローをさらに別の再利用ワークフローから呼び出す場合、シークレットは最終地点まで自動転送されません。
たとえば、次の構成を考えます。
ワークフローA
└─ 再利用ワークフローB
└─ 再利用ワークフローC
AからBへ渡しただけでは、Cからそのシークレットを参照できません。BからCへ、もう一度渡す必要があります。
ワークフローA
jobs:
call-b:
uses: ./.github/workflows/b.yml
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
再利用ワークフローB
on:
workflow_call:
secrets:
deploy_token:
required: true
jobs:
call-c:
uses: ./.github/workflows/c.yml
secrets:
api_token: ${{ secrets.deploy_token }}
再利用ワークフローC
on:
workflow_call:
secrets:
api_token:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Use token
env:
API_TOKEN: ${{ secrets.api_token }}
run: |
if [ -z "${API_TOKEN:-}" ]; then
echo "::error::api_token is empty"
exit 1
fi
この例では、境界ごとに名前が変わっています。
AのPROD_DEPLOY_TOKEN
↓
Bのdeploy_token
↓
Cのapi_token
重要なのは元の名前ではなく、各呼び出し境界でどの名前に対応付けたかです。
GitHub Actionsのシークレットは、直接呼び出した再利用ワークフローにだけ渡されます。A→B→Cなら、A→BとB→Cの両方で受け渡しが必要です。([GitHub Docs][2])
environmentはそのまま呼び出し入力として渡せない
Environmentを利用している場合は、通常のリポジトリシークレットとは分けて考える必要があります。
on.workflow_call には、Environmentそのものを受け取る environment 定義はありません。そのため、「呼び出し元ジョブのEnvironmentを、再利用ワークフローへそのまま移す」という考え方はできません。([GitHub Docs][2])
Environment名を文字列の入力値として受け取り、呼び出し先のジョブで選択することはできます。
on:
workflow_call:
inputs:
environment_name:
required: true
type: string
jobs:
deploy:
environment:
name: ${{ inputs.environment_name }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"
ただし、これはEnvironmentを受け渡しているのではありません。呼び出し先のジョブが、入力された文字列を使ってEnvironment名を選択しているだけです。ジョブのEnvironment名には inputs の式を利用できます。([GitHub Docs][3])
特に注意したいのが、同じ名前のシークレットが重複している場合です。
たとえば、呼び出し元から次のように渡したとします。
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
一方、呼び出し先のジョブが指定するEnvironmentにも deploy_token または対応する同名のシークレットが存在する場合、呼び出し先のEnvironmentシークレットが使われます。呼び出し元から渡した値を使っているつもりでも、実際にはEnvironment側の値が参照される可能性があります。([GitHub Docs][2])
混乱を避けるには、次のどちらかに統一します。
呼び出し元がシークレットを管理する設計
- 呼び出し元から
secretsで明示的に渡す - 呼び出し先に同名のEnvironmentシークレットを置かない
- 再利用ワークフローは渡された値だけを使う
呼び出し先のEnvironmentがシークレットを管理する設計
- 呼び出し先のジョブでEnvironmentを指定する
- シークレットは呼び出し先のEnvironmentから取得する
- 呼び出し元から同名のシークレットを渡さない
両方から同じ名前を供給すると、設定を見ただけでは実際に使われる値を判断しにくくなります。
シークレットの問題とpermissionsの問題を分ける
シークレットが正常に渡っていても、GitHub APIやPackage Registryへのアクセスが失敗することがあります。その場合は、permissions の不足を疑います。
jobs:
call-deploy:
permissions:
contents: read
packages: write
uses: ./.github/workflows/reusable-deploy.yml
secrets:
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
入れ子になった再利用ワークフローでは、権限を途中で強化できません。上位ワークフローの権限を維持するか、さらに制限することはできますが、呼び出し先だけで強い権限へ引き上げることはできません。([GitHub Docs][2])
症状によって切り分けると判断しやすくなります。
| 症状 | 主に確認する箇所 |
|---|---|
| 環境変数が空 | secrets の受け渡し、登録範囲、名前 |
| シークレット確認は成功するがAPIが403 | permissions、対象リソース側の権限 |
| 想定と異なる値が使われる | Environmentシークレットの重複 |
| Bでは使えるがCでは空 | B→Cの secrets 設定 |
| ワークフロー開始前にエラー | workflow_call の定義と渡す名前の不一致 |
GITHUB_TOKEN は、カスタムシークレットとは扱いが異なります。再利用ワークフローでは github.token と secrets.GITHUB_TOKEN を利用できますが、その権限は呼び出し元の設定に制約されます。([GitHub Docs][4])
YAMLが正しくてもsecretが空になるケース
受け渡し設定が正しくても、呼び出し元自身がシークレットへアクセスできなければ、右辺の式は空になります。
次の点を確認してください。
Organizationシークレットの対象リポジトリ
Organizationシークレットには、利用できるリポジトリを限定する設定があります。対象リポジトリから呼び出していなければ、secrets: inherit を指定しても利用できません。
Environmentシークレットの適用先
Environmentシークレットは、そのEnvironmentを使用するジョブとの関係を確認します。リポジトリシークレットと同じ感覚で、すべてのジョブから参照できるとは限りません。
フォークからのPull Request
フォークされたリポジトリから実行されるワークフローでは、通常のシークレットはランナーへ渡されません。例外は制限付きの GITHUB_TOKEN です。
Dependabotが起点のイベント
Dependabotが起点となるイベントでも、通常のActionsシークレットは利用できません。
GitHubでは、Organizationシークレットのアクセス対象をリポジトリ単位で制御できます。また、フォーク由来のワークフローやDependabotイベントでは、通常のシークレットが利用できない制約があります。([GitHub Docs][1])
秘密情報を表示せずに確認する方法
トラブル調査のために、シークレットをそのまま echo するのは避けてください。
- name: Unsafe debug
run: echo "${{ secrets.deploy_token }}"
代わりに、値が存在するかだけを判定します。
- name: Check secret presence
shell: bash
env:
DEPLOY_TOKEN: ${{ secrets.deploy_token }}
run: |
if [ -z "${DEPLOY_TOKEN:-}" ]; then
echo "::error::DEPLOY_TOKEN is empty"
exit 1
fi
echo "DEPLOY_TOKEN is set"
この方法なら、秘密情報の中身をログへ出さずに、受け渡しが成功しているか確認できます。
シークレットは可能な限り環境変数や標準入力でコマンドへ渡し、コマンドライン引数やログへ直接出さないようにします。GitHubのマスキングだけに依存せず、ワークフロー側でも値を表示しない設計にしてください。([GitHub Docs][1])
5分で確認できるトラブルシューティング手順
再利用ワークフロー側でシークレットが空になったら、次の順番で確認します。
- 呼び出し元で参照しているシークレットが実際に登録されているか確認する
- Organizationシークレットなら、呼び出し元リポジトリが許可対象か確認する
- 再利用ワークフローを
jobs.<job_id>.usesで呼び出しているか確認する - 呼び出し先に
on.workflow_call.secretsが定義されているか確認する - 呼び出し元の
secrets左辺と、呼び出し先の定義名を照合する - 呼び出し先がマッピング後の名前で参照しているか確認する
secrets: inheritを使う場合は、OrganizationまたはEnterpriseの条件を確認する- A→B→Cなら、B→Cでもシークレットを渡しているか確認する
- 呼び出し先のEnvironmentに同名シークレットがないか確認する
- 値が存在するのに処理が失敗する場合は、
permissionsを確認する
特に間違いやすいのは、次の対応関係です。
secrets:
呼び出し先で使う名前: ${{ secrets.呼び出し元の登録名 }}
親ワークフローで使えていることは、再利用ワークフローでも自動的に使えることを意味しません。再利用ワークフローの呼び出しを一つの境界として考え、inputs、secrets、permissions、environment をそれぞれ明示的に設計することが重要です。
まずは呼び出し元と呼び出し先のYAMLを横に並べ、workflow_call.secrets、呼び出しジョブの secrets、呼び出し先の ${{ secrets.名前 }} の3か所を照合してください。多段構成なら、A→B、B→Cのように一段ずつ確認すると原因を特定しやすくなります。
[1]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets “Using secrets in GitHub Actions – GitHub Docs”
[2]: https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows “Reuse workflows – GitHub Docs”
[3]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax “Workflow syntax for GitHub Actions – GitHub Docs”
[4]: https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations “Reusing workflow configurations – GitHub Docs”

コメント