GitHub Actions Environment Secrets Empty - environment: Not Set on Job
A secret defined under a GitHub Environment resolves to empty because the job did not set environment:. Environment secrets and variables are only available to jobs explicitly assigned to that environment.
What this error means
A job reads secrets.<NAME> for a value stored as an environment secret and gets an empty string, while repo-level secrets work fine.
Error: required secret DEPLOY_KEY is empty
# DEPLOY_KEY is an environment secret on "production",
# but the job has no environment: productionDiagnose it: what token do you actually have?
Permission failures in Actions are almost never about your repository settings alone. Three things combine: the default GITHUB_TOKEN permission set for the repo or organization, the permissions: block in the workflow, and whether the event is a fork pull request, which downgrades the token to read-only regardless of everything else.
- name: Show the token scopes actually granted
run: |
curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/ | grep -i "^x-oauth-scopes\|^x-accepted"
echo "event: ${{ github.event_name }}"
echo "fork PR: ${{ github.event.pull_request.head.repo.fork }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Common causes
Job missing environment:
Environment-scoped secrets and variables are injected only when the job declares environment:. Without it, those values are not in scope.
Secret stored at the wrong scope
A value saved as an environment secret is not visible as a repo secret, and vice versa. Mismatched scope yields an empty reference.
How to fix it
Assign the job to the environment
Set environment: on the job so its environment secrets and variables load.
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}Confirm the secret scope
- Check whether the secret is defined at repo, environment, or org level.
- Move it to the scope the job uses, or set environment: to match.
- For variables, the same rule applies via the vars context.
Grant the narrowest permission that works
Declaring a permissions: block switches the job from the repository default to exactly what you list, so an incomplete block is a common cause of a new failure right after someone tightened security. List every scope the job needs, not just the one that failed.
permissions:
contents: read # checkout
packages: write # push to GHCR
id-token: write # OIDC to a cloud provider
pull-requests: write # comment on or label a PR
checks: write # publish check runsHow to prevent it
- Set environment: on jobs that consume environment secrets.
- Keep a single documented scope for each secret.
- Verify the secret scope matches how the job is configured.