Skip to content

CI/CD

CI (continuous integration): every push runs lint, tests, and builds automatically. CD (continuous delivery/deployment): every green build can — or does — ship. This is how code goes from commit to production without a hero.

Table of Contents

Installation / Setup

CI runs on a runner (a machine that executes your pipeline). Options:

Platform File location Notes
GitHub Actions .github/workflows/*.yml free for public repos; see github-actions.md
GitLab CI .gitlab-ci.yml built-in runners
Jenkins Jenkinsfile self-hosted, flexible
CircleCI .circleci/config.yml cloud or self-hosted

Local sanity before pushing: run the same commands yourself (lint, pytest, docker build) so CI failures are surprises only in the pipeline, not the code.

Core Syntax

Pipeline anatomy

trigger (push/PR) → jobs (sequential/parallel) → steps → artifacts → (deploy)

A job = one runner + ordered steps. Steps fail fast: a failed test stops the job; a failed job can block the pipeline.

The stages of a typical data/ML pipeline

Stage Checks Example
Lint/format style, obvious bugs ruff check .
Type check static correctness mypy src/
Unit tests logic works pytest
Build it packages docker build
Quality gates performance regressions model metric thresholds
Deploy ship to env push image + rollout

Quality gates for ML (don't just check "tests pass")

  • Data checks: schema, drift, missing-value thresholds (see Data Engineering).
  • Model checks: eval metrics on a fixed benchmark ≥ baseline; no NaN weights; reproducibility seed.
  • Package checks: import works, inference latency bound, image size bound.

Common Operations

The basic loop

git push origin feature/foo        # CI kicks off on the PR
# watch in the UI / CLI
gh pr checks                       # statuses
gh run list / gh run watch         # logs
# merge only when all gates pass

Artifacts: what CI produces

Artifact Consumed by
lint/test reports humans + bots
coverage report code-review gate
container image (app:v1.2.0) deploy step
model file + metrics release, model registry
benchmark report humans

Artifacts should be immutable and versioned (tag = commit SHA or semantic version).

Cache to keep pipelines fast

# typical caches: pip/uv cache, ~/.cache/huggingface, Docker layer cache
# GitHub Actions: actions/cache with key = hash of lockfile

Practical Examples

1. CI for a Python data package

# .github/workflows/ci.yml
on:
  push: { branches: [main] }
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix: { python-version: ["3.11", "3.12"] }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: ${{ matrix.python-version }} }
      - run: pip install -e ".[dev]"
      - run: ruff check src tests
      - run: mypy src
      - run: pytest --cov=src --cov-report=xml
      - uses: codecov/codecov-action@v4

2. Model quality gate before merge

# pseudo-steps (adjust to your framework)
- run: python scripts/eval.py --dataset benchmark.parquet --artifact model.pt
  env:
    MIN_ACCURACY: "0.90"
- run: python scripts/assert_gate.py --metric accuracy --min $MIN_ACCURACY

3. CD: build, push, deploy on tag

on:
  push: { tags: ["v*"] }
jobs:
  release:
    runs-on: ubuntu-latest
    permissions: { contents: write, packages: write }
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t ghcr.io/you/app:${{ github.ref_name }} .
      - run: docker push ghcr.io/you/app:${{ github.ref_name }}
      - uses: actions/gh-pages@v1   # e.g. docs; or call your deploy platform's CLI

Common Patterns

# Path filtering: don't run everything for a README change
on:
  pull_request:
    paths: ["src/**", "tests/**", ".github/**"]

# Concurrency: cancel stale runs on the same branch
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true
# Deploy only after ALL checks pass AND review approves
# main branch protection: "Require status checks to pass" + "Require PR review"

Best Practices

  • Test the cheapest things first (lint → unit → integration) so failures fail fast.
  • Fail the pipeline on warnings you care about — silent warnings rot.
  • Keep builds under ~10 minutes; cache aggressively; parallelize jobs.
  • One source of truth for versions: lockfile, image tag, and artifact tag all derive from the commit.
  • Never let devs skip CI — branch protection enforces it.
  • Deploy from artifacts, not from a re-run — the tested image is the shipped image.
  • Make rollback one click/one command (previous image/tag).
  • Version, version, version: reproducible CI today prevents mysterious "it passed last week."

Common Mistakes

Mistake Consequence Fix
Running lint/tests only locally Broken main arrives silently Require CI on PRs
No caching 5-minute installs every run actions/cache on lockfiles
Tests that depend on order/network Flaky CI Deterministic fixtures; pin services
Building twice (CI test image ≠ deploy image) Untested artifact ships Single build → test that artifact
Secrets in workflow files Leaked credentials ${{ secrets.X }} only; never inline
Huge monolith pipeline Slow feedback Split jobs; fail fast; parallelize
Ignoring flaky tests "Known" failures mask real ones Fix or quarantine, don't tolerate
Deploying on every merge without gates Risky Branch protection + staging

Troubleshooting

Symptom Cause Fix
CI passes locally, fails remotely Version drift Lock deps; match Python versions
Pipeline stuck "queued" No runners / concurrency Check runner availability
Secret *** where expected Not configured Add to repo settings; verify name
Tests pass alone, fail together Shared state Isolate temp files/DBs per test
permission denied in CI Token scopes Grant contents: write, packages: write as needed
Slow pipeline Install every run Cache pip/uv/HF cache

Official Documentation

Last Updated

2026-09-06 — Initial version.