GitHub Actions CI Pipeline Setup

Continuous Integration (CI) is the practice of automatically testing every code change as soon as it is committed. A CI pipeline catches bugs early, enforces code quality standards, and gives the team confidence that the codebase stays in a healthy state. This topic walks through building a complete, production-ready CI pipeline from scratch.

What a CI Pipeline Does

A CI pipeline runs automatically on every pull request and push. It checks that the new code:

  • Compiles without errors
  • Passes all automated tests
  • Meets code style and formatting standards
  • Contains no known security vulnerabilities
  • Has acceptable test coverage
Developer pushes code
        ↓
CI pipeline starts automatically
        ↓
  ┌─────────────┬──────────────┬──────────────┐
  │   lint job  │  test job    │ security job │
  └─────────────┴──────────────┴──────────────┘
        ↓ (all must pass)
  ┌─────────────────────────────┐
  │        build job            │
  └─────────────────────────────┘
        ↓
Pull request shows: ✓ All checks passed
        ↓
Team can safely merge the code

Building a Node.js CI Pipeline

name: Node.js CI

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

jobs:
  lint:
    name: Code Quality Check
    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
      - run: npm run format:check

  test:
    name: Run Tests
    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 test -- --coverage

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: ./coverage
          retention-days: 5

  build:
    name: Build Application
    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

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

Understanding the Pipeline Flow

Trigger: pull_request → main
         │
         ├── lint job starts ──────────────────────────────┐
         │                                                 │
         └── test job starts ───────────────────────────── ┤ (both run in parallel)
                                                           │
                                             build job waits for both
                                                           │
                                             build job runs after lint + test pass

The needs: [lint, test] line on the build job creates this gate. The build only runs when both quality checks succeed. This saves compute time by not building code that already has lint or test failures.

Setting Branch Protection Rules

Connect your CI pipeline to GitHub's branch protection feature to enforce passing checks before merging:

  1. Go to SettingsBranches in your repository
  2. Click Add branch protection rule
  3. Set the branch name pattern to main
  4. Enable Require status checks to pass before merging
  5. Search for and select your workflow jobs: lint, test, build
  6. Enable Require branches to be up to date before merging
  7. Click Save changes

Once configured, GitHub blocks the merge button on any pull request where CI checks fail or have not yet completed.

Adding a Python CI Pipeline

name: Python CI

on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'

      - run: pip install -r requirements-dev.txt

      - name: Lint with flake8
        run: flake8 src/ tests/

      - name: Type check with mypy
        run: mypy src/

      - name: Run pytest with coverage
        run: pytest --cov=src --cov-report=xml

      - name: Upload coverage to artifact
        uses: actions/upload-artifact@v4
        with:
          name: coverage-xml
          path: coverage.xml

CI Status Badges

GitHub generates a status badge image for any workflow. Add it to your README so anyone visiting the repository can see the current CI status at a glance.

Badge URL format:
https://github.com/{owner}/{repo}/actions/workflows/{filename}.yml/badge.svg

In README.md:
![CI](https://github.com/acme/my-app/actions/workflows/ci.yml/badge.svg)
Badge appearance:
  passing → green badge with "passing"
  failing → red badge with "failing"

Common CI Best Practices

  • Run lint and tests in parallel — no reason for them to wait on each other
  • Gate the build job on all quality checks with needs
  • Use npm ci instead of npm install — it uses exact lockfile versions
  • Cache dependencies to keep runs under 2 minutes
  • Upload test coverage reports as artifacts for review
  • Set branch protection to enforce CI before merging
  • Pin all action versions to exact SHAs in production repositories

Measuring CI Pipeline Performance

Target metric        | Goal
---------------------|---------------------------
Total pipeline time  | Under 5 minutes
Test job time        | Under 3 minutes
Cache hit rate       | Above 80%
Flaky test rate      | Below 1%
Pipeline success rate| Above 95%

Monitor these metrics in the GitHub Actions tab. Consistently slow or failing pipelines hurt developer productivity and undermine confidence in the CI process.

Leave a Comment

Your email address will not be published. Required fields are marked *