GitHub Actions Tutorial — Expressions Exercises
Practice Exercises — Expressions & Contexts
Test your understanding of expressions, contexts, conditionals, and built-in functions with these hands-on exercises.
Exercise 1 — Branch-Based Deployment
Task: Create a workflow where:
- Job
buildruns on every push - Job
deploy-stagingruns afterbuildonly on thedevelopbranch - Job
deploy-productionruns afterbuildonly on themainbranch - Job
notifyruns after all jobs, always, and prints which environment was deployed
Show Solution
name: Branch Deploy
on:
push:
branches: [main, develop]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to staging..."
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production..."
notify:
needs: [build, deploy-staging, deploy-production]
if: always()
runs-on: ubuntu-latest
steps:
- run: |
echo "Build: ${{ needs.build.result }}"
echo "Staging: ${{ needs.deploy-staging.result }}"
echo "Production: ${{ needs.deploy-production.result }}"
Exercise 2 — Skip CI Based on Commit Message
Task: Create a workflow that:
- Triggers on push to
main - Skips the entire
testjob if the commit message contains[skip ci]or[no ci] - Always runs a
logstep that prints the commit message
Show Solution
name: Skip CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[no ci]')
steps:
- uses: actions/checkout@v4
- run: npm test
log:
runs-on: ubuntu-latest
steps:
- run: |
echo "Commit message: ${{ github.event.head_commit.message }}"
echo "Author: ${{ github.event.head_commit.author.name }}"
The >- YAML syntax creates a multi-line string without trailing newlines, making complex conditions more readable.
Exercise 3 — Dynamic Matrix from JSON
Task: Create a workflow where:
- Job
setupgenerates a JSON matrix configuration with Node versions[18, 20, 22]and OS[ubuntu-latest, windows-latest] - Job
testusesfromJSON()to consume the dynamic matrix - Each test run prints the OS and Node version
Show Solution
name: Dynamic Matrix
on: push
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
steps:
- name: Generate matrix
id: set
run: |
echo 'matrix={"node":[18,20,22],"os":["ubuntu-latest","windows-latest"]}' >> "$GITHUB_OUTPUT"
test:
needs: setup
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: |
echo "OS: ${{ matrix.os }}"
echo "Node: ${{ matrix.node }}"
node --version
This creates 6 jobs (3 Node versions × 2 OS options), all from a dynamically generated matrix.
Exercise 4 — Context Debugging
Task: Create a workflow_dispatch workflow with a debug-level choice input (values: minimal, full). When full is selected, dump the entire github context as JSON. Always print the run number and actor.
Show Solution
name: Debug Contexts
on:
workflow_dispatch:
inputs:
debug-level:
description: 'Debug level'
type: choice
options:
- minimal
- full
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Basic info
run: |
echo "Run #${{ github.run_number }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
- name: Full github context
if: inputs.debug-level == 'full'
run: echo '${{ toJSON(github) }}'
Exercise 5 — Error Handling with Status Functions
Task: Create a workflow with three steps:
- A test step that runs
exit 1(simulated failure) withcontinue-on-error: true - A step that runs only if the test failed — uploads a (fake) error report
- A step that always runs and prints the outcome and conclusion of the test step
Show Solution
name: Status Functions
on: push
jobs:
demo:
runs-on: ubuntu-latest
steps:
- name: Run tests (will fail)
id: test
continue-on-error: true
run: |
echo "Running tests..."
exit 1
- name: Upload error report
if: steps.test.outcome == 'failure'
run: echo "Uploading error report..."
- name: Summary
if: always()
run: |
echo "Test outcome: ${{ steps.test.outcome }}"
echo "Test conclusion: ${{ steps.test.conclusion }}"
Key points:
outcomeisfailure(the actual result)conclusionissuccess(becausecontinue-on-error: trueoverrides it)- We check
outcometo detect the real failure, whileconclusionreflects the effective result for the workflow
← Back to: Chapter 7: Expressions
Next Chapter: Chapter 8: Artifacts & Caching →