GitHub Actions Tutorial — Next Steps Exercises
Practice Exercises — Next Steps
Final exercises to cap off your GitHub Actions learning journey.
Exercise 1 — Monorepo Path Filter
Task: Write a workflow for a monorepo with packages/api/ and packages/web/ that:
- Detects which packages changed
- Only runs the CI job for changed packages
- Works on both push and pull_request events
Show Solution
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.filter.outputs.api }}
web: ${{ steps.filter.outputs.web }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api:
- 'packages/api/**'
web:
- 'packages/web/**'
ci-api:
needs: detect
if: needs.detect.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd packages/api && npm ci && npm test
ci-web:
needs: detect
if: needs.detect.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd packages/web && npm ci && npm test
If only packages/api/ files changed, only ci-api runs. If both changed, both run.
Exercise 2 — GitHub App Token Workflow
Task: Explain why the following workflow won’t trigger CI on the PR it creates, and fix it using a GitHub App token:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
echo "update" >> deps.txt
git add . && git commit -m "Update deps"
git push origin automated-update
- run: gh pr create --title "Update" --body "Auto"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Show Solution
Why it won’t trigger CI: PRs created with GITHUB_TOKEN do not trigger other workflows. This is a deliberate safety measure to prevent infinite workflow loops.
Fix with GitHub App:
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- run: |
git config user.name "my-bot[bot]"
git config user.email "my-bot@users.noreply.github.com"
echo "update" >> deps.txt
git add . && git commit -m "Update deps"
git push origin automated-update
- run: gh pr create --title "Update" --body "Auto"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PRs created with a GitHub App token will trigger CI workflows.
Exercise 3 — Concurrency Control
Task: Write the concurrency configuration for these scenarios:
- Cancel previous runs when a new push arrives on the same branch
- Never cancel production deployments (queue them instead)
- Allow only one deploy per environment at a time
Show Solution
1. Cancel previous runs per branch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
2. Queue production deploys (never cancel):
jobs:
deploy-production:
concurrency:
group: deploy-production
cancel-in-progress: false # queue, don't cancel
environment: production
# ...
3. One deploy per environment:
jobs:
deploy:
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
environment: ${{ inputs.environment }}
# ...
cancel-in-progress: true cancels queued runs. false makes them wait.
Exercise 4 — Non-CI/CD Automation
Task: Write a workflow that:
- Runs every day at midnight UTC
- Finds all issues labeled
needs-triagethat are older than 7 days - Adds a comment asking the author for more information
- Adds a
stale-triagelabel
Show Solution
name: Triage Reminder
on:
schedule:
- cron: '0 0 * * *'
permissions:
issues: write
jobs:
remind:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'needs-triage',
state: 'open',
sort: 'created',
direction: 'asc'
});
for (const issue of issues) {
if (new Date(issue.created_at) < sevenDaysAgo) {
// Check if we already commented
const labels = issue.labels.map(l => l.name);
if (labels.includes('stale-triage')) continue;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `@${issue.user.login} — this issue has been awaiting triage for over 7 days. Could you provide more details to help us prioritize it?`
});
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['stale-triage']
});
}
}
Exercise 5 — Design Your Learning Plan
Task: Based on the learning path, create a personal 4-week plan to go from beginner to advanced. For each week, specify:
- Which chapters to study
- Which project to build
- What to practice
Show Solution
| Week | Study | Build | Practice |
|---|---|---|---|
| 1 | Introduction, Getting Started, Triggers, Jobs & Runners | First CI workflow (hello world) | YAML syntax, trigger types, job dependencies |
| 2 | Steps & Actions, Env & Secrets, Expressions, Artifacts & Caching | Node.js CI project | Matrix builds, caching, artifact upload |
| 3 | Matrix, Reusable Workflows, Custom Actions, Deployments | Docker + GHCR project, Multi-Env Deploy project | Reusable workflows, composite actions, OIDC |
| 4 | Self-Hosted Runners, Troubleshooting, Advanced Patterns, Next Steps | Reusable Library project, Custom Action project | Monorepo patterns, GitHub Apps, act local testing |
Daily routine:
- Read one sub-topic (20 minutes)
- Complete the exercises at the end (30 minutes)
- Iterate on your project (30 minutes)
By week 4, you should be able to architect CI/CD pipelines for real projects with reusable workflows, custom actions, and multi-environment deployments.
🎉 Congratulations! You’ve completed the GitHub Actions tutorial.
← Back to: Chapter 16: Next Steps