100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
CI/CD with GitHub Actions
50 minintermediate

CI Practice — Build & Test Workflow

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.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is building the complete match management infrastructure: the pitch is prepared (dev), the warm-up match is played (staging approval), and only after the selectors sign off does the Test match begin (production). You'll experience being both the deployer (pushing the commit that initiates the pipeline) and the approver (clicking the approval button in GitHub's reviewer interface) — understanding both sides of the gate is essential for designing gates that are genuinely useful rather than bureaucratic obstacles.

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.

Analogy🏏Cricket
🏏 Think of it like cricket: configuring the three environments and their protection rules in the GitHub UI before writing any workflow is like agreeing the match's playing conditions with the officials before the toss — not mid-innings. Just as environment protection rules live entirely in settings and there is no YAML for them, the tournament's rules on reviews, powerplays, and appeals are fixed by the governing body off the field, not written into a batsman's technique. Just as the workflow simply names an environment and GitHub enforces whatever rules are configured, a player simply takes the field and the umpires enforce the pre-agreed conditions — the player doesn't carry the rulebook, the officials do. The payoff: settle the playing conditions once, in the right place, and every delivery afterwards is automatically governed by them without cluttering the play itself.
bash
# 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.

Analogy🏏Cricket
🏏 Think of it like cricket: The warm-up session runs on the hotel practice ground with no spectators, no formal protocols — players take the field and start immediately. The dev environment is the hotel practice ground: automated, immediate, no gatekeeping. Confirming the dev pipeline runs automatically before adding gated environments is like confirming the practice session proceeds smoothly before worrying about the match-day protocols.
yaml
# 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.

Analogy🏏Cricket
🏏 Think of it like cricket: The warm-up session is complete (dev deployed). Now the match-day protocols begin: the umpires inspect the pitch, the team captains arrive for the toss, the grounds team prepare the outfield. The staging approval is the toss ceremony — a formal, structured pause before the match begins, requiring the participating parties to confirm readiness before play commences.
yaml
# 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.

Analogy🏏Cricket
🏏 Think of it like cricket: After the warm-up (dev) and the pitch inspection ceremony (staging), the Test match finally begins with all the formal protocols: national anthems, player presentations, the coin toss, a final equipment check. Each is a structured delay ensuring everything is ready before the ball is bowled. The 2-minute production wait timer is the Test match's opening ceremony — a deliberate pause giving everyone a final opportunity to abort if something has gone wrong.
yaml
# 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: test

Step 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.

Analogy🏏Cricket
🏏 Think of it like cricket: running the three scenarios — automatic push, manual dispatch with production, and a rejected staging deploy — is like rehearsing how a match unfolds under different calls before the real fixture. Just as an automatic push flows through dev and staging but stops short of production, an ordinary passage of play proceeds freely up to the boundary rope but can't cross into a scoring appeal without review. Just as manual dispatch with production enabled runs the full three-stage pipeline, a captain formally opting to declare unlocks the complete sequence of events that follows. And just as a deliberately rejected staging deployment halts the pipeline, a third umpire ruling not-out on review sends the batsman back and the passage of play simply stops there. Testing all three teaches you how each gate behaves. The payoff: like rehearsing every umpiring outcome in advance, you learn exactly how approvals let a deploy through, how manual triggers open the full path, and how a rejection cleanly stops the release.
bash
# 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.
Lesson 8 of 24
0% complete