← Debugging Guides

阅读中文版本

GitHub Actions Secret Not Found — Empty Environment Variable

Fix empty secrets in GitHub Actions by verifying the secret name, scoping it to the right environment, and handling fork and Dependabot restrictions.

intermediatedebugginggithub-actionssecretsci

Symptoms

  • A step fails because ${{ secrets.TOKEN }} expands to an empty string
  • The workflow works on push but not on pull requests from forks
  • Authentication fails with a 401 although the secret is configured

Possible causes

  • The secret name in the workflow differs from the configured name (case-sensitive)
  • The secret is scoped to an environment the job does not declare
  • Fork PRs and Dependabot do not receive repository secrets by default

Diagnosis steps

Confirm the secret exists and its scope

gh secret list --repo <owner>/<repo> && gh secret list --env production

Interpretation

Listing secrets per scope shows whether the name exists at the level your job uses.

Print only whether it is set, never the value

if [ -z "${{ secrets.TOKEN }}" ]; then echo 'TOKEN is empty'; fi

Interpretation

This detects an empty expansion without leaking the value into logs.

Root cause

GitHub resolves secrets at runtime within their scope. A name mismatch, an environment-scoped secret used by a job without that environment, or a fork/Dependabot context all yield an empty value rather than an error, so downstream auth fails.

Fix

Match the secret name exactly, declare environment: production on jobs that use environment secrets, pass secrets explicitly to reusable workflows, and use pull_request_target with care or a separate token flow for fork PRs.

Risk note

pull_request_target with secrets exposes them to untrusted code; never run fork code with secrets in the same job.

Verify the fix

Rerun the job and confirm the secret is non-empty at the point of use; check that auth succeeds and no value appears in logs.

Symptoms

The error “GitHub Actions Secret Not Found — Empty Environment Variable” points at a specific failure mode. This guide documents the symptoms you can observe, the commands that isolate the cause, the root cause, the fix, and the checks that prove the fix works.

Observed symptoms include:

  • A step fails because ${{ secrets.TOKEN }} expands to an empty string
  • The workflow works on push but not on pull requests from forks
  • Authentication fails with a 401 although the secret is configured

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Confirm the secret exists and its scope

gh secret list --repo <owner>/<repo> && gh secret list --env production

Listing secrets per scope shows whether the name exists at the level your job uses.

Step 2: Print only whether it is set, never the value

if [ -z "${{ secrets.TOKEN }}" ]; then echo 'TOKEN is empty'; fi

This detects an empty expansion without leaking the value into logs.

Likely contributors, in order of frequency:

  • The secret name in the workflow differs from the configured name (case-sensitive)
  • The secret is scoped to an environment the job does not declare
  • Fork PRs and Dependabot do not receive repository secrets by default

Root Cause

GitHub resolves secrets at runtime within their scope. A name mismatch, an environment-scoped secret used by a job without that environment, or a fork/Dependabot context all yield an empty value rather than an error, so downstream auth fails.

The Fix

Match the secret name exactly, declare environment: production on jobs that use environment secrets, pass secrets explicitly to reusable workflows, and use pull_request_target with care or a separate token flow for fork PRs.

Watch out: pull_request_target with secrets exposes them to untrusted code; never run fork code with secrets in the same job.

jobs:
  deploy:
    environment: production          # required to see environment secrets
    steps:
      - name: Guard against empty secret
        run: |
          if [ -z "${{ secrets.API_TOKEN }}" ]; then
            echo '::error::API_TOKEN is empty'
            exit 1
          fi
        # never echo the secret value itself

Verify and Prevent

Rerun the job and confirm the secret is non-empty at the point of use; check that auth succeeds and no value appears in logs.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.