Skip to content
Latchkey LogoLatchkey home

GitHub Actions composite action cannot access secrets directly

A GitHub Actions composite action cannot access secrets because the secrets context is not one of the names the expression parser recognizes inside an action.yml. The failure is a parse error rather than an empty value, so nothing runs at all.

The names a composite action file may use, beside the one its parser rejects outright
The runner builds the message in ParseException as the description, the raw token in quotes, then the position and expression when a token is available.

What this error means

The run fails immediately with an annotation naming a file, a line and a column, and no job appears in the run at all. The file named is the action's action.yml, which may live in another repository entirely, so the first place you look is the workflow that called it and the workflow is fine. The message quotes the offending name in single quotes and, when the parser has a token to point at, appends the position and the whole expression.

Actions log, quoted from BrunoSobrino/TheMystic-Bot-MD#384
[error] thomasbnt/hello-to-news-contributions/v1.0/action.yml (Line: 14, Col: 23): Unrecognized named-value: 'secrets'

The message is built by the expression parser

This is not the action complaining and it is not a permission check. ParseException in the runner assembles the text from a fixed description for the kind of failure, the raw token that failed, and where it sat. The UnrecognizedNamedValue kind maps to the description Unrecognized named-value, and the message becomes that description, a colon, the token in single quotes, and then, when a token object is available, . Located at position with a one-based index within expression: and the whole expression.

So the parser was reading an expression, reached the name secrets, and had no such name in the set it was given for that file. It is the same failure you would get from a misspelled context. Nothing was evaluated, nothing was empty, and no job was scheduled.

Common causes

The action.yml references secrets directly

The literal case. A composite step uses ${{ secrets.NPM_TOKEN }} in a run, an env or a with, having been written by copying a workflow step into an action. The name is valid in the file it came from and not in the file it landed in.

A step-level or job-level if tests a secret

The same message from a plain workflow. Guarding work on whether a secret is set is a reasonable thing to want, and if is the obvious place to put it, but secrets is not available in either if. This is the version that appears in repositories with no composite actions at all.

A secret is referenced in a key that does not accept it

Less common and harder to spot, because the same file uses secrets successfully a few lines away. Keys like runs-on, strategy and a job's environment do not accept the context, so one expression fails while its neighbors are fine.

The action was refactored from a reusable workflow

Reusable workflows do accept a secrets block, so moving one to a composite action moves code that was correct into a file where it is not. The diff looks like a simple relocation, which is why the error arrives as a surprise.

How to fix it

Declare the secret as an input on the action

Add an input to the action and read it through the inputs context. Composite actions do not receive INPUT_ environment variables automatically, so use the context rather than the variable.

action.yml (illustrative)
name: publish
inputs:
  npm-token:
    description: 'Token used to publish'
    required: true
runs:
  using: composite
  steps:
    - shell: bash
      env:
        NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
      run: npm publish

Pass it explicitly at the call site

The caller is the only place that can read the secret, so it hands it over as an ordinary input. The value remains masked in logs.

.github/workflows/release.yml (illustrative)
      - uses: ./.github/actions/publish
        with:
          npm-token: ${{ secrets.NPM_TOKEN }}

For an if condition, go through env

Assign the secret to an environment variable at the job or step level, where the context is accepted, and test the variable in the condition. This is the supported shape for branching on whether a secret is configured.

.github/workflows/release.yml (illustrative)
jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
    steps:
      - if: env.NPM_TOKEN != ''
        run: npm publish

Consider a reusable workflow when there are many secrets

A reusable workflow accepts a secrets block and supports secrets: inherit, so it is the better shape when a unit of work genuinely needs several secrets. A composite action is the better shape when it needs one or two and should be usable from other repositories.

Where secrets resolves and where it does not

Availability is per key, not per file, and GitHub publishes the list. These are the four places people most often try, and only two of them work.

Key the expression sits inIs secrets an accepted nameWhat to use instead
An action.yml of a composite actionNo, at any keyAn input declared on the action and passed by the caller
jobs.<job_id>.if in a workflowNoCopy the secret into env on the job and test env.NAME
jobs.<job_id>.steps.if in a workflowNoCopy the secret into env on the step and test env.NAME
jobs.<job_id>.steps.with and steps.envYesNothing, this is where a secret is passed in

Why the restriction exists and what replaces it

GitHub states the rule and the reason together: "The secrets context is not available for composite actions due to security reasons. If you want to pass a secret to a composite action, you need to do it explicitly as an input." A composite action is code that runs inside your job, frequently from another repository at a tag that can move, and giving it ambient access to every secret in the run would make reviewing what it can reach impossible.

Passing secrets as inputs is therefore the feature rather than the workaround. It also has a property worth wanting: the call site lists exactly which secrets the action receives, so a reader of the workflow can see the blast radius without opening the action.

Why there is no recorded run on this page

There is nothing to record, in the strongest sense on this page: the expression fails to parse, so the run is rejected before any job is created, no runner is assigned, and no step ever exists. The whole of the evidence is the annotation and the grammar that produced it, and we read that grammar in the runner's own ParseException rather than inferring it from examples. The annotation above is quoted from a public pull request where the failing file belonged to a third-party action.

How to prevent it

  • Treat an action's inputs as its whole interface, and write every credential it needs into that list.
  • When moving steps out of a workflow into an action, rewrite every secrets. reference in the same commit.
  • Guard on env rather than on secrets, everywhere, so the habit never produces this failure.
  • Read the context availability table before putting an expression in an unfamiliar key.

Frequently asked questions

Why is this a parse error rather than an empty value?
Because the name is not defined in that file at all. The runner validates expressions against the set of names allowed in the key being read, and an unknown name is rejected during parsing. There is no evaluation, so there is no empty string to observe, and no job is created to observe it in.
Can I use secrets anywhere in an action.yml?
No. The secrets context is not available in a composite action at any key, which GitHub states with its reason: it is withheld for security, and a secret should be passed explicitly as an input. Inputs are the supported channel and they also document at the call site which secrets the action can see.
Why does my workflow get this error with no composite action involved?
Because secrets is also unavailable in if conditions, at both job and step level. Writing if: secrets.NAME != '' produces the same message from an ordinary workflow file. Assign the secret to env first and test env.NAME in the condition instead.
What does the position in the message refer to?
It is a one-based index into the expression that failed, followed by that expression in full. The runner adds both only when it has a token to point at, which is why some annotations carry just the name in quotes. The line and column before it locate the expression in the file.

Related guides

References

Composite actions run unchanged on Latchkey runners, at $0.0025/min at 2 vCPU against $0.006. Start free → 30-day trial · No credit card