Time to write code that runs code. In this lesson you'll create a real workflow, push it, and watch GitHub execute it — the first tangible proof that your pipeline works.

GitHub Actions: Where Pipelines Live

In GitHub Actions, pipelines are YAML files stored in a special directory in your repo:

📁 your-repo/ ├── 📁 .github/ │ └── 📁 workflows/ │ ├── ci.yml │ ├── deploy.yml │ └── release.yml ├── 📁 src/ ├── 📁 tests/ ├── Dockerfile └── package.json This is where your pipelines live! Each .yml file = one workflow
The moment you push a .yml file into .github/workflows/, GitHub automatically recognises it as a workflow. No setup, no configuration, no "enable CI" button.

The Simplest Possible Workflow

Here's the absolute minimum — a workflow that runs one command:

name: Hello CI

# WHEN to run (the trigger)
on:
  push:
    branches: [main]

# WHAT to do
jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello from CI! 🎉"

Let's map every line to the mental model from Lesson 01:

name: Hello CI on: push jobs: greet: runs-on: ubuntu-latest steps: - run: echo "Hello" ⚡ Trigger 📋 Job 🖥️ Runner 👣 Step "Run when code is pushed to main" "A unit of work called 'greet'" "Execute on an Ubuntu VM" "Run this shell command"
Every YAML keyword maps directly to a pipeline concept

🏋️ Exercise: Build It, Push It, Watch It Run

Goal: Create a workflow that runs tests on every push. Follow these steps exactly:

Step 1: Create the application

cd cicd-mastery   # your repo from Lesson 01 prep

# Create a simple app
mkdir -p src tests

cat > src/math.js << 'EOF'
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
module.exports = { add, multiply };
EOF

cat > tests/math.test.js << 'EOF'
const { add, multiply } = require('../src/math');

test('add: 2 + 3 = 5', () => {
  expect(add(2, 3)).toBe(5);
});

test('multiply: 4 × 5 = 20', () => {
  expect(multiply(4, 5)).toBe(20);
});
EOF

# Initialize Node project
npm init -y
npm install --save-dev jest

# Add test script to package.json
npx json -I -f package.json -e 'this.scripts.test="jest"'

Step 2: Verify locally

npm test
# Should show: 2 tests passed ✓

Step 3: Create the workflow

mkdir -p .github/workflows

cat > .github/workflows/ci.yml << 'EOF'
name: CI

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

jobs:
  test:
    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'

      - name: 📚 Install dependencies
        run: npm ci

      - name: 🧪 Run tests
        run: npm test
EOF

Step 4: Push and observe

git add .
git commit -m "feat: add CI pipeline"
git push origin main

Step 5: Watch it run

  1. Go to your repo on GitHub
  2. Click the "Actions" tab
  3. Click the running workflow
  4. Click the "test" job
  5. Expand each step and read the logs

🎉 You just ran your first CI pipeline.

What Just Happened (Sequence Diagram)

You GitHub Runner (VM) Actions git push Detects push event Reads .github/workflows/ Spin up Ubuntu VM Fresh VM ready ✓ actions/checkout@v4 Code cloned ✓ actions/setup-node@v4 Node.js 20 ready ✓ npm ci npm test → PASS ✅ Report: Success ✅ Green check on commit
The full lifecycle of a workflow run — from git push to green check

Understanding uses: vs run:

Steps come in two flavours:

uses: — Run a pre-built Action
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with:
    node-version: '20'

Like calling a library function. Someone else wrote the logic; you pass parameters via with:.

run: — Execute a shell command
- run: npm ci
- run: npm test
- run: |
    echo "Multi-line"
    echo "commands"

Direct shell access. Runs in bash by default. Use | for multi-line.

The actions/checkout Mystery

New users often miss this: the runner starts with an EMPTY filesystem. Your code isn't there. actions/checkout is what clones your repo onto the runner.

Runner (before) 📭 Empty! No code. checkout@v4 Runner (after) ├── src/math.js ├── tests/math.test.js └── package.json
Without checkout, your subsequent steps would fail — there's no code to work with
If you forget actions/checkout, every run: step that references your code will fail with "file not found." This is the #1 beginner mistake.

🧠 Recall Check

  1. Where must workflow YAML files live in your repository?
  2. What does runs-on: ubuntu-latest specify?
  3. Why is actions/checkout@v4 almost always the first step?
  4. What's the difference between uses: and run:?
Reveal answers
  1. .github/workflows/ — GitHub auto-discovers any .yml file here.
  2. The runner — a fresh Ubuntu VM that executes the job.
  3. Because the runner starts empty — no code. Checkout clones your repo onto it.
  4. uses: runs a pre-built action (reusable logic). run: executes a shell command directly.

What You Built

You now have a working CI pipeline. Every time you push to main or open a PR, GitHub will automatically run your tests and report the result. You'll never merge broken code without knowing.

Next lesson: Triggers & Events — controlling exactly when your pipeline runs (and when it doesn't).