GitHub Actionsの再利用ワークフローにsecretsが渡らない原因と解決手順

親ワークフローでは ${{ 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が403permissions、対象リソース側の権限
想定と異なる値が使われるEnvironmentシークレットの重複
Bでは使えるがCでは空B→Cの secrets 設定
ワークフロー開始前にエラーworkflow_call の定義と渡す名前の不一致

GITHUB_TOKEN は、カスタムシークレットとは扱いが異なります。再利用ワークフローでは github.tokensecrets.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分で確認できるトラブルシューティング手順

再利用ワークフロー側でシークレットが空になったら、次の順番で確認します。

  1. 呼び出し元で参照しているシークレットが実際に登録されているか確認する
  2. Organizationシークレットなら、呼び出し元リポジトリが許可対象か確認する
  3. 再利用ワークフローを jobs.<job_id>.uses で呼び出しているか確認する
  4. 呼び出し先に on.workflow_call.secrets が定義されているか確認する
  5. 呼び出し元の secrets 左辺と、呼び出し先の定義名を照合する
  6. 呼び出し先がマッピング後の名前で参照しているか確認する
  7. secrets: inherit を使う場合は、OrganizationまたはEnterpriseの条件を確認する
  8. A→B→Cなら、B→Cでもシークレットを渡しているか確認する
  9. 呼び出し先のEnvironmentに同名シークレットがないか確認する
  10. 値が存在するのに処理が失敗する場合は、permissions を確認する

特に間違いやすいのは、次の対応関係です。

secrets:
  呼び出し先で使う名前: ${{ secrets.呼び出し元の登録名 }}

親ワークフローで使えていることは、再利用ワークフローでも自動的に使えることを意味しません。再利用ワークフローの呼び出しを一つの境界として考え、inputssecretspermissionsenvironment をそれぞれ明示的に設計することが重要です。

まずは呼び出し元と呼び出し先の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”

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次