Pipelines need configuration: API keys, deployment targets, feature flags. But NOT all configuration is equal — some is sensitive. This lesson teaches you GitHub's configuration hierarchy and how to use it safely.

The Configuration Hierarchy

GitHub Actions provides three levels of configuration, each with different visibility and security:

🔒 Secrets Sensitive values AZURE_CLIENT_ID DB_PASSWORD DEPLOY_TOKEN Properties: • Encrypted at rest • Masked in logs (***) • Never shown after creation • Not available in forks $​{{ secrets.NAME }} 📋 Variables Non-sensitive config APP_NAME = "my-api" CLUSTER_NAME = "aks-prod" REGION = "eastus" Properties: • Visible in settings UI • Shown in logs (plain text) • Good for config that changes • Available in forks (public) $​{{ vars.NAME }} 🌍 Environments Per-env config + controls staging APP_URL, DB_HOST (different!) production APP_URL, DB_HOST (different!) Controls: • Required reviewers • Wait timer • Branch restrictions • Own secrets + variables environment: production
Three configuration mechanisms — use the right one for the right purpose

Secrets: Handling Sensitive Data

Setting Secrets

Repository → Settings → Secrets and variables → Actions → New repository secret

Hardcode secrets in YAML:

run: curl -H "Token: abc123secret"

Visible in git history FOREVER

Use secrets context:

run: curl -H "Token: ${{ secrets.API_TOKEN }}"

Encrypted, masked in logs, never exposed

How Secrets Appear in Logs

$ echo "Deploying with token: $API_TOKEN" Deploying with token: *** ↑ GitHub automatically masks the secret value
Secrets are masked in logs but NOT impossible to leak. Never echo a secret to a file, embed it in a URL, or pass it to an untrusted action. If you accidentally expose one, rotate immediately.

Variables: Non-Sensitive Configuration

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to ${{ vars.ENVIRONMENT }}
        run: |
          echo "Deploying to ${{ vars.APP_NAME }}"
          echo "Region: ${{ vars.AZURE_REGION }}"
          echo "Cluster: ${{ vars.CLUSTER_NAME }}"

Variables are for things like app names, regions, cluster names — anything you want configurable but NOT secret.

Environments: Per-Stage Configuration + Approval Gates

Environments are the most powerful configuration mechanism. They combine secrets + variables + deployment controls.

CI Jobs lint, test, build environment: staging ✅ Auto-deploys (no gates) Own secrets: STAGING_DB_URL Own vars: APP_URL 👤 environment: production 🛑 Requires approval Own secrets: PROD_DB_URL Wait timer: 5 min Environment Protection Rules 🔲 Required reviewers — named people must approve before the job runs ⏱️ Wait timer — mandatory delay (e.g., 5 min) to allow cancellation 🌿 Deployment branches — only main can deploy to production
Environments give you per-stage secrets AND approval gates — essential for production safety

Using Environments in YAML

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.myapp.com  # Shown in GitHub UI
    steps:
      - run: echo "DB: ${{ secrets.DB_URL }}"
        # ↑ Gets the STAGING-specific DB_URL secret!

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production        # ← This triggers the approval gate!
      url: https://myapp.com
    steps:
      - run: echo "DB: ${{ secrets.DB_URL }}"
        # ↑ Gets the PRODUCTION-specific DB_URL secret!
Same secret name, different values per environment. Your workflow code stays identical across environments — only the configuration changes. This is the secret to clean multi-env pipelines.

🏋️ Exercise: Set Up Environments

Step 1: Create environments in GitHub

  1. Go to repo → Settings → Environments
  2. Create "staging" — no protection rules
  3. Create "production" — add yourself as required reviewer
  4. Add a variable DEPLOY_TARGET to each (value: "staging-server" / "production-server")

Step 2: Create a deployment workflow

# .github/workflows/deploy.yml
name: Deploy

on:
  workflow_dispatch:  # Manual trigger for now

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "🚀 Deploying to ${{ vars.DEPLOY_TARGET }}"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: echo "🚀 Deploying to ${{ vars.DEPLOY_TARGET }}"

Step 3: Trigger and observe

  1. Go to Actions → Deploy → Run workflow
  2. Watch staging deploy automatically
  3. See production pause and wait for your approval
  4. Click "Review deployments" → Approve → production deploys

🎉 You just built a gated deployment pipeline!

The env: Keyword (Workflow-Level)

For values that aren't secrets or GitHub-managed variables, use inline env::

# Workflow-level (available to all jobs)
env:
  NODE_ENV: production
  IMAGE_NAME: my-app

jobs:
  build:
    # Job-level (only this job)
    env:
      CI: true
    steps:
      - name: Use env vars
        # Step-level (only this step)
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: echo "$NODE_ENV $IMAGE_NAME $CI"

Decision Framework: What Goes Where?

DataWhere to StoreWhy
Azure client IDSecretAuth credential
Database passwordEnvironment secretDifferent per env
App nameVariableNot sensitive, changes rarely
Cluster name per envEnvironment variableDifferent per env, not secret
NODE_ENV=productionInline env:Static, not configurable
Image registry URLVariableNot sensitive, shared across jobs

🧠 Recall Check

  1. What's the syntax difference between accessing a secret and a variable?
  2. Can you read a secret's value after setting it in the GitHub UI?
  3. How do you make a deployment job require human approval?
  4. If staging and production both have a secret called DB_URL, how does the workflow know which to use?
Reveal answers
  1. ${{ secrets.NAME }} vs ${{ vars.NAME }}
  2. No. Secrets are write-only from the UI. Once saved, you can only update or delete them, never view the value.
  3. Add environment: production to the job, and configure the "production" environment with required reviewers in Settings → Environments.
  4. The environment: key on the job determines which environment's secrets are injected. environment: staging → staging's DB_URL. environment: production → production's DB_URL.
You now know the full configuration model: Secrets for sensitive data, Variables for non-sensitive config, Environments for per-stage isolation + approval gates. Together, they let you build one workflow that deploys safely to any number of environments.

Next lesson: Artifacts & Job Outputs — passing files and data between jobs.