GitHub Actions Tutorial — Jobs & Runners Exercises

2 minute read

Practice Exercises — Jobs & Runners

Test your understanding of job configuration, dependencies, outputs, and containers with these hands-on exercises.


Exercise 1 — Diamond Pipeline

Task: Create a workflow with this execution pattern:

              ┌──────┐
         ┌───►│ lint │───┐
┌───────┐│    └──────┘   │  ┌────────┐
│ build │─┤               ├─►│ deploy │
└───────┘│    ┌──────┐   │  └────────┘
         └───►│ test │───┘
              └──────┘
  • build runs first
  • lint and test run in parallel after build
  • deploy runs after both lint and test succeed
Show Solution

name: Diamond Pipeline

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  lint:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Linting..."

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."

  deploy:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."


Exercise 2 — Pass Version Between Jobs

Task: Create a workflow where:

  1. Job prepare generates a version string in the format YYYY.MM.DD-<short-sha> (e.g., 2025.03.28-abc1234) and sets it as a job output
  2. Job build reads the version from prepare and prints “Building version X”
  3. Job tag reads the version from prepare and prints “Tagging version X”
Show Solution

name: Version Pipeline

on: push

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.version }}
    steps:
      - name: Generate version
        id: ver
        run: |
          VERSION="$(date +%Y.%m.%d)-${GITHUB_SHA::7}"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "Generated version: $VERSION"

  build:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building version ${{ needs.prepare.outputs.version }}"

  tag:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - run: echo "Tagging version ${{ needs.prepare.outputs.version }}"


Exercise 3 — Integration Tests with Services

Task: Create a workflow that:

  1. Runs inside a node:20 container
  2. Starts a PostgreSQL 16 service container
  3. Waits for PostgreSQL to be healthy
  4. Runs a command that verifies the database connection
Show Solution

name: Integration Tests

on: push

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: node:20

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Install PostgreSQL client
        run: |
          apt-get update
          apt-get install -y postgresql-client

      - name: Test database connection
        run: |
          psql postgresql://test:test@postgres:5432/testdb -c "SELECT 1 AS connected;"

Inside a container job, you connect to the service using the service name (postgres) as the hostname.


Exercise 4 — Cleanup on Failure

Task: Create a workflow with three jobs:

  1. setup — creates resources (just echo for now)
  2. test — runs tests that fail (exit 1)
  3. cleanup — always runs after test, even if it fails, and prints “Cleaning up resources”
Show Solution

name: Cleanup on Failure

on: push

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Setting up resources..."

  test:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Running tests..."
          exit 1

  cleanup:
    needs: test
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Check test result
        run: |
          echo "Test result: ${{ needs.test.result }}"
          echo "Cleaning up resources..."

The if: always() on the cleanup job ensures it runs regardless of whether the test job succeeded or failed.


Exercise 5 — Cross-Platform Testing

Task: Create a workflow that runs echo "Testing on $RUNNER_OS" on three different runners in parallel:

  1. ubuntu-latest
  2. windows-latest
  3. macos-latest

Don’t use matrix strategy (we’ll learn that in Chapter 9). Create three separate jobs.

Show Solution

name: Cross-Platform Tests

on: push

jobs:
  test-linux:
    runs-on: ubuntu-latest
    steps:
      - name: Test
        run: echo "Testing on $RUNNER_OS"

  test-windows:
    runs-on: windows-latest
    steps:
      - name: Test
        run: echo "Testing on $env:RUNNER_OS"
        shell: pwsh

  test-macos:
    runs-on: macos-latest
    steps:
      - name: Test
        run: echo "Testing on $RUNNER_OS"

Note that Windows uses PowerShell by default, so the environment variable syntax is $env:RUNNER_OS. All three jobs run in parallel.


← Back to: Chapter 4: Jobs & Runners

Next Chapter: Chapter 5: Steps & Actions →

Updated: