GitHub Actions

The Secrets Debugging Checklist: Why Your API Key Isn't Reaching the CI Job

GitHub Actions secret not found? GitLab CI variable empty? Step through this security-conscious checklist to find why secrets aren't available in your CI jobs — including the fork PR trap.

D

Daxtack Team

Engineering

7 min read

Your CI pipeline was working fine. Then someone adds a new API integration, sets up the secret in GitHub, references it in the workflow — and the job fails. The API call returns 401 Unauthorized, or worse, the secret value is just empty and the step silently does the wrong thing.

Secrets in CI are a special kind of debugging challenge: you can't print them to verify they're correct (and you shouldn't), so the normal "add a console.log" approach doesn't work. You need a different diagnostic strategy.

The Most Common Cause: Fork PRs Don't Get Secrets

This catches every team exactly once, and it's by design.

When a contributor opens a pull request from a fork of your repository, GitHub Actions does not expose your repository secrets to the workflow. This is a critical security feature — if it didn't work this way, anyone could fork your repo, add echo ${{ secrets.AWS_KEY }} to the workflow, and steal your credentials.

But the error message doesn't say "secrets are unavailable because this is a fork PR." The secret variable simply resolves to an empty string, and whatever step depends on it fails downstream with a confusing, unrelated error.

How to handle this:

  • Use pull_request_target instead of pull_request if you need secrets on fork PRs (but understand the security implications — the workflow runs in the context of the base branch)
  • Or, split your workflow: run tests (no secrets needed) on pull_request, run integration tests (secrets needed) on pull_request_target with explicit checkout of the PR commit
  • Or, skip secret-dependent steps on forks:
- name: Integration test
  if: github.event.pull_request.head.repo.full_name == github.repository
  run: npm run test:integration
  env:
    API_KEY: ${{ secrets.API_KEY }}

Secret Scope Issues

GitHub has three levels of secret scope, and mixing them up causes silent failures:

ScopeAccessible ByCommon Mistake
Repository secretsAll workflows in that repoNone — this is the default and usually correct
Environment secretsOnly jobs that specify environment:Forgetting to add environment: production to the job
Organization secretsRepos granted access via policyNew repo isn't added to the org secret's access list

The environment trap: If you store a secret in the "production" environment but your job doesn't declare environment: production, the secret simply doesn't exist from the job's perspective:

# Broken: secret is in "production" environment but job doesn't specify it
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: curl -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" ...

# Fixed: declare the environment
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production    # ← This makes environment secrets available
    steps:
      - run: curl -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" ...

Naming Mismatches

Simpler than it sounds, and surprisingly common:

  • Case sensitivity — GitHub secret names are case-insensitive, but the reference in your workflow is case-sensitive in some contexts. secrets.api_key vs secrets.API_KEY — use the exact name you set
  • Typossecrets.DATABSE_URL (note the missing A) won't throw an error. It just returns an empty string. There's no "secret not found" warning
  • GitLab difference — GitLab CI/CD variables are case-sensitive and use $VARIABLE_NAME syntax instead of ${{ secrets.NAME }}

Safe Verification Without Leaking

Never print a secret value. Instead, verify that a secret exists and is non-empty without revealing its contents:

# GitHub Actions — check existence
- name: Verify secrets
  run: |
    if [ -z "$API_KEY" ]; then
      echo "::error::API_KEY secret is not set or empty"
      exit 1
    fi
    echo "API_KEY is set (length: ${#API_KEY})"
  env:
    API_KEY: ${{ secrets.API_KEY }}

# Safer: just check the expression directly
- name: Check secret
  if: secrets.API_KEY == ''
  run: |
    echo "::error::API_KEY is not configured"
    exit 1

For GitLab CI:

# .gitlab-ci.yml
verify_secrets:
  stage: .pre
  script:
    - |
      if [ -z "$DEPLOY_TOKEN" ]; then
        echo "DEPLOY_TOKEN is not set"
        exit 1
      fi
      echo "DEPLOY_TOKEN is set (length: ${#DEPLOY_TOKEN})"

Expired or Rotated Credentials

Secrets that worked last month might be expired now:

  • API tokens — many services issue tokens with 90-day expiry by default
  • OAuth tokens — refresh tokens can expire if unused for too long
  • AWS keys — security-conscious orgs rotate keys regularly but may not update CI

Prevention:

  • Use OIDC tokens instead of static secrets where possible. GitHub Actions supports OIDC natively for AWS, GCP, and Azure — no stored credentials needed
  • Set calendar reminders for credential rotation dates
  • Add a .pre stage that validates credentials before running expensive jobs

The Complete Checklist

  1. ☐ Is this a fork PR? Secrets aren't available on fork PRs by default
  2. ☐ Is the secret name spelled correctly? (No typos, correct case)
  3. ☐ Is the secret at the right scope? (Repo vs environment vs org)
  4. ☐ Does the job declare the correct environment:?
  5. ☐ Is the secret actually set? (Check Settings → Secrets)
  6. ☐ Has the credential expired or been rotated?
  7. ☐ For GitLab: is the variable protected? Protected variables only work on protected branches
  8. ☐ For GitLab: is the variable masked? Masked variables must match a regex pattern

Since you can't (and shouldn't) print secret values in logs, this is one of the trickier failure classes to auto-diagnose. Daxtack flags the pattern — job fails right after a step that references a secret — without ever touching the secret value itself.

SecretsGitHub ActionsGitLab CICI/CDAPI KeysSecurityEnvironment VariablesDebugging

Debug CI/CD failures in 30 seconds

Daxtack uses AI to automatically analyze your build logs, find the root cause, and suggest fixes — right in your pull request.

Related Articles