GitHub Actions Tutorial — Steps & Actions Exercises
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:
- Checks out the repository
- Sets up Node.js 20 with npm caching
- Installs dependencies with
npm ci - Runs linting with
npm run lint - Runs tests with
npm test - 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:
- Step 1: Generates a build ID in the format
build-<date>-<short-sha>and sets it as a step output - Step 2: Sets the build ID as an environment variable using
$GITHUB_ENV - 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:
- Always runs:
echo "Starting CI" - Only on
mainbranch:echo "Running deployment checks" - Only on pull requests:
echo "Running PR validation" - 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:
- Uses
actions/checkout@v4with full history (fetch-depth: 0) - Counts the total number of commits and sets it as a step output
- Gets the latest tag and sets it as a step output
- 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:
echo "Step 1: Setup"— must succeedexit 1— fails, but should NOT stop the workflow (use continue-on-error)- Check if step 2 failed and print “Step 2 failed, continuing…”
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: trueprevents step 2 from stopping the workflowsteps.risky.outcomegives the actual result (failure), whilesteps.risky.conclusionwould besuccessbecause of continue-on-errorif: always()ensures step 4 runs regardless of previous outcomes
← Back to: Chapter 5: Steps & Actions
Next Chapter: Chapter 6: Environment & Secrets →