GitHub Actions Tutorial — Environment & Secrets Exercises
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:
- Set
MESSAGE=workflowat workflow level - Set
MESSAGE=jobat job level - Step 1: Print
MESSAGE(should be “job”) - Step 2: Set
MESSAGE=stepat step level and print it (should be “step”) - Step 3: Print
MESSAGEagain 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:
- Sets permissions to read contents and write pull-requests
- Checks out the repo
- Uses an env variable for a secret called
DEPLOY_TOKEN - Prints “Token length: X” using the
DEPLOY_TOKENwithout 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:
- Deploys to
stagingenvironment first (auto-deploys) - Then deploys to
productionenvironment (has required reviewers) - Each job uses environment-specific
API_URLvariable andDEPLOY_KEYsecret - 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:
- Determines the environment name based on the branch (
main→ production, anything else → staging) - Sets the environment name as an output
- 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:
- Triggers on push to
main - Requests an OIDC token
- Authenticates to AWS using OIDC (not access keys)
- 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: writeis mandatory for OIDC- The
role-to-assumeARN 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 →