Time to build a pipeline you'd actually use at work. In this lesson you'll set up ESLint, Jest with coverage, and a build step — wired together in a production-shaped CI workflow.
What Real CI Looks Like
🏋️ Step 1: Set Up Your Project
Upgrade your cicd-mastery repo with real tooling:
cd cicd-mastery
# Add more source code
cat > src/calculator.js << 'EOF'
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
module.exports = { add, subtract, multiply, divide };
EOF
# Add comprehensive tests
cat > tests/calculator.test.js << 'EOF'
const { add, subtract, multiply, divide } = require('../src/calculator');
describe('Calculator', () => {
describe('add', () => {
test('adds positive numbers', () => expect(add(2, 3)).toBe(5));
test('adds negative numbers', () => expect(add(-1, -2)).toBe(-3));
test('adds zero', () => expect(add(5, 0)).toBe(5));
});
describe('subtract', () => {
test('subtracts numbers', () => expect(subtract(5, 3)).toBe(2));
test('handles negative result', () => expect(subtract(3, 5)).toBe(-2));
});
describe('multiply', () => {
test('multiplies numbers', () => expect(multiply(3, 4)).toBe(12));
test('multiplies by zero', () => expect(multiply(5, 0)).toBe(0));
});
describe('divide', () => {
test('divides numbers', () => expect(divide(10, 2)).toBe(5));
test('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
EOF
# Install dev dependencies
npm install --save-dev eslint jest
# Create ESLint config
cat > .eslintrc.json << 'EOF'
{
"env": { "node": true, "jest": true, "es2021": true },
"extends": "eslint:recommended",
"rules": {
"no-unused-vars": "error",
"eqeqeq": "error",
"no-console": "warn"
}
}
EOF
# Update package.json scripts
npx json -I -f package.json -e '
this.scripts = {
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src/ tests/",
"build": "node scripts/build.js"
}
'
# Create build script
mkdir -p scripts
cat > scripts/build.js << 'EOF'
const fs = require('fs');
const path = require('path');
const dist = path.join(__dirname, '..', 'dist');
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true });
fs.mkdirSync(dist);
// Copy source
fs.readdirSync(path.join(__dirname, '..', 'src')).forEach(f => {
fs.copyFileSync(
path.join(__dirname, '..', 'src', f),
path.join(dist, f)
);
});
// Write build metadata
const info = {
version: require('../package.json').version,
commit: process.env.GITHUB_SHA || 'local',
time: new Date().toISOString()
};
fs.writeFileSync(path.join(dist, 'build.json'), JSON.stringify(info, null, 2));
console.log('✅ Build complete:', info);
EOF
# Verify everything works locally
npm run lint
npm run test:coverage
npm run build
🏋️ Step 2: The Production CI Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
paths-ignore: ['**.md']
pull_request:
branches: [main]
workflow_dispatch:
# Cancel in-progress runs on the same branch
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ────────────────────────────────────────
lint:
name: 🔍 Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
# ────────────────────────────────────────
test:
name: 🧪 Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Coverage summary
if: always()
run: |
echo "## 🧪 Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All tests passed with coverage" >> $GITHUB_STEP_SUMMARY
# ────────────────────────────────────────
build:
name: 📦 Build
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
retention-days: 7
- name: Build summary
run: |
echo "## 📦 Build Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Artifact: \`dist-${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
cat dist/build.json >> $GITHUB_STEP_SUMMARY
New Concepts Introduced
1. cache: 'npm' — Dependency Caching
2. concurrency: — Don't Waste Resources
3. $GITHUB_STEP_SUMMARY — Rich Job Reports
Write Markdown to $GITHUB_STEP_SUMMARY and it appears as a formatted report on the workflow run page. Great for test results, coverage, build info.
🏋️ Step 3: Push & Observe
- Commit and push everything
- Go to Actions tab — watch lint and test run simultaneously
- Click into the run and look at the Summary tab (your step summary renders there)
- Download the build artifact
- Try breaking a lint rule (e.g.,
var x = 1;but never use x) — push and watch lint fail, build skip
What You Now Have
You have a CI pipeline that a production team would recognise:
- ✅ Linting catches style/quality issues
- ✅ Tests catch logic bugs with coverage tracking
- ✅ Build only runs if quality gates pass
- ✅ Artifacts are stored for downstream consumption
- ✅ Caching makes it fast
- ✅ Concurrency prevents waste
- ✅ Summary gives you at-a-glance results
🧠 Recall Check
- What does
cache: 'npm'insetup-nodeactually cache? - What does
concurrency: cancel-in-progress: truedo when you push rapidly? - Why do lint and test run in parallel, but build runs after both?
- Where does
$GITHUB_STEP_SUMMARYcontent appear?
Reveal answers
- The npm cache directory (
~/.npm). This meansnpm ciresolves packages from local cache instead of downloading from the registry — much faster. - Cancels older in-progress runs for the same branch, keeping only the latest. Saves minutes on rapid pushes.
- No
needs:on lint/test = parallel.needs: [lint, test]on build = waits for both. You don't want to build if code is broken. - On the workflow run's Summary tab in the GitHub Actions UI. It renders as Markdown.
Next lesson: Caching & Optimization — making your pipeline blazing fast.