We recently migrated our Docker build workflows to use a shared reusable workflow. The migration looked straightforward: extract the build steps, parameterize the inputs, and call the shared workflow with secrets: inherit. CI immediately broke.
Invalid workflow file (Line: 15, Col: 19):
Unrecognized named-value: 'secrets'
The fix took 20 minutes. Understanding why took longer, and every answer led to another "but wait" question.
The Setup
The calling workflow passed a Rails master key as a build arg:
jobs:
build:
uses: our-org/shared/.github/workflows/build-image.yml@main
secrets: inherit
with:
build_args: |
RAILS_MASTER_KEY=${{ secrets.RAILS_MASTER_KEY }}
This fails at parse time. GitHub validates the workflow file before anything runs and rejects secrets in that with: block.
Job-Level with: Is Not Step-Level with:
This is where it gets confusing, because with: appears in two very different places in a workflow file, and they have different rules.
Step-level with: passes inputs to an action. The secrets context is available here:
steps:
- uses: docker/build-push-action@v6
with:
build-args: RAILS_MASTER_KEY=${{ secrets.RAILS_MASTER_KEY }} # works
Job-level with: passes inputs to a reusable workflow. The secrets context is not available here:
jobs:
build:
uses: org/repo/.github/workflows/shared.yml@main
with:
build_args: ${{ secrets.RAILS_MASTER_KEY }} # fails
The contexts reference documents this in a table. For jobs.<job_id>.with.<with_id>, the allowed contexts are:
github, needs, strategy, matrix, inputs, vars
No secrets.
But the Docs Say Secrets Are Available "From Any Step in a Job"
The secrets context documentation says:
This context is the same for each job in a workflow run. You can access this context from any step in a job.
That's true, for steps. The job-level with: on a reusable workflow call is not a step. It's a job-level declaration that gets parsed and validated with a restricted set of contexts before any job runs.
But I Have secrets: inherit
The reusable workflows docs explain that secrets: inherit implicitly passes secrets to the called workflow. And it does. At runtime, the called workflow's steps can reference secrets.RAILS_MASTER_KEY directly.
The problem is that with: is evaluated on the caller side. secrets: inherit makes secrets available inside the called workflow, not in the caller's with: expression. These are two parallel channels:
-
with:sends named inputs (restricted contexts, validated at parse time) -
secrets:sends secrets (available at runtime in the called workflow's steps)
But What About Org vs Repo Secrets?
Doesn't matter for this error. secrets: inherit passes both org-level and repo-level secrets to the called workflow. The error is a static validation failure at parse time. GitHub isn't even looking at which secrets exist or where they're stored. It's rejecting the secrets context in with: regardless.
The Fix
Pass the secret name as a string through with:. Resolve it inside the called workflow where secrets is available.
In the calling workflow:
with:
master_key_secret: RAILS_MASTER_KEY # plain string, no secrets context
In the shared workflow, add an input and resolve it in the build step:
on:
workflow_call:
inputs:
master_key_secret:
description: "Secret name for Rails master key (empty to skip)"
required: false
type: string
default: ""
# In the build step:
build-args: |
${{ inputs.master_key_secret != '' && format('RAILS_MASTER_KEY={0}', secrets[inputs.master_key_secret]) || '' }}
The secrets[inputs.master_key_secret] pattern dynamically looks up a secret by name. It's the same approach GitHub's own docs use for parameterized registry credentials. The input defaults to empty, so repos that don't need it aren't affected.
The Takeaway
Two things that look the same in YAML, with: on a step and with: on a reusable workflow call, have fundamentally different context availability. If you're migrating from inline steps to reusable workflows and your secrets.* references break, that's why. Pass the name, not the value.
References:
- Contexts reference, context availability table
-
Reusing workflows,
secrets: inheritand workflow inputs - Secrets context, "available from any step in a job"
Top comments (1)
Great writeup — the
withvssecretsdistinction trips up basically everyone the first time they build reusable workflows.One thing worth adding: if you're using
secrets: inheritand the called workflow is in a different repo, you also need to make sure the calling repo has access to those secrets at the org level. I've seen teams debug the YAML syntax for hours only to realize the secret just wasn't shared to that repo.The
docker/build-push-actionwith--secretflag approach is also worth looking at for the Rails master key case specifically — it avoids baking secrets into build args entirely, which means they never appear in the image layer history.