Skip to content
Latchkey LogoLatchkey home

Unable to get ACTIONS_ID_TOKEN_REQUEST_URL in GitHub Actions

Unable to get ACTIONS_ID_TOKEN_REQUEST_URL is thrown by @actions/core when an environment variable it expects is absent, before any request is made to GitHub or to a cloud provider. The runner never received the variable, so nothing on the machine can produce it.

Two OIDC environment variables and the order the toolkit reads them when minting a token
getIDToken reads the URL first, so the URL message is the one you see even though both variables are absent together. The catch block then prefixes it with "Error message: ".

What this error means

A cloud login, an attestation step or any action that mints an OIDC token fails within a second of starting. No provider is contacted, so there is no cloud-side error to look at and no audit entry anywhere. The line in the log is rarely the bare sentence: it usually arrives with one or two prefixes bolted on by the code that asked for the token, and those prefixes are the most useful part.

Actions log, quoted from astropy/extension-helpers#156
Error: Unhandled error: Error: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable

Where the sentence comes from, and what wraps it

The string is thrown in OidcClient.getIDTokenUrl() in @actions/core, in a two line branch: read process.env.ACTIONS_ID_TOKEN_REQUEST_URL, and if it is falsy, throw. That is the entire test. The variable is injected into the job environment by the Actions service, so its absence is decided before the runner starts your step.

The wrapper matters as much as the message. getIDToken() calls that function inside a try, and its catch rethrows with a fixed prefix: the new error's message is Error message: followed by the original. So the bare sentence essentially never appears alone in a log. Whatever sits in front of it was added by the code that asked for the token, which makes the prefix a reliable way to identify the caller.

Common causes

The job did not request id-token: write

The dominant cause. GitHub injects the two variables only for jobs that hold the id-token permission at write, so without it the environment is simply missing them. There is no warning at startup and no mention in the job summary beyond the permissions list itself.

The permission was declared somewhere it does not reach the job

Written at the workflow level in a file where a job also has its own permissions block, the job block replaces the workflow one rather than merging with it, so the job silently loses id-token. This is the version where the file plainly contains the right line and the job still has nothing.

A reusable workflow was called without the permission at the caller

The called workflow cannot grant itself more than the caller has. A job inside it that needs OIDC gets nothing unless the caller's job declared id-token: write, and the file that fails is not the file that has to change.

An organization or enterprise policy caps what the token may hold

Worth ruling out when the workflow is provably correct. A restrictive default or an explicit policy can withhold the scope, and the symptom is identical to forgetting the line. The run's own permissions listing is where the request and the grant can be compared.

How to fix it

Declare the permission on the job that mints the token

Put it on the job rather than the workflow, so it applies where it is needed and nothing else gains it. GitHub notes that this setting "only enables fetching and setting the OIDC token; it does not grant write access to other resources", so it is not a broad grant.

.github/workflows/deploy.yml (illustrative)
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v5

Remember that a job block replaces the workflow block

If the job has its own permissions, every scope it needs has to be listed there, including contents: read for checkout. Adding id-token: write at the workflow level will not reach a job that overrides it.

Declare it at the caller for a reusable workflow

GitHub is explicit about this for workflows outside your organization: set id-token to write "explicitly at the caller workflow or job level", which "ensures the OIDC token is only available to intended caller workflows". Change the calling file, not the called one.

.github/workflows/release.yml (illustrative)
jobs:
  call-deploy:
    permissions:
      id-token: write
      contents: read
    uses: ./.github/workflows/deploy.yml

Confirm the grant rather than the request

Open the failing job and read the permissions it was actually given. If the workflow asks for id-token: write and the job did not receive it, no edit to the file will help and the next question is an organization policy.

Two variables, read in a fixed order

The OIDC handshake needs both an endpoint and a bearer token for that endpoint, and the toolkit reads them at different moments. When the permission is absent, neither is present, and yet only one message can be first.

VariableWhich toolkit step reads itWhen it is reached
ACTIONS_ID_TOKEN_REQUEST_URLgetIDTokenUrl(), the first call inside getIDToken()Always first, so this is the message you see
ACTIONS_ID_TOKEN_REQUEST_TOKENgetRequestToken(), when the HTTP client is constructedOnly after a URL was found, so it is rare in practice

What the prefix tells you about the caller

Because getIDToken() always adds Error message: , anything in front of that came from your step. A line reading Failed to get OIDC token: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable was produced by an action that caught the toolkit error and added its own description. A line with a doubled Error: Unhandled error: Error: in front came from an action that let the rejection escape to its top-level handler.

Neither prefix changes the cause, and neither is worth debugging on its own. What they are good for is identifying which step asked, which is genuinely useful when several steps in a job could have been the one that wanted a token.

Why there is no recorded run on this page

A recorded run here would be a video of an absence. The failing branch reads one environment variable, finds nothing, and throws, all before a socket is opened, so the log holds exactly one line and that line is already quoted above from a public issue. The part worth establishing is which of the two variables is read first and what the wrapper adds, and neither of those is observable from a log at all: they are facts about the toolkit, which we read in its source instead.

How to prevent it

  • Keep id-token: write on the smallest job that needs it, so a refactor that splits jobs makes the loss visible.
  • When a job declares any permissions, list every scope it needs rather than relying on a workflow-level block.
  • Set the permission at the caller whenever a reusable workflow performs the login.
  • Prefer OIDC over stored cloud credentials, since the failure mode here is a missing line rather than a leaked key.

Frequently asked questions

Why does my log say "Error message:" twice over?
Because two layers added text. getIDToken() catches the original throw and rethrows with a fixed Error message: prefix, and whatever called it usually adds a description of its own. The repetition is normal and carries no extra meaning beyond telling you which step asked for the token.
Do I need id-token: write for a job that only reads?
You need it for any job that mints an OIDC token, whatever it does afterwards. The name is misleading: GitHub documents that the setting only enables fetching and setting the OIDC token and does not grant write access to other resources. A read-only deployment that authenticates over OIDC still requires it.
Why is the other variable never the one that fails?
Because of the order. getIDToken() resolves the URL before it builds the HTTP client that reads the bearer token, so the URL branch is always reached first. The two variables are injected together and withheld together, which means the second branch is effectively unreachable in the ordinary case.
Can a step set the variable itself as a workaround?
No, and there is nothing to set it to. The endpoint and its bearer token are issued by the Actions service for a job that was authorized to have them; they are not values the runner can compute. A job that could grant itself an OIDC token would defeat the point of the permission.

Related guides

References

You are already editing this job block. The line above the permission is Latchkey, at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card