Skip to content

[BUG] Environment-scoped secrets unreachable from reusable workflow without secrets: inherit, despite called job declaring environment #4453

Description

@michaels-omneo-workers

Describe the bug

A reusable workflow declares environment: ${{ inputs.environment_name }} on its job, expecting environment-scoped secrets to resolve via ${{ secrets.MY_SECRET }}. Per the official docs, this should be sufficient:

"If a called workflow needs to access environment secrets, the environment must be defined in the called workflow."

Empirically it isn't sufficient. Every env-scoped secret resolves to "" (empty string) at the called workflow's runtime unless the caller also adds secrets: inherit.

To reproduce

Caller workflow (.github/workflows/caller.yml):

on: workflow_dispatch
jobs:
  call-it:
    uses: ./.github/workflows/reusable.yml
    with:
      target_environment: my-env
    # NO secrets: inherit

Reusable workflow (.github/workflows/reusable.yml):

on:
  workflow_call:
    inputs:
      target_environment:
        required: true
        type: string
    # NO secrets: block declared

jobs:
  worker:
    runs-on: ubuntu-latest
    environment: ${{ inputs.target_environment }}
    steps:
      - name: Show secret length
        env:
          MY_SECRET: ${{ secrets.MY_SECRET }}
        run: |
          echo "MY_SECRET length: ${#MY_SECRET}"
          [ -z "$MY_SECRET" ] && echo "EMPTY" || echo "POPULATED"

Setup: create environment my-env on the repo. Set environment secret MY_SECRET to any non-empty value via UI or gh secret set MY_SECRET --env my-env --repo OWNER/REPO.

Trigger the caller via workflow_dispatch.

Expected behavior

MY_SECRET length: <non-zero> and POPULATED. The reusable's job binds to my-env, and ${{ secrets.MY_SECRET }} resolves from that environment's scope.

Actual behavior

MY_SECRET length: 0 and EMPTY. The step's env: block in the runner log renders:

env:
  MY_SECRET:

(blank after the colon — not *** mask).

The env binding takes effect for things like environment protection rules and vars, but secrets resolve as if the job had no env binding.

Workaround

Add secrets: inherit to the caller:

jobs:
  call-it:
    uses: ./.github/workflows/reusable.yml
    with:
      target_environment: my-env
    secrets: inherit   # <-- this fixes it

After this, MY_SECRET resolves correctly to its env-scoped value.

Why this is confusing

secrets: inherit is documented as passing the caller's secrets to the called workflow. But in this reproduction the caller has no environment binding — so the caller's own scope doesn't include MY_SECRET either. Yet adding inherit somehow enables the resolution. This suggests the inherit mechanism is doing more than just secret transfer; it may be elevating the called workflow's resolution context. Whatever the mechanism, the docs don't describe it.

Real-world impact

A production CI/CD pipeline shipped a matrix of ~12 tenants with per-tenant GitHub Environments holding per-tenant deploy secrets. The reusable workflow pattern (called with environment_name: ${{ matrix.tenant }}) was thought to scope secrets per tenant. It silently didn't — every secret resolved empty.

The failure was masked for over a week because legacy in-cluster Kubernetes Secrets continued providing the same values via envFrom on running pods (Kubernetes pods don't re-read Secret values after start). Workers only crashed on a kustomize-secretGenerator–built Secret whose name changes on every deploy (hash suffix), forcing pod restarts onto the broken value.

The official guidance led to a broken architecture. Either:

  • The docs need to call out the secrets: inherit requirement
  • OR the runner should make env binding actually sufficient

Runner Version and Platform

  • Runner: ubuntu-latest (whatever GHA-hosted version)
  • gh CLI 2.91.0 — but issue is independent of the CLI; UI-uploaded secrets behave the same

What's not working

Environment-scoped secrets in reusable workflows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions