GitHub Actions Tutorial — Practice Projects Exercises
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:
- A dependency audit step (
npm audit) - A CodeQL analysis job that runs in parallel with the test matrix
- 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:
- Triggers via
workflow_dispatchwith aversioninput - Deploys the specified version to production
- 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:
build-statusmust besuccessorfailuretest-countmust be a non-negative integer- 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:
- Accepts
python-version(default'3.12') andworking-directoryinputs - Sets up Python with pip caching
- Installs from
requirements.txt - Runs
pytestwith coverage - 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 →