GitHub Actions Tutorial — Expressions Exercises

3 minute read

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:

  1. Job build runs on every push
  2. Job deploy-staging runs after build only on the develop branch
  3. Job deploy-production runs after build only on the main branch
  4. Job notify runs 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:

  1. Triggers on push to main
  2. Skips the entire test job if the commit message contains [skip ci] or [no ci]
  3. Always runs a log step 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:

  1. Job setup generates a JSON matrix configuration with Node versions [18, 20, 22] and OS [ubuntu-latest, windows-latest]
  2. Job test uses fromJSON() to consume the dynamic matrix
  3. 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:

  1. A test step that runs exit 1 (simulated failure) with continue-on-error: true
  2. A step that runs only if the test failed — uploads a (fake) error report
  3. 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:

  • outcome is failure (the actual result)
  • conclusion is success (because continue-on-error: true overrides it)
  • We check outcome to detect the real failure, while conclusion reflects the effective result for the workflow

← Back to: Chapter 7: Expressions

Next Chapter: Chapter 8: Artifacts & Caching →

Updated: