What You'll Build
You will assemble a complete CI workflow for CricketPulse that integrates ESLint code quality checks, Prettier formatting validation, Jest unit tests with coverage reporting, and a PostgreSQL integration test suite. The workflow runs all three quality gates in parallel before triggering the test suite, enforces the 'fail fast, cheap first' ordering, and uploads test coverage and result artifacts. You will deliberately introduce a linting violation and a failing test to experience how the Actions UI surfaces both failures simultaneously — reinforcing the value of parallel quality gates as a developer productivity tool. By the end, you will have a fully functional CI pipeline that mirrors production-quality workflows at real software organisations.
Prerequisites
- Completed Lesson 4 exercise — the CricketPulse repository exists with `.github/workflows/` directory and a basic workflow running.
- Node.js 20 installed locally for running npm commands before pushing.
- ESLint and Prettier installed as dev dependencies in the CricketPulse project (`npm install --save-dev eslint prettier`).
- Jest installed as a dev dependency with a configured test script (`npm run test:unit`) and a test file in `tests/`.
- Understanding of GitHub Actions `needs:`, `services:`, and `if: always()` from Lessons 5–7.
Setup & Project Structure
The exercise builds on the CricketPulse repository from Lesson 4. You will add ESLint and Prettier configuration files, a Jest test file, and the multi-job CI workflow. The final project structure separates source code, tests, and configuration clearly — a structure you will maintain throughout the remaining course modules. Run the setup commands to scaffold any files that don't yet exist.
# Setup commands — run from the cricketpulse repository root
# Install dev dependencies
npm install --save-dev eslint prettier jest @types/jest
# Create ESLint config
cat > .eslintrc.json << 'EOF'
{
"env": { "node": true, "es2022": true },
"extends": ["eslint:recommended"],
"rules": {
"no-console": "warn",
"no-unused-vars": "error",
"eqeqeq": "error"
}
}
EOF
# Create Prettier config
cat > .prettierrc << 'EOF'
{ "semi": true, "singleQuote": true, "trailingComma": "es5", "printWidth": 100 }
EOF
# Create test directory and sample test
mkdir -p tests
cat > tests/cricketpulse.test.js << 'EOF'
const { calculateRunRate, validateMatchData } = require('../src/cricketpulse');
describe('CricketPulse: Run Rate Calculator', () => {
test('calculates correct run rate for completed innings', () => {
const runRate = calculateRunRate({ runs: 180, overs: 20 });
expect(runRate).toBe(9.0);
});
test('calculates partial over run rate', () => {
const runRate = calculateRunRate({ runs: 87, overs: 9.3 });
expect(parseFloat(runRate.toFixed(2))).toBe(9.14);
});
test('throws on invalid overs', () => {
expect(() => calculateRunRate({ runs: 100, overs: 0 })).toThrow('Overs must be greater than 0');
});
});
describe('CricketPulse: Match Data Validation', () => {
test('validates complete match data object', () => {
const match = { matchId: 'IPL-2024-001', venue: 'Wankhede', teams: ['MI', 'CSK'] };
expect(validateMatchData(match)).toBe(true);
});
test('rejects match data missing required fields', () => {
expect(validateMatchData({ matchId: 'IPL-2024-001' })).toBe(false);
});
});
EOF
# Create source module
mkdir -p src
cat > src/cricketpulse.js << 'EOF'
function calculateRunRate({ runs, overs }) {
if (overs <= 0) throw new Error('Overs must be greater than 0');
const fullOvers = Math.floor(overs);
const ballsFraction = (overs - fullOvers) / 0.6;
const totalOvers = fullOvers + ballsFraction;
return runs / totalOvers;
}
function validateMatchData(match) {
return !!(match.matchId && match.venue && match.teams && match.teams.length === 2);
}
module.exports = { calculateRunRate, validateMatchData };
EOF
echo 'CricketPulse project structure ready'Step 1 — Foundation
Create the three parallel quality gate jobs: lint, format-check, and type-check. These form the foundation of the CI pipeline — they run simultaneously on push and pull_request events, each checking a different quality dimension. None of these jobs depends on the others, so they start immediately and report concurrently. The developer sees all quality violations in a single CI run rather than having to fix one issue, push, wait for CI, fix the next issue, and repeat.
# File: .github/workflows/cricketpulse-ci.yml — Step 1: Quality gates
name: CricketPulse CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: ESLint Code Quality
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 ESLint
run: npx eslint 'src/**/*.js' 'tests/**/*.js'
format-check:
name: Prettier Format Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Check Prettier formatting
run: npx prettier --check 'src/**/*.js' 'tests/**/*.js'Step 2 — Core Logic
Add the unit test job that depends on both quality gates passing. This job runs Jest with coverage collection and uploads both the coverage report and the JUnit XML test results as artifacts. The `if: always()` on the artifact upload ensures that test reports are available even when tests fail — critical for debugging failures without re-running the pipeline.
# File: .github/workflows/cricketpulse-ci.yml — Step 2: Add unit tests
# (append to jobs section from Step 1)
unit-tests:
name: Jest Unit Tests
runs-on: ubuntu-latest
needs: [lint, format-check] # Only run if both quality gates pass
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Run Jest with coverage
run: |
npx jest --coverage \
--coverageDirectory=coverage \
--coverageReporters=lcov,text-summary \
--reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_DIR: test-results
JEST_JUNIT_OUTPUT_NAME: results.xml
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-${{ github.sha }}
path: coverage/
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ github.sha }}
path: test-results/Step 3 — Integration & Enhancement
Add the integration test job with a PostgreSQL service container, following the unit-tests job. This brings the complete 'quality gates → unit tests → integration tests' pipeline together, forming the full CI workflow. The integration test job uses environment-specific secrets and service containers to test against a real database — validating that the CricketPulse data layer queries work correctly, not just the pure business logic.
# File: .github/workflows/cricketpulse-ci.yml — Step 3: Complete workflow
# (append integration-tests job to Step 2's workflow)
integration-tests:
name: Integration Tests (PostgreSQL)
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: cricketpulse_test
POSTGRES_USER: cricketer
POSTGRES_PASSWORD: ipl2024
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Run integration tests
run: npx jest --testPathPattern='tests/integration'
env:
DATABASE_URL: postgresql://cricketer:ipl2024@localhost:5432/cricketpulse_test
NODE_ENV: testStep 4 — Testing & Verification
Push the complete workflow and observe each stage running in the Actions UI. Then test the failure scenarios: introduce a lint error, observe it block the test jobs, fix it, and then introduce a failing test to observe how Jest reports the failure in the log and artifact outputs.
# Push and verify the complete workflow
git add .github/workflows/cricketpulse-ci.yml src/ tests/ .eslintrc.json .prettierrc
git commit -m 'feat: complete CI pipeline with quality gates and test suite'
git push origin main
# Expected Actions UI: 4 jobs
# lint ────────────────────────────────────── ✅
# format-check ───────────────────────────── ✅
# unit-tests (starts after both above pass) ─ ✅
# integration-tests (starts after unit-tests) ✅
# TEST FAILURE SCENARIO: introduce a lint error
# In src/cricketpulse.js, add: var unusedVar = 'cricket';
git add src/cricketpulse.js
git commit -m 'test: introduce lint violation'
git push origin main
# Expected: lint ❌ → unit-tests skipped → integration-tests skipped
# format-check still runs (parallel with lint, no dependency)
# RESTORE and test a failing test scenario
git revert HEAD --no-edit && git push origin main
# In tests/cricketpulse.test.js, change expect(runRate).toBe(9.0) to toBe(10.0)
git add tests/ && git commit -m 'test: introduce failing assertion' && git push origin main
# Expected: lint ✅ format-check ✅ unit-tests ❌ integration-tests skipped
# Coverage and test result artifacts still uploaded (if: always())Warning: If the PostgreSQL service container job fails with 'Health check failed', check that no other process on the runner is using port 5432 (unlikely on hosted runners) and that the `options:` health check syntax is correct — the `>-` YAML multiline scalar is required when combining multiple `--health-` flags. Copy the options block exactly and verify there are no invisible whitespace characters if pasting from a web browser.
Extension Challenge: Add a fifth job `coverage-report` that runs after `unit-tests` with `if: always()`, downloads the coverage artifact, and writes a coverage summary to `$GITHUB_STEP_SUMMARY` showing total line coverage percentage extracted from `lcov.info`. This gives every workflow run a coverage dashboard visible in the Actions summary tab without requiring a separate coverage service.
- Parallel quality gates (lint, format-check) run simultaneously and both must pass before the unit-test job starts — this gives developers all quality violations in one CI run rather than serially.
- The `concurrency:` key with `cancel-in-progress: true` automatically cancels stale workflow runs when new commits are pushed to the same branch, saving runner minutes on PR workflows.
- Service containers with `--health-cmd` health checks ensure PostgreSQL is ready before integration test steps execute, eliminating intermittent 'connection refused' test failures.
- Uploading test results and coverage reports with `if: always()` ensures diagnostic artifacts are available precisely when they're needed most — when tests are failing.
- The 4-job pipeline (lint → unit-tests → integration-tests, format-check in parallel with lint) mirrors production CI patterns used at organisations like GitHub, Shopify, and Stripe.
- Observing both lint failure (quality gates block test jobs) and test failure (test job fails but quality gates show green) demonstrates how different failure types surface distinctly in the Actions UI.