CI/CD Pipeline
You are designing or reviewing the CI/CD pipeline for: $ARGUMENTS
A CI/CD pipeline is a quality gate, not a deployment script. Every stage must have a clear pass/fail condition. A pipeline that always passes is not a pipeline — it is a deployment button.
Principles
- Fail fast. Run the fastest, cheapest checks first (lint, type-check) before slow ones (tests, build, deploy).
- Every stage is a gate. A failing stage stops the pipeline. No bypass without an explicit documented override.
- Pipelines are code. Version-controlled, reviewed like application code, never edited in the UI.
- Reproducible builds. The same commit always produces the same artefact.
- Secrets are never logged. Use masked secrets; never
echo $SECRETor let tools print them.
1. Pipeline Stages (in order)
| Stage | What runs | Fail mode |
|---|---|---|
| Lint & format | ESLint, Prettier, PHPStan, Black, etc. | Any violation → fail |
| Type check | TypeScript, mypy, Psalm | Any type error → fail |
| Unit tests | Fast, isolated tests | Any failing test → fail |
| Build | Compile, bundle, package | Build error → fail |
| Security scan | Dependency audit, SAST, secret detection | CRITICAL finding → fail |
| Integration tests | Tests needing DB, external services | Any failing test → fail |
| E2E tests | Browser/API smoke tests against built artefact | Critical path failure → fail |
| Deploy (staging) | Deploy to staging environment | Deploy failure → fail |
| Smoke test (staging) | Post-deploy health check | Health check failure → fail |
| Deploy (production) | On main/tag only; manual approval for high-risk | Deploy failure → alert + rollback |
2. GitHub Actions
Full Pipeline Example
# .github/workflows/ci.yml
name: CI / CD
on:
push:
branches: [main, 'release/**']
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'npm' }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
runs-on: ubuntu-latest
needs: lint
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 3s
--health-retries 5
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'npm' }
- run: npm ci
- run: npm test -- --coverage
env:
DATABASE_URL: postgres://test:test@localhost:5432/testdb
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
security:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- name: Detect secrets
uses: trufflesecurity/trufflehog@main
with: { path: ./ }
- name: Dependency audit
run: npm audit --audit-level=high
- name: SAST scan
uses: github/codeql-action/init@v3
with: { languages: javascript }
- uses: github/codeql-action/analyze@v3
build:
runs-on: ubuntu-latest
needs: [test, security]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'npm' }
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
docker:
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
packages: 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 }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: ${{ github.ref == 'refs/heads/main' }}
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan image
run: |
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
trivy image --exit-code 1 --severity CRITICAL \
ghcr.io/${{ github.repository }}:${{ github.sha }}
deploy-staging:
runs-on: ubuntu-latest
needs: docker
if: github.ref == 'refs/heads/main'
environment: staging
steps:
- name: Deploy to staging
run: |
kubectl set image deployment/myapp \
app=ghcr.io/${{ github.repository }}:${{ github.sha }} \
-n staging
kubectl rollout status deployment/myapp -n staging --timeout=5m
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://myapp.example.com
steps:
- name: Deploy to production
run: |
kubectl set image deployment/myapp \
app=ghcr.io/${{ github.repository }}:${{ github.sha }} \
-n production
kubectl rollout status deployment/myapp -n production --timeout=10m
3. GitLab CI Equivalent
# .gitlab-ci.yml
stages: [lint, test, build, security, deploy]
default:
image: node:22-alpine
cache:
key: $CI_COMMIT_REF_SLUG
paths: [node_modules/]
before_script:
- npm ci
lint:
stage: lint
script:
- npm run lint
- npm run typecheck
test:
stage: test
services:
- name: postgres:16-alpine
alias: postgres
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: postgres://test:test@postgres:5432/testdb
script:
- npm test -- --coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
security:
stage: security
script:
- npm audit --audit-level=high
allow_failure: false
deploy-staging:
stage: deploy
environment: staging
only: [main]
script:
- kubectl set image deployment/myapp app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n staging
- kubectl rollout status deployment/myapp -n staging --timeout=5m
deploy-production:
stage: deploy
environment: production
only: [main]
when: manual
script:
- kubectl set image deployment/myapp app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production --timeout=10m
4. Pipeline Checklist
- [ ] Lint and type-check run before tests
- [ ] Tests run in parallel where possible
- [ ] Secrets never echoed or logged
- [ ] Docker image scanned for CVEs before deploy
- [ ] Secrets detected (TruffleHog or GitLeaks)
- [ ] Dependency audit runs (
npm audit,pip-audit,composer audit) - [ ] Staging deploy required before production
- [ ] Production deploy requires manual approval or is tag-triggered
- [ ] On failure: alert team, do not silently fail
- [ ] Pipeline runs complete in < 15 minutes (split or optimise if longer)
- [ ] Flaky tests are quarantined, not re-tried indefinitely
5. Common Anti-Patterns to Fix
| Anti-pattern | Fix |
|---|---|
continue-on-error: true on security steps |
Remove it — security failures must block |
| Hard-coded credentials in YAML | Use masked CI secrets |
actions/checkout without pinned SHA |
Pin to @v4 or a specific commit SHA |
| Deploying on every PR | Deploy to staging only on main; use preview envs for PRs |
| Single long job with 20 steps | Split into parallel jobs with needs dependencies |
No cache on npm ci / pip install |
Add cache: step to avoid 3-minute reinstalls |