GitHub Actions Tutorial — Practice Projects Exercises

4 minute read

Practice Exercises — Practice Projects

Extend the projects from this chapter with these additional challenges.


Exercise 1 — Add Security Scanning to Node.js CI

Task: Extend the Node.js CI project to include:

  1. A dependency audit step (npm audit)
  2. A CodeQL analysis job that runs in parallel with the test matrix
  3. The workflow should still pass even if audit finds low-severity issues
Show Solution

name: Node.js CI (Extended)

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

permissions:
  contents: read
  security-events: write

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Security audit
        run: npm audit --audit-level=high    # fail only on high/critical
        continue-on-error: false

  codeql:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript
      - uses: github/codeql-action/analyze@v3

  test:
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

The audit and codeql jobs run in parallel with the test matrix. --audit-level=high means low and moderate vulnerabilities don’t fail the build.


Exercise 2 — Add Vulnerability Scanning to Docker Build

Task: Extend the Docker + GHCR project to scan the built image for vulnerabilities using Trivy before pushing.

Show Solution

Add this step between the build and push steps (or build without push, scan, then push):


steps:
  # ... (checkout, setup buildx, login)

  - name: Build (local only)
    uses: docker/build-push-action@v6
    with:
      context: .
      push: false
      load: true
      tags: my-app:scan

  - name: Scan with Trivy
    uses: aquasecurity/trivy-action@master
    with:
      image-ref: my-app:scan
      format: 'table'
      exit-code: '1'                  # fail on vulnerabilities
      severity: 'CRITICAL,HIGH'       # only fail on critical/high

  - name: Build and push
    uses: docker/build-push-action@v6
    with:
      context: .
      push: true
      platforms: linux/amd64,linux/arm64
      tags: ${{ steps.meta.outputs.tags }}
      cache-from: type=gha
      cache-to: type=gha,mode=max

Note: We build twice — first with load: true (single platform, for scanning), then with push: true (multi-platform, for registry). The scan step fails the workflow if CRITICAL or HIGH vulnerabilities are found.


Exercise 3 — Add Rollback to Multi-Env Deploy

Task: Add a manual rollback job to the multi-environment deploy pipeline that:

  1. Triggers via workflow_dispatch with a version input
  2. Deploys the specified version to production
  3. Skips the staging → smoke test flow
Show Solution

name: Deploy Pipeline

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to rollback to (S3 prefix)'
        required: true
      skip-staging:
        description: 'Skip staging deploy'
        type: boolean
        default: true

permissions:
  id-token: write
  contents: read

jobs:
  test:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy-staging:
    if: github.event_name == 'push'
    needs: test
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      # ... staging deploy steps

  deploy-production:
    if: github.event_name == 'push'
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      # ... production deploy steps

  rollback:
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - name: Rollback to version
        run: |
          echo "Rolling back to version: ${{ inputs.version }}"
          aws s3 sync s3://backups/${{ inputs.version }}/ s3://production-bucket --delete
          echo "✅ Rollback complete"

The rollback job only runs on workflow_dispatch and goes straight to production with the specified version.


Exercise 4 — Add Input Validation to the Custom Action

Task: Modify the PR Comment Bot action to validate inputs:

  1. build-status must be success or failure
  2. test-count must be a non-negative integer
  3. If validation fails, the action should fail with a clear error message
Show Solution

Update comment.sh with validation:

#!/bin/bash
set -e

# Validate build-status
if [ "$BUILD_STATUS" != "success" ] && [ "$BUILD_STATUS" != "failure" ]; then
  echo "::error::Invalid build-status '$BUILD_STATUS'. Must be 'success' or 'failure'."
  exit 1
fi

# Validate test-count
if ! [[ "$TEST_COUNT" =~ ^[0-9]+$ ]]; then
  echo "::error::Invalid test-count '$TEST_COUNT'. Must be a non-negative integer."
  exit 1
fi

# Validate PR context
if [ -z "$PR_NUMBER" ]; then
  echo "::error::This action must run on a pull_request event."
  exit 1
fi

# Build and post comment (same as before)
if [ "$BUILD_STATUS" = "success" ]; then ICON="✅"; else ICON="❌"; fi

BODY="## ${ICON} Build Report
| Metric | Value |
|--------|-------|
| **Status** | ${BUILD_STATUS} |
| **Tests** | ${TEST_COUNT} passed |"

if [ -n "$PREVIEW_URL" ]; then
  BODY="${BODY}
| **Preview** | [Open Preview](${PREVIEW_URL}) |"
fi

gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BODY"

Using ::error:: creates visible annotations in the GitHub Actions UI.


Exercise 5 — Create a Python CI Reusable Workflow

Task: Add a ci-python.yml reusable workflow to the library project that:

  1. Accepts python-version (default '3.12') and working-directory inputs
  2. Sets up Python with pip caching
  3. Installs from requirements.txt
  4. Runs pytest with coverage
  5. Uploads coverage as an artifact
Show Solution

name: Reusable Python CI

on:
  workflow_call:
    inputs:
      python-version:
        description: 'Python version'
        type: string
        default: '3.12'
      working-directory:
        description: 'Working directory'
        type: string
        default: '.'

jobs:
  ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: 'pip'
          cache-dependency-path: ${{ inputs.working-directory }}/requirements.txt

      - run: pip install -r requirements.txt

      - run: pytest --cov --cov-report=html

      - uses: actions/upload-artifact@v4
        with:
          name: coverage-python
          path: ${{ inputs.working-directory }}/htmlcov/
          retention-days: 14

Called from any repo:


jobs:
  ci:
    uses: my-org/.github/.github/workflows/ci-python.yml@v1
    with:
      python-version: '3.12'


← Back to: Chapter 15: Practice Projects

Next Chapter: Chapter 16: Next Steps →

Updated: