CI/CD ›
Platforms & Pipeline Design
This page compares the major CI/CD platforms, describes the common pipeline topologies (linear, DAG, matrix, fan-out/fan-in), covers the techniques that keep pipelines fast, and sets out a testing strategy that fits inside them. Rollout mechanics are on Deployment Strategies; credentials, supply-chain hardening, and GitOps are on Security, GitOps & Operations.
Core Concepts
Every CI/CD system uses much the same model, though the vocabulary differs:
| Concept | Meaning | GitHub Actions | GitLab CI/CD | Jenkins |
|---|---|---|---|---|
| Definition file | Pipeline as code, versioned with the repo | .github/workflows/*.yml |
.gitlab-ci.yml |
Jenkinsfile (Groovy) |
| Run | One execution triggered by an event | workflow run | pipeline | build |
| Unit of scheduling | Runs on one machine or container | job | job | stage / node block |
| Step | One command or reusable action | step (run / uses) |
script line |
step |
| Executor | Machine that runs jobs | runner (hosted or self-hosted) | runner | agent |
| Reuse | Share logic across repos | reusable workflows, actions | CI/CD components, include
|
shared libraries |
| Deployment target | Named environment with protection rules | environment | environment | (plugins) |
Two properties matter more than any feature list: where jobs run (vendor-hosted vs your own runners, which determines cost, network access, and blast radius) and how pipelines are composed (reusable units that let a platform team maintain one hardened template instead of hundreds of copies).
Platforms
GitHub Actions
Event-driven workflows tightly integrated with GitHub repositories, pull requests, and packages. The Marketplace of third-party actions is its biggest strength and, as the 2025 tj-actions/changed-files compromise showed, its biggest supply-chain risk (see Hardening GitHub Actions).
# .github/workflows/python.yml
name: Python CI
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -r requirements.txt
- run: pytest -q
Notable capabilities:
- Hosted runners for Linux, Windows, and macOS, including native arm64 Linux runners. Larger runners go up to 96 vCPU / 384 GB RAM on x64 and 64 vCPU on arm64, and there is a GPU runner (NVIDIA T4) for ML workloads.
- Environments with required reviewers, wait timers, and branch restrictions. These are the manual gate in continuous delivery.
-
Reusable workflows (
workflow_call) and composite actions for sharing pipelines across repositories. - OIDC federation to AWS, Azure, GCP, and Vault, so no long-lived cloud keys are stored (see OIDC federation).
-
Merge queue, which tests each PR against the latest
mainplus everything queued ahead of it before merging. -
Artifact attestations (
actions/attest) for signed SLSA build provenance.
Pricing (2026): usage in public repositories is free. GitHub cut hosted-runner prices by up to 39% from 1 January 2026. A planned per-minute platform charge for self-hosted runners was announced for March 2026 and then postponed after community feedback, so check current billing documentation before planning around it.
GitLab CI/CD
Part of GitLab’s single-application DevSecOps platform, which includes the source host, registry, security scanners, and environments in one product. Pipelines are defined in .gitlab-ci.yml. Use rules: for conditions; the older only:/except: keywords are deprecated in its favor.
# .gitlab-ci.yml
stages: [test, build, deploy]
default:
image: node:24
test:
stage: test
script:
- npm ci
- npm test
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths: [dist/]
deploy:
stage: deploy
needs: [build] # DAG edge: start as soon as build finishes
environment: production
script:
- ./scripts/deploy.sh dist/
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # continuous delivery: a human clicks deploy
Notable capabilities: needs: for DAG pipelines that ignore stage boundaries, parallel: matrix: for matrix jobs, parent/child and multi-project pipelines, CI/CD components published to a CI/CD Catalog for versioned reuse, merge trains (GitLab’s merge queue), and id_tokens: for OIDC authentication to clouds and Vault.
Jenkins
The long-standing self-hosted automation server. Its plugin ecosystem can integrate with almost anything, and it is still common in enterprises with on-premises requirements. The cost is operational: the controller, plugins, and agents need patching, and plugin compatibility is a recurring source of breakage. Prefer declarative pipelines over scripted ones, and run builds in ephemeral container agents rather than on the controller.
// Jenkinsfile (declarative)
pipeline {
agent { docker { image 'maven:3-eclipse-temurin-21' } }
options { timeout(time: 30, unit: 'MINUTES') }
stages {
stage('Build') { steps { sh 'mvn -B -DskipTests package' } }
stage('Test') {
steps { sh 'mvn -B test' }
post { always { junit 'target/surefire-reports/*.xml' } }
}
stage('Deploy') {
when { branch 'main' }
steps { sh './deploy.sh' }
}
}
}
CircleCI
A hosted CI service known for fast Docker-based executors, strong caching, and orbs (versioned, reusable config packages). It also offers macOS, arm, and GPU resource classes and self-hosted runners.
# .circleci/config.yml
version: 2.1
jobs:
build-and-test:
docker:
- image: cimg/node:24.11
steps:
- checkout
- restore_cache:
keys: [npm-{{ checksum "package-lock.json" }}]
- run: npm ci
- save_cache:
key: npm-{{ checksum "package-lock.json" }}
paths: [~/.npm]
- run: npm test
workflows:
main:
jobs: [build-and-test]
Other Platforms
- Buildkite: a hosted control plane with agents that always run on your own infrastructure. Popular for large monorepos and security-sensitive builds because source code and secrets never leave your network.
- Azure Pipelines: part of Azure DevOps. Strong Windows/.NET support and multi-stage YAML pipelines.
- Tekton: a Kubernetes-native framework (CNCF) where tasks and pipelines are custom resources. It is usually a building block for internal platforms rather than something developers use directly. Tekton Chains adds signed SLSA provenance.
- Dagger: pipelines written as code in Go, Python, or TypeScript that run in containers, identically on a laptop and on any CI host. Useful for escaping YAML and vendor lock-in.
- Drone / Harness CI: Drone is container-native and now owned by Harness. Travis CI, once the default for open source, has largely been replaced by GitHub Actions and is mostly found in legacy projects.
Platform Comparison
| Platform | Hosting | Config | Strengths | Trade-offs | Typical fit |
|---|---|---|---|---|---|
| GitHub Actions | SaaS, self-hosted runners | YAML | Deep GitHub integration, Marketplace, OIDC, attestations | Third-party action supply-chain risk; YAML logic gets unwieldy | Projects hosted on GitHub |
| GitLab CI/CD | SaaS or self-managed | YAML | One platform for SCM, CI, registry, and security; DAGs; components | Heavier to self-manage; best features in paid tiers | Teams wanting an all-in-one DevSecOps suite |
| Jenkins | Self-hosted | Groovy | Unlimited flexibility, huge plugin catalog | Operational burden, plugin drift, security patching | Legacy or on-prem enterprise estates |
| CircleCI | SaaS, self-hosted runners | YAML | Fast executors, caching, orbs, test splitting | Credit-based cost at scale | Speed-sensitive SaaS teams |
| Buildkite | Hybrid (your agents) | YAML | Scale, data stays in your network, dynamic pipelines | You operate the agent fleet | Large monorepos, regulated builds |
| Azure Pipelines | SaaS, self-hosted agents | YAML | Windows/.NET, Azure integration | Microsoft-centric | Azure and .NET organizations |
| Tekton | Your Kubernetes cluster | Kubernetes CRDs | Cloud-native, composable, Chains provenance | Low-level; needs a platform team | Internal developer platforms |
Pipeline Topologies
Linear
Stages run strictly in order: build → test → deploy. Easy to reason about, and adequate for small projects. The cost is that total time is the sum of every stage, even when stages do not depend on each other.
Directed Acyclic Graph (Fan-out / Fan-in)
Jobs declare their actual dependencies (needs: in both GitHub Actions and GitLab), and the scheduler runs everything whose inputs are ready. Independent checks fan out in parallel after the build, and a single job fans in once they all succeed. The critical path, not the sum of all jobs, determines wall-clock time.
flowchart LR
B["build<br/>(produce artifact)"] --> U["unit tests"]
B --> L["lint + typecheck"]
B --> S["security scan"]
B --> I1["integration shard 1"]
B --> I2["integration shard 2"]
U --> P["publish image<br/>(fan-in)"]
L --> P
S --> P
I1 --> P
I2 --> P
P --> D["deploy staging"]
# GitHub Actions: fan-out on `needs: build`, fan-in on `needs: [ ... ]`
jobs:
build: { runs-on: ubuntu-latest, steps: [ ... ] }
unit: { needs: build, runs-on: ubuntu-latest, steps: [ ... ] }
lint: { needs: build, runs-on: ubuntu-latest, steps: [ ... ] }
scan: { needs: build, runs-on: ubuntu-latest, steps: [ ... ] }
publish:
needs: [unit, lint, scan]
runs-on: ubuntu-latest
steps: [ ... ]
Matrix
One job definition expanded over a cross-product of parameters, such as runtime versions × operating systems. Use exclude/include to trim combinations, and fail-fast: false when you want the full picture rather than the first failure.
strategy:
fail-fast: false
matrix:
node: [20, 22, 24]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
- os: windows-latest
node: 20
GitLab’s equivalent is parallel: matrix:. Matrices multiply cost quickly, so test the full matrix on main or nightly and a representative subset on PRs.
Monorepo: Affected-Target Pipelines
In a monorepo, rebuilding everything on every change does not scale. Pipelines compute the set of affected projects from the changed paths and the dependency graph, then build and test only those. Nx, Turborepo, Bazel, Pants, and Please all do this. At minimum, path filters (on.push.paths, GitLab rules: changes:) skip unrelated workflows. See Monorepo Architecture and Monorepo Tooling.
Build Once, Promote Everywhere
Deployment stages should consume the artifact produced by the build stage, identified by an immutable digest, rather than rebuilding it. Promotion then means pointing the next environment at the same digest. This guarantees that what was tested is what ships, and it makes provenance and signing meaningful.
flowchart LR
CI["CI build"] -->|"push image@sha256:ab12..."| REG[("Registry")]
REG -->|"same digest"| DEV["dev"]
REG -->|"same digest"| STG["staging"]
REG -->|"same digest"| PRD["production"]
Keeping Pipelines Fast
Pipeline duration is a developer-productivity metric. The main levers, roughly in order of payoff:
| Technique | How | Typical effect |
|---|---|---|
| Dependency caching | Cache package-manager stores keyed on the lockfile hash (setup-* actions’ cache: input, GitLab cache:key:files) |
Minutes saved on install steps |
| Container layer caching | BuildKit cache export (--cache-to type=registry or type=gha), and ordering Dockerfile layers from least to most volatile |
Image builds become incremental |
| Parallelism and sharding | DAG fan-out; split test suites across N jobs by timing data (GitLab parallel: N with CI_NODE_INDEX, CircleCI test splitting, Playwright/Jest --shard) |
Near-linear speedup for large suites |
| Test selection | Run only tests affected by the change (dependency graph, test impact analysis); run the full suite on main
|
Large PR-time savings in big repos |
| Remote build cache | Share compiled outputs across machines and developers (Bazel remote cache, Nx/Turborepo remote cache, Gradle build cache) | Unchanged targets are never rebuilt |
| Cancel superseded runs |
concurrency: cancel-in-progress (GitHub), interruptible: true (GitLab) |
Frees runners during rapid pushes |
| Right-sized runners | Larger or arm64 runners for CPU-bound jobs; they often cost less per completed job | Shorter compile and test times |
Also consider a merge queue (GitHub merge queue, GitLab merge trains). It serializes merges and tests each PR combined with the ones ahead of it, which keeps main green without requiring every author to rebase and rerun.
Testing Strategy
The classic test pyramid says to have many fast, isolated unit tests, fewer integration tests, and a small number of slow end-to-end tests. The pipeline mirrors it: the cheapest, most frequent tests run earliest.
| Layer | Scope | When it runs | Target duration | Examples |
|---|---|---|---|---|
| Static analysis | Lint, format, type-check | Every push (also pre-commit) | < 1 min | ESLint, Ruff, tsc, go vet
|
| Unit | One function or class, no I/O | Every push | < 5 min | Jest, Vitest, pytest, JUnit |
| Integration / contract | Real database or queue in containers; API contracts between services | Every PR | 5–15 min | Testcontainers, Pact |
| End-to-end | Full user flows through a browser or API against a deployed environment | Pre-merge smoke subset; full suite on staging | 10–30 min | Playwright, Cypress |
| Performance | Load and latency against SLO thresholds | On main, nightly, or pre-release |
Varies | k6, Gatling, Locust |
| Post-deploy verification | Smoke tests and canary analysis in production | Every deploy | Minutes | Synthetic checks, canary analysis |
Practices that keep the suite trustworthy:
- Treat flaky tests as defects. Detect them automatically (reruns that change outcome), quarantine them out of the blocking path, and track them to a fix. Common causes are real time and sleeps (mock the clock), test-order dependence, shared state, and unpinned external services.
- Use real dependencies in containers (Testcontainers, service containers) rather than mocks for integration tests. It catches driver, SQL-dialect, and migration bugs that mocks hide.
- Consumer-driven contract tests (Pact) let microservices verify API compatibility without standing up the whole system for end-to-end tests.
-
Gate on meaningful thresholds. Examples are coverage on changed lines rather than an absolute percentage, or a p95 latency budget in k6
thresholds. Blanket gates tend to be gamed.
// k6 load test: the pipeline fails if either threshold is breached
import http from 'k6/http';
export const options = {
vus: 50,
duration: '2m',
thresholds: {
http_req_duration: ['p(95)<300'], // ms
http_req_failed: ['rate<0.01'],
},
};
export default function () {
http.get(`${__ENV.BASE_URL}/api/health`);
}
See Also
- Deployment Strategies — rolling, blue-green, canary, and feature flags
- Security, GitOps & Operations — securing pipelines, provenance, and GitOps
- Docker — reproducible build environments and layer caching
- Please Build — a monorepo build system with affected-target builds
- Monorepo Tooling — Bazel, Nx, Turborepo, and remote caching