# CI/CD Fundamentals

## 1. Definitions

- **Continuous Integration (CI)**: every change is merged to the main branch frequently (at least daily) and **automatically built and tested**. The goal: integration problems surface in minutes, not at the end of a sprint.
- **Continuous Delivery (CD)**: every change that passes the pipeline is **releasable** — deploying to production is a business decision (a button), not an engineering project.
- **Continuous Deployment**: every change that passes the pipeline **is deployed** to production automatically, with no human gate.

```
commit → build → test → package (image) → deploy to staging → checks → deploy to production
└──────────── CI ──────────────┘└──────────────── CD (delivery / deployment) ────────────────┘
```

## 2. A Typical Pipeline

| Stage | What happens | Fails the build when |
|---|---|---|
| checkout + setup | fetch code, restore dependency cache | — |
| lint / format / type-check | static checks (ESLint, Prettier, `tsc --noEmit`) | any error |
| unit tests | fast, isolated tests | a test fails, coverage drops below the threshold |
| build | compile, bundle, build the container image | build error |
| security | dependency audit, secret scan, SAST, image scan | high/critical findings with fixes available |
| integration tests | app + real DB/broker in containers (service containers, Testcontainers) | a test fails |
| publish artifact | push the image tagged with the **commit SHA** (immutable) | — |
| deploy staging | deploy the *same* artifact | rollout fails |
| smoke / e2e tests | a few critical journeys against staging | a test fails |
| deploy production | promote the same artifact (manual approval or automatic) with a safe strategy (`02-deployment_strategies/`) | health checks / SLO burn → automatic rollback |

Principles: **build once, promote the same artifact** through environments (never rebuild for prod); **fast feedback first** (lint and unit tests before slow stages); **everything as code** (pipeline YAML in the repo).

## 3. GitHub Actions in Practice

```yaml
name: ci
on:
  pull_request:
  push:
    branches: [main]

concurrency: { group: ci-${{ github.ref }}, cancel-in-progress: true }   # stop superseded runs
permissions: { contents: read }                                         # least privilege by default

jobs:
  test:
    runs-on: ubuntu-24.04
    strategy:
      matrix: { node: [20, 22] }
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: test }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres" --health-interval 5s --health-retries 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "${{ matrix.node }}", cache: npm }
      - run: npm ci
      - run: npm run lint && npm run typecheck
      - run: npm test -- --coverage
        env: { DATABASE_URL: postgres://postgres:test@localhost:5432/postgres }

  image:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-24.04
    permissions: { contents: read, packages: write, id-token: write }
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/acme/api:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

Key concepts: **workflows** (YAML files) triggered by **events**; **jobs** run on **runners** in parallel unless linked with `needs`; **steps** run commands or reusable **actions**; **matrix** fans out over versions; **services** start sidecar containers; **secrets** and **environments** (with required reviewers) gate deployments.

## 4. Keeping Pipelines Fast and Trustworthy

- **Cache** dependency downloads and Docker layers; run independent jobs **in parallel**; split slow test suites (sharding).
- **Target: < 10 minutes** to a green PR. Slow pipelines get bypassed.
- **Flaky tests** destroy trust — quarantine and fix them; never "just re-run until green".
- **Trunk-based development** with short-lived branches and feature flags keeps integration continuous.
- **Branch protection**: required checks and reviews before merging to `main`.

## 5. Pipeline Security

The CI system holds the keys to production — treat it as production:

- least-privilege tokens (`permissions:` per job), **OIDC** federation to clouds instead of long-lived cloud keys in secrets
- pin third-party actions to a **commit SHA**, not a moving tag
- don't expose secrets to workflows triggered by forks (`pull_request_target` pitfalls)
- secret scanning on every push; never `echo` secrets
- sign artifacts and record provenance (SLSA, cosign attestations)

## Under the Hood: What Happens When You Push

1. The Git host emits a **webhook event** (`push`, `pull_request`) to the CI service.
2. The CI service evaluates the workflow files **at that commit**, expands the job graph (matrix × jobs), and queues jobs.
3. A **runner** (an ephemeral VM or container for hosted runners) picks up a job, gets a short-lived job token, checks out the exact commit SHA, and executes steps as processes — each `uses:` action is itself code (JavaScript, a container, or composite YAML) downloaded at its pinned version.
4. Steps share the workspace filesystem within a job; **jobs don't share files** unless they upload/download artifacts or use caches.
5. Results are reported back as **commit statuses/checks**, which branch protection uses to allow or block merges.

Because hosted runners are fresh each time, every byte not cached must be re-downloaded — which is why caching and image layer reuse dominate pipeline speed.

![A commit flows through lint and type checks, unit tests, build and security scans, integration tests, publishing an image tagged with the commit SHA, deployment to staging with smoke tests, and promotion of the same artifact to production](diagrams/01-pipeline-stages.svg)

## Common Pitfalls

1. **Rebuilding for each environment** — what you tested isn't what you ship.
2. **Mutable tags** (`:latest`) for deployments — you can't tell what's running or roll back precisely.
3. **Slow pipelines** that developers bypass.
4. **Flaky tests** tolerated.
5. **Long-lived cloud credentials** in CI secrets; unpinned third-party actions.
6. **No automated rollback** — CD without a way back.

## Quick Reference

```
CI: merge often · build + test every change · fast feedback (< 10 min)
CD: same artifact promoted through environments · tagged by commit SHA · gated or automatic
GitHub Actions: on → jobs (needs, matrix, services) → steps (uses / run) · permissions · environments
security: least privilege · OIDC to cloud · pin actions by SHA · scan · sign
```
