Lesson 4 of 6

Secrets & Variables

The moment your workflow does more than test — deploys somewhere, publishes a package, calls an API — it needs credentials. And a workflow file is the worst place for a credential: it's in git history forever, visible to everyone with repo access. GitHub's answer is a store outside the repo: secrets for sensitive values, variables for plain configuration.

💡 The Bottom Line

Store credentials in Settings → Secrets and variables → Actions; reference them as ${{ secrets.NAME }}. They're encrypted at rest, never appear in the file, and are masked in logs (printed values become ***). Non-sensitive config uses ${{ vars.NAME }} — same mechanics, visible values, no masking.

Using a secret

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
          SITE_URL: ${{ vars.SITE_URL }}
        run: ./deploy.sh

The pattern to copy: pass the secret into the step as an env: variable, and let the script read $DEPLOY_TOKEN from the environment. Avoid splicing ${{ secrets.X }} directly into run: command text — inlined values can leak through shell tracing and error messages in ways environment variables don't.

Scopes: repository, environment, organization

The token you get for free

Every run receives secrets.GITHUB_TOKEN automatically — a short-lived credential for the GitHub API, scoped to this repo, expiring when the job ends. It's how workflows push commits, comment on PRs, or publish releases without you creating anything. Its default permissions are set repo-wide, and a workflow can (and should) narrow them with a permissions: block — least privilege, one YAML key.

🙋 There Are No Dumb Questions

Q: Can I look at a secret's value after saving it?
A: No — write-only by design. You can overwrite or delete it, never view it. So keep the canonical copy in a real password manager, and treat GitHub as a deployment target for the value, not its home. (And if you're thinking run: echo $MY_SECRET — masking turns it into ***, though clever encodings can defeat masking, which is why the next box exists.)

⚠️ Watch Out

Masking is a safety net, not a guarantee: a secret that's transformed (base64, split across lines, URL-encoded) won't match the masker's pattern and prints in the clear. And logs are visible to everyone who can see the repo — public means public. If a secret ever appears in a log, treat it as burned: rotate the credential, then fix the step that printed it. The CI/CD track's rule applies verbatim: rotate, don't just delete.

🔧 Try It Yourself

Watch masking work. In your scratch repo: Settings → Secrets and variables → Actions → New repository secret, name TEST_SECRET, any value. Then:

on: workflow_dispatch
jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - env:
          S: ${{ secrets.TEST_SECRET }}
        run: |
          echo "The secret is: $S"
          echo "Length: ${#S}"

Run it and read the log: the value prints as ***, while the length prints fine — masked, but real, and genuinely available to your script.

✅ Quick Check

What's the difference between a secret and a variable in GitHub Actions?


Next up: assembling everything so far — checkout, setup, cache, lint, matrix tests, build, artifact — into a real CI pipeline.

← Back to
Next →