GitHub Actions Tutorial — Environment & Secrets Exercises

3 minute read

Practice Exercises — Environment & Secrets

Test your understanding of environment variables, secrets, GITHUB_TOKEN, environments, and OIDC with these hands-on exercises.


Exercise 1 — Environment Variable Scopes

Task: Create a workflow that demonstrates variable override priority:

  1. Set MESSAGE=workflow at workflow level
  2. Set MESSAGE=job at job level
  3. Step 1: Print MESSAGE (should be “job”)
  4. Step 2: Set MESSAGE=step at step level and print it (should be “step”)
  5. Step 3: Print MESSAGE again without step-level override (should be “job”)
Show Solution

name: Variable Scopes

on: workflow_dispatch

env:
  MESSAGE: workflow

jobs:
  demo:
    runs-on: ubuntu-latest
    env:
      MESSAGE: job
    steps:
      - name: Step 1 - Job level wins
        run: echo "MESSAGE=$MESSAGE"
        # Output: MESSAGE=job

      - name: Step 2 - Step level wins
        env:
          MESSAGE: step
        run: echo "MESSAGE=$MESSAGE"
        # Output: MESSAGE=step

      - name: Step 3 - Back to job level
        run: echo "MESSAGE=$MESSAGE"
        # Output: MESSAGE=job

Step-level env: only applies to that step. Once the step ends, the job-level value is back.


Exercise 2 — Secrets and Token Permissions

Task: Create a workflow that:

  1. Sets permissions to read contents and write pull-requests
  2. Checks out the repo
  3. Uses an env variable for a secret called DEPLOY_TOKEN
  4. Prints “Token length: X” using the DEPLOY_TOKEN without exposing the value
Show Solution

name: Secret Usage

on: workflow_dispatch

permissions:
  contents: read
  pull-requests: write

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Use secret safely
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Token length: ${#DEPLOY_TOKEN}"
          echo "Token is set: $([ -n "$DEPLOY_TOKEN" ] && echo yes || echo no)"

Using ${#DEPLOY_TOKEN} prints the string length without exposing the actual value. The echo of the token itself would be masked by GitHub (***).


Exercise 3 — Multi-Environment Deployment

Task: Create a workflow that:

  1. Deploys to staging environment first (auto-deploys)
  2. Then deploys to production environment (has required reviewers)
  3. Each job uses environment-specific API_URL variable and DEPLOY_KEY secret
  4. Include deployment URLs for both environments
Show Solution

name: Multi-Environment Deploy

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.myapp.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        env:
          API_URL: ${{ vars.API_URL }}
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "Deploying to staging..."
          echo "API: $API_URL"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        env:
          API_URL: ${{ vars.API_URL }}
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "Deploying to production..."
          echo "API: $API_URL"

The staging and production environments can have different values for API_URL and DEPLOY_KEY. The production job will wait for approval if required reviewers are configured.


Exercise 4 — Dynamic Configuration

Task: Create a workflow that:

  1. Determines the environment name based on the branch (main → production, anything else → staging)
  2. Sets the environment name as an output
  3. Uses that output in a deploy job’s environment: field
Show Solution

name: Dynamic Environment

on:
  push:
    branches: [main, develop, 'feature/**']

jobs:
  determine-env:
    runs-on: ubuntu-latest
    outputs:
      env-name: ${{ steps.env.outputs.name }}
      env-url: ${{ steps.env.outputs.url }}
    steps:
      - name: Set environment
        id: env
        run: |
          if [ "$GITHUB_REF_NAME" = "main" ]; then
            echo "name=production" >> "$GITHUB_OUTPUT"
            echo "url=https://myapp.com" >> "$GITHUB_OUTPUT"
          else
            echo "name=staging" >> "$GITHUB_OUTPUT"
            echo "url=https://staging.myapp.com" >> "$GITHUB_OUTPUT"
          fi

  deploy:
    needs: determine-env
    runs-on: ubuntu-latest
    environment:
      name: ${{ needs.determine-env.outputs.env-name }}
      url: ${{ needs.determine-env.outputs.env-url }}
    steps:
      - run: echo "Deploying to ${{ needs.determine-env.outputs.env-name }}"

This dynamically sets the environment based on the branch, which means different secrets and protection rules apply automatically.


Exercise 5 — OIDC with AWS

Task: Write the on:, permissions:, and job configuration (just the structure, not actual AWS resources) for a workflow that:

  1. Triggers on push to main
  2. Requests an OIDC token
  3. Authenticates to AWS using OIDC (not access keys)
  4. Deploys files to an S3 bucket
Show Solution

name: Deploy to AWS

on:
  push:
    branches: [main]

permissions:
  id-token: write    # required for OIDC token
  contents: read     # required for checkout

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1
          # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY needed!

      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-app-bucket --delete

Key points:

  • id-token: write is mandatory for OIDC
  • The role-to-assume ARN points to an IAM role configured with a GitHub OIDC trust policy
  • No static credentials are stored — the token is generated fresh each run

← Back to: Chapter 6: Environment & Secrets

Next Chapter: Chapter 7: Expressions & Contexts →

Updated: