actions / actions/runner

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

Open
#4,453 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
6.3k
Forks
1.4k
Avg merge
1d 16h
Merged PRs (30d)
24

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the caller and reusable workflow examples in .github/workflows/caller.yml and .github/workflows/reusable.yml with and without secrets: inherit. Compare the workflow_dispatch results for MY_SECRET, then trace how reusable-workflow environment and secret contexts are handled. Done means the behavior is explained and either the environment secret resolves as expected or the required documentation change is identified.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, github-actions
Domain
ci-cd, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
44/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.