Skip to main content

Exercises and projects


Table of contents

  1. Basic exercises
  2. Intermediate exercises
  3. Project 1: Node.js pipeline
  4. Project 2: Automatic release
  5. Project 3: Monorepo
  6. Validation checklist


1 - Basic exercises

Exercise 1.1: Hello World

Create a workflow that displays "Hello World" on every push.

Solution
name: Hello World

on: push

jobs:
hello:
runs-on: ubuntu-latest
steps:
- run: echo "Hello World!"

Exercise 1.2: Multi-event

Create a workflow triggered by push AND pull_request.

Solution
name: Multi-Event

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Triggered by ${{ github.event_name }}"

Exercise 1.3: Environment variables

Create a workflow with a global variable and a step variable.

Solution
name: Variables

on: push

env:
GLOBAL_VAR: "I am global"

jobs:
vars:
runs-on: ubuntu-latest
steps:
- name: Use global
run: echo "$GLOBAL_VAR"

- name: Use local
env:
LOCAL_VAR: "I am local"
run: echo "$LOCAL_VAR"

Exercise 1.4: Checkout and list

Create a workflow that checks out the code and lists the files.

Solution
name: Checkout

on: push

jobs:
list:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ls -la
- run: cat README.md || echo "No README"

🔝 Back to table of contents



2 - Intermediate exercises

Exercise 2.1: npm cache

Create a workflow with npm cache using setup-node.

Solution
name: Build with Cache

on: push

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

- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- run: npm ci
- run: npm run build

Exercise 2.2: Multi-version matrix

Test on Node 16, 18 and 20.

Solution
name: Matrix Test

on: push

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16, 18, 20]

name: Node ${{ matrix.node }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

- run: node --version

Exercise 2.3: Dependent jobs

Create 3 jobs: build → test → deploy (sequential).

Solution
name: Pipeline

on: push

jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."

test:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."

deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."

Exercise 2.4: Artifacts

Create a file in one job and download it in another.

Solution
name: Artifacts

on: push

jobs:
create:
runs-on: ubuntu-latest
steps:
- run: echo "Hello" > message.txt

- uses: actions/upload-artifact@v4
with:
name: message
path: message.txt

read:
needs: create
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: message

- run: cat message.txt

Exercise 2.5: Conditions

Run a job only on the main branch.

Solution
name: Conditional

on: push

jobs:
always:
runs-on: ubuntu-latest
steps:
- run: echo "Always runs"

main-only:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Only on main"

🔝 Back to table of contents



3 - Project 1: Complete Node.js pipeline

Goal

Create a complete CI/CD pipeline for a Node.js application:

  • Lint with ESLint
  • Tests with Jest
  • Build
  • Conditional deployment

Solution

name: Node.js CI/CD

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

env:
NODE_VERSION: '18'

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'

- run: npm ci
- run: npm run lint

test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'

- run: npm ci
- run: npm test -- --coverage

- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'

- run: npm ci
- run: npm run build

- uses: actions/upload-artifact@v4
with:
name: build
path: dist/

deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: ./dist

- run: echo "Deploying to staging..."

deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: ./dist

- run: echo "Deploying to production..."

🔝 Back to table of contents



4 - Project 2: Automatic release

Goal

Create a workflow that:

  • Triggers on v* tags
  • Builds the application
  • Creates a GitHub release with the assets

Solution

name: Release

on:
push:
tags:
- 'v*'

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

- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'

- run: npm ci
- run: npm run build

- name: Create archive
run: tar -czf dist.tar.gz dist/

- uses: actions/upload-artifact@v4
with:
name: release-archive
path: dist.tar.gz

release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-archive

- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist.tar.gz
generate_release_notes: true

🔝 Back to table of contents



5 - Project 3: Monorepo

Goal

Create a pipeline for a monorepo with:

  • Frontend (React)
  • Backend (Node.js)
  • Conditional deployment based on the modified files

Solution

name: Monorepo CI

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

jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v4

- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
backend:
- 'backend/**'

frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- run: npm ci
- run: npm test
- run: npm run build

backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- run: npm ci
- run: npm test

deploy:
needs: [frontend, backend]
if: always() && github.ref == 'refs/heads/main' && (needs.frontend.result == 'success' || needs.backend.result == 'success')
runs-on: ubuntu-latest
steps:
- run: echo "Deploying changed components..."

🔝 Back to table of contents



6 - Validation checklist

Skills to validate

SkillChecked
Create a basic workflow
Use checkout and setup-node
Configure the cache
Use variables and secrets
Create dependent jobs
Use artifacts
Configure a matrix
Add conditions
Create a reusable workflow
Configure environments
Deploy automatically

Self-assessment

  1. Can you create a lint → test → build → deploy pipeline?
  2. Do you know how to use the cache to speed up your builds?
  3. Can you test on several Node versions?
  4. Do you know how to share data between jobs?
  5. Can you deploy conditionally based on the branch?

Congratulations!

You have completed the GitHub Actions course! 🎉

You now know how to:

  • ✅ Create CI/CD workflows
  • ✅ Use Marketplace actions
  • ✅ Manage secrets and variables
  • ✅ Optimize with cache and artifacts
  • ✅ Test across environments with matrix builds
  • ✅ Build reusable workflows
  • ✅ Create complete deployment pipelines

Next steps

  • Apply this knowledge to your projects
  • Explore the Marketplace to discover new actions
  • Create your own reusable actions
  • Share your workflows with your team

← Back to the course table of contents