← Back to Git series
🔀
Git & GitHub Deep Dive
workflow yml · triggers · matrix · cache · secrets

Chapter 8 — GitHub Actions

GitHub Actions runs CI/CD workflows defined as YAML files in `.github/workflows/`. Every `push` or `pull_request` can trigger automated tests, builds, lints, deployments.

actionsciworkflow
Duration
1-2 hours
Level
📊 Intermediate
Prerequisite
🎯 Chapter 7
Outcome
Automate CI with GitHub Actions

What you'll learn

  • 1Write a basic workflow file: triggers, jobs, steps.
  • 2Use the most common triggers (`push`, `pull_request`).
  • 3Run tests across a matrix (multiple OS / Node versions).
  • 4Cache dependencies between runs.
  • 5Upload and download artifacts.
  • 6Use secrets safely.

Core Concepts

1) Workflow anatomy

yaml
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Hello, Actions!"
Top-level keyWhat
`name`Workflow display name
`on`Trigger(s)
`jobs`One or more parallel job groups
`steps`Sequential actions inside a job

2) Common triggers

yaml
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: "0 9 * * 1"   # every Monday 9am UTC
  workflow_dispatch:       # manual button

3) Matrix builds

yaml
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [18, 20, 22]
runs-on: ${{ matrix.os }}

Runs `3 × 3 = 9` parallel jobs.

4) Caching

yaml
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}

The cache restores from this key (or a prefix match) — huge speedup.

5) Artifacts

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

Download from the Actions UI; or use `actions/download-artifact` in a later job.

6) Secrets

In repo settings → Secrets and variables → Actions:

yaml
env:
  API_KEY: ${{ secrets.API_KEY }}

Never echo secrets in plain output — GitHub masks them but redaction is best-effort.

Examples

Example 1 — `ex01_hello.yml`: push triggers "Hello"

**Output**

text
=== workflow run ===
Run echo "Hello, Actions!"
Hello, Actions!

Key: minimal viable workflow. Save under `.github/workflows/hello.yml` and push.

Example 2 — `ex02_test.yml`: Node project with `npm test`

**Output**

text
=== workflow run ===
checkout → setup-node@v4 → npm ci → npm test
all green

Key: `actions/setup-node` provisions Node; `npm ci` is the CI-friendly install.

Example 3 — `ex03_matrix.yml`: OS × Node matrix

**Output**

text
=== workflow run ===
ubuntu+18 ✓ ubuntu+20 ✓ ubuntu+22 ✓
macos+18 ✓ macos+20 ✓ macos+22 ✓
windows+18 ✓ windows+20 ✓ windows+22 ✓

Key: catch portability bugs early; cost is parallel runner minutes.

Example 4 — `ex04_cache.yml`: cache `node_modules`

**Output**

text
=== workflow run ===
cache hit → install skipped (10s instead of 60s)

Key: cache the install — biggest CI win for most projects.

Common mistakes

  1. **Workflow file in the wrong path** — must be `.github/workflows/*.yml`, not `.github/*.yml`.
  2. **Secrets in `if`** — Secrets are not available to `if:` expressions; use them in steps.
  3. **Forgetting `actions/checkout`** — your workflow runs against an empty workspace otherwise.
  4. **Matrix without `fail-fast: false`** — one fail aborts the rest; you lose visibility.
  5. **Long-running jobs** — Actions has timeouts; split or shorten.

Recap

  • A workflow is YAML in `.github/workflows/`.
  • Triggers say "when"; jobs say "what"; steps say "how".
  • Matrix gives breadth; cache gives speed; artifacts give continuity.
  • Secrets are encrypted; treat them with care anyway.

Try it

bash
mkdir -p .github/workflows
cp src/ex01_hello.yml .github/workflows/hello.yml
git add . && git commit -m "ci: hello world workflow"
git push   # check the Actions tab

💻 Examples

Runnable examples — see the output yourself.

ex01_hello.ymlpush triggers "Hello"
CODE
name: Hello World

on:
 push:
 branches: [main]
 workflow_dispatch:

jobs:
 greet:
 runs-on: ubuntu-latest
 steps:
 - name: Checkout
 uses: actions/checkout@v4

 - name: Print environment info
 run: |
 echo "=== Hello, GitHub Actions! ==="
 echo "Repository : ${{ github.repository }}"
 echo "Branch : ${{ github.ref_name }}"
 echo "Actor : ${{ github.actor }}"
 echo "Commit SHA : ${{ github.sha }}"
 echo "Runner OS : ${{ runner.os }}"

 - name: List files
 run: ls -la
▶ Output
=== workflow run ===
Run echo "Hello, Actions!"
Hello, Actions!
ex02_test.ymlNode project with `npm test`
CODE
name: Node.js CI

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

jobs:
 test:
 runs-on: ubuntu-latest

 steps:
 - name: Checkout code
 uses: actions/checkout@v4

 - name: Setup Node.js 20
 uses: actions/setup-node@v4
 with:
 node-version: '20'

 - name: Install dependencies
 run: npm ci

 - name: Run linter
 run: npm run lint --if-present

 - name: Run tests
 run: npm test --if-present

 - name: Build
 run: npm run build --if-present

 - name: Report status
 if: always()
 run: |
 echo "Job status: ${{ job.status }}"
▶ Output
=== workflow run ===
checkout → setup-node@v4 → npm ci → npm test
all green
ex03_matrix.ymlOS × Node matrix
CODE
name: Matrix Build

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

jobs:
 test:
 name: Test on ${{ matrix.os }} / Node ${{ matrix.node-version }}
 runs-on: ${{ matrix.os }}

 strategy:
 fail-fast: false
 matrix:
 os: [ubuntu-latest, macos-latest, windows-latest]
 node-version: [18, 20, 22]
 exclude:
 - os: windows-latest
 node-version: 18

 steps:
 - name: Checkout
 uses: actions/checkout@v4

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

 - name: Show environment
 run: |
 node --version
 npm --version

 - name: Install dependencies
 run: npm ci

 - name: Run tests
 run: npm test --if-present

 summary:
 name: Matrix Summary
 needs: test
 runs-on: ubuntu-latest
 if: always()
 steps:
 - name: Print result
 run: echo "All matrix jobs completed with status ${{ needs.test.result }}"
▶ Output
=== workflow run ===
ubuntu+18 ✓ ubuntu+20 ✓ ubuntu+22 ✓
macos+18 ✓ macos+20 ✓ macos+22 ✓
windows+18 ✓ windows+20 ✓ windows+22 ✓
ex04_cache.ymlcache `node_modules`
CODE
name: Build with Cache and Artifacts

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

jobs:
 build:
 runs-on: ubuntu-latest

 steps:
 - name: Checkout
 uses: actions/checkout@v4

 - name: Setup Node.js with npm cache
 uses: actions/setup-node@v4
 with:
 node-version: '20'
 cache: 'npm'

 - name: Install dependencies
 run: npm ci

 - name: Run tests
 run: npm test --if-present

 - name: Build
 run: npm run build --if-present

 - name: Upload build artifacts
 uses: actions/upload-artifact@v4
 with:
 name: dist-${{ github.sha }}
 path: |
 dist/
 !dist/**/*.map
 retention-days: 7
 if-no-files-found: warn

 deploy:
 name: Deploy to production
 needs: build
 runs-on: ubuntu-latest
 if: github.ref == 'refs/heads/main' && github.event_name == 'push'
 environment: production

 steps:
 - name: Download artifacts
 uses: actions/download-artifact@v4
 with:
 name: dist-${{ github.sha }}
 path: dist/

 - name: Deploy
 env:
 DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
 TARGET_HOST: ${{ secrets.TARGET_HOST }}
 run: |
 echo "Deploying to $TARGET_HOST..."
 echo "Token length: ${#DEPLOY_TOKEN} chars"
 echo "Deploy complete!"
▶ Output
=== workflow run ===
cache hit → install skipped (10s instead of 60s)

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Problem 1 (hw01.yml)

Goal: Write a workflow `ci.yml` that runs on `push` to `main` and any `pull_request`. The job should: checkout, set up Node 20, run `npm ci` and `npm test`.

Requirements
  • Filename: hw01.yml
Toggle solution
SOLUTION
name: CI Report

on:
 push:
 branches: [main]
 workflow_dispatch:

jobs:
 report:
 runs-on: ubuntu-latest

 steps:
 - name: Checkout
 uses: actions/checkout@v4

 - name: Generate report
 run: |
 {
 echo "=== Build Report ==="
 echo "Date: $(date)"
 echo ""
 echo "=== System Info ==="
 uname -a
 echo ""
 echo "=== Recent Commits ==="
 git log --oneline -5
 } > report.txt
 cat report.txt

 - name: Upload report artifact
 uses: actions/upload-artifact@v4
 with:
 name: ci-report
 path: report.txt
 retention-days: 3
Exercise 2

Problem 2 (hw02.yml)

Goal: Extend the above to a matrix across `[ubuntu-latest, macos-latest]` × Node `[18, 20]`, with caching of `~/.npm`.

Requirements
  • Filename: hw02.yml
Toggle solution
SOLUTION
name: Python Matrix CI

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

jobs:
 test:
 name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
 runs-on: ${{ matrix.os }}

 strategy:
 fail-fast: false
 matrix:
 os: [ubuntu-latest, macos-latest]
 python-version: ['3.10', '3.11', '3.12']

 steps:
 - name: Checkout
 uses: actions/checkout@v4

 - name: Setup Python ${{ matrix.python-version }}
 uses: actions/setup-python@v5
 with:
 python-version: ${{ matrix.python-version }}

 - name: Install dependencies
 run: |
 python -m pip install --upgrade pip
 if [ -f requirements.txt ]; then
 pip install -r requirements.txt
 else
 echo "requirements.txt none — "
 fi

 - name: Run tests
 run: |
 if python -m pytest --version 2>/dev/null; then
 python -m pytest
 else
 echo "pytest none — "
 fi

 - name: Show Python version
 run: python --version
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗