GitHub Actions Tutorial — Steps & Actions Exercises

3 minute read

Practice Exercises — Steps & Actions

Test your understanding of run commands, actions, inputs/outputs, and step communication with these hands-on exercises.


Exercise 1 — Multi-Step Build Pipeline

Task: Create a workflow with a single job that:

  1. Checks out the repository
  2. Sets up Node.js 20 with npm caching
  3. Installs dependencies with npm ci
  4. Runs linting with npm run lint
  5. Runs tests with npm test
  6. Builds the project with npm run build

Each step should have a descriptive name:.

Show Solution

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint code
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build project
        run: npm run build


Exercise 2 — Pass Data Between Steps

Task: Create a workflow that:

  1. Step 1: Generates a build ID in the format build-<date>-<short-sha> and sets it as a step output
  2. Step 2: Sets the build ID as an environment variable using $GITHUB_ENV
  3. Step 3: Prints the build ID using the environment variable
Show Solution

name: Build ID Pipeline

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Generate build ID
        id: build-id
        run: |
          BUILD_ID="build-$(date +%Y%m%d)-${GITHUB_SHA::7}"
          echo "id=$BUILD_ID" >> "$GITHUB_OUTPUT"
          echo "Generated: $BUILD_ID"

      - name: Set as env var
        run: echo "BUILD_ID=${{ steps.build-id.outputs.id }}" >> "$GITHUB_ENV"

      - name: Use build ID
        run: echo "Building with ID: $BUILD_ID"

Step 1 creates the output. Step 2 promotes it to an environment variable. Step 3 uses the environment variable directly.


Exercise 3 — Conditional Steps

Task: Create a workflow that:

  1. Always runs: echo "Starting CI"
  2. Only on main branch: echo "Running deployment checks"
  3. Only on pull requests: echo "Running PR validation"
  4. Always runs (even on failure): echo "CI complete"
Show Solution

name: Conditional Steps

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - name: Start CI
        run: echo "Starting CI"

      - name: Deployment checks (main only)
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: echo "Running deployment checks"

      - name: PR validation (PRs only)
        if: github.event_name == 'pull_request'
        run: echo "Running PR validation"

      - name: CI complete
        if: always()
        run: echo "CI complete"


Exercise 4 — Working with Action Outputs

Task: Create a workflow that:

  1. Uses actions/checkout@v4 with full history (fetch-depth: 0)
  2. Counts the total number of commits and sets it as a step output
  3. Gets the latest tag and sets it as a step output
  4. Prints both values: “Commits: X, Latest tag: Y”
Show Solution

name: Repo Info

on: push

jobs:
  info:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout with full history
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get repo stats
        id: stats
        run: |
          COMMITS=$(git rev-list --count HEAD)
          echo "commits=$COMMITS" >> "$GITHUB_OUTPUT"

          TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "no-tag")
          echo "latest-tag=$TAG" >> "$GITHUB_OUTPUT"

      - name: Print info
        run: |
          echo "Commits: ${{ steps.stats.outputs.commits }}"
          echo "Latest tag: ${{ steps.stats.outputs.latest-tag }}"


Exercise 5 — Error Handling with continue-on-error

Task: Create a workflow with four steps:

  1. echo "Step 1: Setup" — must succeed
  2. exit 1 — fails, but should NOT stop the workflow (use continue-on-error)
  3. Check if step 2 failed and print “Step 2 failed, continuing…”
  4. echo "Step 4: Cleanup" — must always run
Show Solution

name: Error Handling

on: push

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - name: Step 1 - Setup
        run: echo "Step 1: Setup"

      - name: Step 2 - Risky operation
        id: risky
        continue-on-error: true
        run: exit 1

      - name: Step 3 - Check failure
        if: steps.risky.outcome == 'failure'
        run: echo "Step 2 failed, continuing..."

      - name: Step 4 - Cleanup
        if: always()
        run: echo "Step 4: Cleanup"

Key points:

  • continue-on-error: true prevents step 2 from stopping the workflow
  • steps.risky.outcome gives the actual result (failure), while steps.risky.conclusion would be success because of continue-on-error
  • if: always() ensures step 4 runs regardless of previous outcomes

← Back to: Chapter 5: Steps & Actions

Next Chapter: Chapter 6: Environment & Secrets →

Updated: