CI/CD Mastery
End-to-end guide for building, testing, securing, and deploying software with continuous integration and continuous delivery pipelines.
1. Pipeline Fundamentals
Core Concepts
| Concept | Definition |
|---|---|
| Continuous Integration (CI) | Automatically build and test every commit to detect regressions early. |
| Continuous Delivery (CD) | Every commit that passes CI is ready to deploy; deployment is manual gate. |
| Continuous Deployment | Every passing commit is deployed automatically β no human gate. |
| Pipeline | Ordered set of stages/jobs that transform source code into running software. |
| Stage | Logical group of jobs that run in sequence (build β test β deploy). |
| Job / Step | Atomic unit of work inside a stage β runs on a single agent/runner. |
| Artifact | Immutable file produced by a job and consumed downstream. |
| Cache | Mutable store to speed up dependency resolution across runs. |
Pipeline Anatomy
ββββββββββββ ββββββββββββ βββββββββββββ ββββββββββββ ββββββββββββ
β Source ββββΆβ Build ββββΆβ Test ββββΆβ Stage ββββΆβ Deploy β
β Checkout β β Compile β β Unit/Int β β Artifact β β Prod β
ββββββββββββ ββββββββββββ βββββββββββββ ββββββββββββ ββββββββββββ
Design Principles
Fail fast β run cheap/fast tests first; expensive E2E tests later.
Idempotent β re-running a job produces the same result.
Deterministic β pin dependencies & toolchains; no floating tags.
Minimal secrets β least-privilege tokens scoped to the job.
Observable β structured logs, metrics on duration/failure rates.
Immutable artifacts β build once, promote the same artifact through environments.
2. GitHub Actions
Workflow Syntax
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
id-token: write # OIDC for cloud auth
security-events: write # CodeQL
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.meta.outputs.version }}
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: dist
path: dist/
retention-days: 5
test:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx vitest run --shard=${{ matrix.shard }}/3
security:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/analyze@v3
with:
languages: javascript
docker:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix=
type=semver,pattern={{version}}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: docker
runs-on: ubuntu-latest
environment: staging
steps:
- run: echo "Deploy ${{ needs.docker.outputs.version || 'latest' }} to staging"
deploy-prod:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- run: echo "Deploy ${{ needs.docker.outputs.version || 'latest' }} to production"
Actions Matrix
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
Reusable Workflows
# .github/workflows/_deploy.yml (reusable)
on:
workflow_call:
inputs:
environment:
required: true
type: string
image_tag:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: |
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
-d "{\"tag\": \"${{ inputs.image_tag }}\"}"
# .github/workflows/ci.yml (caller)
jobs:
deploy-staging:
uses: ./.github/workflows/_deploy.yml
with:
environment: staging
image_tag: ${{ needs.docker.outputs.version }}
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
OIDC Authentication
# AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubOIDC
aws-region: us-east-1
GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/actions
service_account: deploy@project.iam.gserviceaccount.com
Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
Caching Strategies
# Native cache via setup-* actions
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # auto-caches ~/.npm
Manual cache
uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.local/bin/cog
key: python-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
restore-keys: python-${{ runner.os }}-
Docker layer cache via buildx
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max
3. GitLab CI
.gitlab-ci.yml
stages:
- build
- test
- security
- package
- deploy
variables:
REGISTRY: $CI_REGISTRY
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_DRIVER: overlay2
default:
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
before_script:
- npm ci --ignore-scripts
---------- BUILD ----------
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
---------- TEST ----------
test:unit:
stage: test
script:
- npx vitest run --coverage
coverage: '/All files[^|]\|[^|]\s+([\d.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test:integration:
stage: test
services:
- postgres:16-alpine
variables:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
DATABASE_URL: postgres://test:test@postgres:5432/testdb
script:
- npx vitest run --config vitest.integration.config.ts
---------- SECURITY ----------
sast:
stage: security
trigger:
include:
- template: Security/SAST.gitlab-ci.yml
container_scanning:
stage: security
trigger:
include:
- template: Security/Container-Scanning.gitlab-ci.yml
---------- PACKAGE ----------
docker:build:
stage: package
image: docker:24
services:
- docker:24-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
script:
- docker build -t $IMAGE .
- docker push $IMAGE
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
---------- DEPLOY ----------
.deploy_template:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context $KUBE_CONTEXT
- kubectl set image deployment/$APP $APP=$IMAGE -n $NAMESPACE
- kubectl rollout status deployment/$APP -n $NAMESPACE --timeout=120s
deploy:staging:
extends: .deploy_template
variables:
APP: myapp
NAMESPACE: staging
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy:production:
extends: .deploy_template
variables:
APP: myapp
NAMESPACE: production
environment:
name: production
url: https://app.example.com
rules:
- if: $CI_COMMIT_TAG
when: manual
---------- REVIEW APPS ----------
review:
extends: .deploy_template
variables:
APP: review-${CI_MERGE_REQUEST_IID}
NAMESPACE: review
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://review-${CI_MERGE_REQUEST_IID}.example.com
on_stop: review:stop
rules:
- if: $CI_MERGE_REQUEST_IID
review:stop:
extends: .deploy_template
stage: deploy
script:
- kubectl delete namespace review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_MERGE_REQUEST_IID
action: stop
rules:
- if: $CI_MERGE_REQUEST_IID
when: manual
GitLab CI Key Features
| Feature | Syntax | Purpose |
|---|---|---|
| extends | extends: .template | DRY job config via inheritance |
| rules | rules: [{ if, when, ... }] | Conditional job inclusion |
| trigger | trigger: { include: ... } | Multi-project / downstream pipelines |
| needs | needs: [job] | DAG execution β run out of stage order |
| services | services: [postgres] | Sidecar containers for tests |
| artifacts:reports | reports: { cobertura, junit } | Native report ingestion |
| environment | environment: { name, url } | Deployment tracking & protection |
| cache | cache: { key, paths } | Dependency caching across pipelines |
4. Jenkins
Declarative Jenkinsfile
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent none
options {
buildDiscarder(logRotator(numToKeepStr: '30'))
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
timestamps()
}
environment {
REGISTRY = 'ghcr.io'
IMAGE = "${REGISTRY}/myorg/myapp"
DOCKER_CONFIG = credentials('docker-registry-auth')
}
triggers {
pollSCM('H/5 ')
upstream(upstreamProjects: 'infrastructure/deploy', threshold: hudson.model.Result.SUCCESS)
}
stages {
stage('Build') {
agent { label 'node20' }
steps {
sh 'npm ci && npm run build'
}
post {
always {
stash name: 'dist', includes: 'dist/**'
}
}
}
stage('Test') {
parallel {
stage('Unit') {
agent { label 'node20' }
steps {
unstash 'dist'
sh 'npx vitest run'
}
post {
always { junit 'reports/junit.xml' }
}
}
stage('Lint') {
agent { label 'node20' }
steps {
sh 'npm run lint'
}
}
}
}
stage('Security Scan') {
agent { label 'node20' }
steps {
sh 'npx audit-ci --moderate'
// Trivy FS scan
sh 'trivy fs --exit-code 1 --severity HIGH,CRITICAL .'
}
}
stage('Docker Build & Push') {
agent { label 'docker' }
steps {
unstash 'dist'
script {
def tag = env.GIT_COMMIT?.take(8) ?: env.BUILD_NUMBER
docker.withRegistry("https://${REGISTRY}", 'docker-registry-auth') {
def img = docker.build("${IMAGE}:${tag}", '--build-arg VERSION=${tag} .')
img.push()
if (env.GIT_BRANCH == 'main') {
img.push('latest')
}
}
}
}
}
stage('Deploy Staging') {
agent { label 'kubectl' }
steps {
sh "kubectl set image deployment/myapp myapp=${IMAGE}:${env.GIT_COMMIT.take(8)} -n staging"
sh 'kubectl rollout status deployment/myapp -n staging --timeout=120s'
}
}
stage('Deploy Production') {
agent { label 'kubectl' }
input {
message 'Deploy to production?'
ok 'Deploy'
}
steps {
sh "kubectl set image deployment/myapp myapp=${IMAGE}:${env.GIT_COMMIT.take(8)} -n production"
sh 'kubectl rollout status deployment/myapp -n production --timeout=120s'
}
}
}
post {
failure { slackSend color: 'danger', message: "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} failed!" }
success { slackSend color: 'good', message: "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded." }
}
}
Shared Libraries
// vars/deployK8s.groovy (shared library)
def call(Map config) {
sh """
kubectl set image deployment/${config.app} ${config.app}=${config.image} \
-n ${config.namespace}
kubectl rollout status deployment/${config.app} \
-n ${config.namespace} --timeout=${config.timeout ?: '120s'}
"""
}
// vars/dockerBuildPush.groovy
def call(Map config) {
docker.withRegistry("https://${config.registry}", config.credId) {
def img = docker.build("${config.image}:${config.tag}", config.context ?: '.')
img.push()
img.push('latest')
}
}
// Jenkinsfile using shared lib
@Library('my-pipeline-lib') _
pipeline {
agent any
stages {
stage('Deploy Staging') {
steps {
deployK8s app: 'myapp', image: 'ghcr.io/org/myapp:abc1234', namespace: 'staging'
}
}
}
}
Multibranch Pipeline
// Automatic branch discovery in Jenkins multibranch project
// Each branch with a Jenkinsfile gets its own pipeline
// PRs get pipelines via GitHub/Branch API integration
// Branch indexing triggers on push via webhook
Jenkins best practices:
Pin plugin versions; use plugin.yaml for reprovisioning.Use agent none at top level; label each stage.Stash/unstash instead of checking out multiple times.Use shared libraries for cross-team patterns.Keep Jenkinsfiles in source control (pipeline-as-code).
5. Deployment Strategies
Blue-Green Deployment
Two identical environments. Switch traffic atomically.
# Kubernetes blue-green via Service selector swap
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
slot: blue # β toggle this value to switch
ports:
- port: 80
targetPort: 8080
# Deploy new version to green
kubectl apply -f deployment-green.yaml
Verify green is healthy
kubectl wait --for=condition=Available deploy/myapp-green --timeout=120s
Run smoke tests against green
curl -sf http://green.internal/health
Swap Service selector to green
kubectl patch svc myapp -p '{"spec":{"selector":{"slot":"green"}}}'
Rollback: patch selector back to blue
Canary Deployment
Gradually shift traffic to the new version.
# Istio VirtualService β 10% canary
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts: [myapp.example.com]
http:
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
# Argo Rollouts Canary
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m }
- setWeight: 30
- pause: { duration: 5m }
- setWeight: 60
- pause: { duration: 5m }
- setWeight: 100
canaryService: myapp-canary
stableService: myapp-stable
Rolling Update (Kubernetes default)
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # pods above desired count during rollout
maxUnavailable: 0 # never go below desired count
A/B Testing (Feature Flags)
# LaunchDarkly / Unleash / custom feature-flag evaluation
if feature_flags.is_enabled("new-checkout", user_id, default=False):
return new_checkout(request)
return legacy_checkout(request)
# Unleash feature toggle config
apiVersion: unleash.example.com/v1
kind: FeatureToggle
metadata:
name: new-checkout
spec:
strategy: gradualRollout
parameters:
percentage: 25
groupId: checkout-experiment
Feature Flag Deployment Flow
βββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Deploy ββββΆβ Feature OFF ββββΆβ Monitor β
β (anytime) β β in prod β β no impact β
βββββββββββββ ββββββββ¬ββββββββ ββββββββββββββββ
β
βΌ Toggle ON
ββββββββββββββββ ββββββββββββββββ
β Feature ON ββββΆβ Rollback: β
β Gradual % β β Toggle OFF β
ββββββββββββββββ ββββββββββββββββ
Strategy Comparison
| Strategy | Downtime | Rollback Speed | Cost | Complexity |
|---|---|---|---|---|
| Blue-Green | Zero | Instant (selector swap) | 2Γ resources | Medium |
| Canary | Zero | Fast (shift traffic) | 1.1Γ resources | High |
| Rolling | Near-zero | Medium (re-rollout) | 1Γ resources | Low |
| Feature Flags | Zero | Instant (toggle off) | 1Γ resources | Medium |
6. Release Automation
Semantic Versioning (SemVer)
MAJOR.MINOR.PATCH
β β βββ bug fixes (backward compatible)
β βββββββ new features (backward compatible)
ββββββββββββ breaking changes
Automated Versioning with Conventional Commits
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for versioning
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx semantic-release
.releaserc.json (semantic-release)
{
"branches": ["main", { "name": "next", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
["@semantic-release/exec", {
"prepareCmd": "npm version ${nextRelease.version} --no-git-tag-version"
}],
["@semantic-release/git", {
"assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
"message": "chore(release): ${nextRelease.version}"
}],
"@semantic-release/github"
]
}
Changelog Generation
# git-cliff (Rust-based, fast)
git-cliff --output CHANGELOG.md
cliff.toml
[changelog]
header = """# Changelog\n\nAll notable changes to this project will be documented.\n"""
body = """
{% if version %}## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}## [Unreleased]
{% endif %}{% for commit in commits %}- {{ commit.message | trim_end_matches(pat="\\n") }}{% endfor %}"""
trim = true
GitHub Releases in CI
# .github/workflows/publish-release.yml
name: Publish Release
on:
push:
tags: ['v*']
jobs:
release:
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
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
dist/*.tar.gz
dist/*.sha256
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
7. Docker / OCI Image Building in CI
Multi-stage Dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package-lock.json package.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
Production stage
FROM gcr.io/distroless/nodejs20-debian12:latest
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER nonroot:nonroot
EXPOSE 3000
CMD ["dist/server.js"]
BuildKit + Cache in GitHub Actions
- uses: docker/setup-buildx-action@v3
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
provenance: true # SLSA provenance
sbom: true # SBOM attestation
Container Scanning
# Trivy β run in CI
name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '1'
severity: 'CRITICAL,HIGH'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
8. Testing in CI
Test Pyramid in Pipelines
β± E2E β² β slow, flaky, few
β± Integration β² β medium speed, moderate count
β± Unit Tests β² β fast, many, deterministic
Parallel Test Sharding
# Vitest / Jest shards in GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx vitest run --shard=${{ matrix.shard }}/4
Integration Tests with Services
# GitHub Actions β Postgres service
jobs:
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ['5432:5432']
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- run: npx vitest run --config vitest.integration.config.ts
Security Scanning in CI
| Tool | What it scans | CI Usage |
|---|---|---|
| CodeQL | SAST (C/C++, JS, Python, Javaβ¦) | github/codeql-action/analyze@v3 |
| Trivy | Container images, FS, IaC | aquasecurity/trivy-action@master |
| Snyk | Dependencies (SCA) | snyk/actions/test@master |
| TruffleHog | Secrets in git history | trufflesecurity/trufflehog@main |
| hadolint | Dockerfile lint | hadolint/hadolint-action@v3.1.0 |
| Checkov | IaC (Terraform, K8s, CloudFormation) | bridgecrew/checkov-action@master |
| OSV-scanner | Dependency vulnerabilities | google/osv-scanner-action@v1 |
# Secret scanning pre-commit
name: TruffleHog
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
9. Secret Management in Pipelines
Principles
Never hardcode secrets in YAML, scripts, or Dockerfiles.
Use OIDC instead of long-lived cloud credentials where possible.
Scope secrets to the narrowest environment (staging vs production).
Rotate secrets regularly; automate rotation.
Audit secret access logs.
| Platform | Mechanism | Notes |
|---|---|---|
| GitHub Actions | ${{ secrets.NAME }} | Org, repo, or environment level |
| GitHub Environments | Protection rules + required reviewers | Gate deployments |
| GitLab CI | $CI_VARIABLE (masked, protected) | Project or group level |
| Jenkins | credentials() binding | Vault plugin recommended |
| HashiCorp Vault | Dynamic secrets (TTL, auto-revoke) | hashicorp/vault-action@v3 |
Vault Integration in GitHub Actions
- name: Import Vault Secrets
uses: hashicorp/vault-action@v3
id: vault
with:
url: https://vault.example.com
role: myapp-ci
method: jwt
path: github
secrets: |
secret/data/myapp/db DATABASE_URL | DB_URL;
secret/data/myapp/api API_KEY | API_KEY;
run: |
echo "Connecting to ${{ steps.vault.outputs.DB_URL }}"
npm run migrate
GitHub Environment Protections
# Settings β Environments β production
- Required reviewers: @devops-lead
- Wait timer: 5 minutes
- Branch: main only
- Deployment branch rules: main, release/*
deploy:
runs-on: ubuntu-latest
environment: production # enforces the above rules
steps:
- run: deploy-to-prod.sh
10. Monitoring Pipelines
Key Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
| Build Duration | Time from trigger to completion | > 2Γ median |
| Flake Rate | % of passes that are intermittent | > 5% |
| Mean Time to Recovery | Time from failure to green | > 30 min |
| Deploy Frequency | Deploys per day/week | Trending down |
| Change Failure Rate | % of deploys causing incidents | > 15% |
| Pipeline Success Rate | % of runs that complete green | < 90% |
GitHub Actions Monitoring
# .github/workflows/pipeline-metrics.yml
name: Pipeline Metrics
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
metrics:
runs-on: ubuntu-latest
steps:
- run: |
echo "conclusion=${{ github.event.workflow_run.conclusion }}" >> "$GITHUB_STEP_SUMMARY"
echo "run_time=$(( $(date +%s) - $(date -d '${{ github.event.workflow_run.run_started_at }}' +%s) ))" >> "$GITHUB_STEP_SUMMARY"
curl -X POST https://metrics.example.com/api/pipeline \
-d "workflow=${{ github.event.workflow_run.name }}" \
-d "conclusion=${{ github.event.workflow_run.conclusion }}" \
-d "duration=${run_time}"
Datadog / Grafana Pipeline Integration
# Send CI metrics to Datadog
uses: datadog/cli-pipeline-metrics@v1
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
pipeline-id: ${{ github.run_id }}
Alerting on Failures
# Slack alert on pipeline failure
post:
failure:
runs-on: ubuntu-latest
steps:
- uses: slackapi/slack-github-action@v1.27.0
with:
payload: |
{
"text": "β Pipeline failed",
"blocks": [{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ github.workflow }} failed in run <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.run_number }}>"
}
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
dev ββautoβββΆ staging ββmanualβββΆ production
β β β
βΌ βΌ βΌ
unit+lint integration+smoke canary+monitor
βΌ βΌ βΌ
dev cluster staging cluster prod cluster
# ArgoCD Application β dev
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-dev
namespace: argocd
spec:
source:
repoURL: https://github.com/org/myapp-manifests
targetRevision: main
path: overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
# Promote: update image tag in staging overlay
automation/script or CI step:
run: |
kustomize edit set image myapp=ghcr.io/org/myapp:${{ github.sha }}
git commit -am "chore: promote ${{ github.sha }} to staging"
git push
Environment Matrix
# Deploy to multiple environments via matrix
jobs:
deploy:
strategy:
matrix:
include:
- environment: dev
namespace: dev
auto_deploy: true
- environment: staging
namespace: staging
auto_deploy: true
- environment: production
namespace: production
auto_deploy: false
runs-on: ubuntu-latest
environment: ${{ matrix.environment }}
if: matrix.auto_deploy || github.event.inputs.deploy_prod == 'true'
steps:
- run: kubectl set image deployment/myapp myapp=$IMAGE -n ${{ matrix.namespace }}
Flowcharts & Decision Diagrams
Pipeline Architecture Decision Tree
What CI/CD platform? βββββββββββββββββββββββββββββ
β β
ββ GitHub-hosted project? ββYesβββ GitHub Actions
β (native, marketplace, OIDC)
ββ GitLab repo? ββYesβββ GitLab CI
β (built-in, .gitlab-ci.yml)
ββ Enterprise/legacy? ββYesβββ Jenkins
β (flexible, shared libraries)
ββ Multi-cloud/hybrid βββ Tekton + ArgoCD
(K8s-native, GitOps)
Deployment Flow Decision Tree
Deploy to prod? ββ¬β Risk appetite: low βββ Blue-green (2 envs, instant rollback)
ββ Risk appetite: medium βββ Canary (5%β25%β100%, observe metrics)
ββ Risk appetite: high βββ Rolling (maxSurge=25%, gradual)
ββ Feature flag needed? ββYesβββ Dark launch β flag ON β full rollout
Pipeline Failure Debugging Flowchart
Pipeline failed? ββ¬β Build stage βββ Check dependency cache, Dockerfile, build logs
ββ Test stage βββ Check test report, flaky test history, service containers
ββ Security scan βββ Check vulnerability severity, allowlist, triage
ββ Deploy stage βββ Check credentials, namespace, health checks
ββ Integration test βββ Check service mesh, network policies, timeouts
Testing Strategy Enhancement for CI/CD
Test Pyramid in CI
β± E2E β² β 5% β Cypress, Playwright (expensive, fragile)
β±βββββββββ²
β± Integration β² β 15% β Service containers, API tests
β±βββββββββββββββββ²
β± Contract Tests β² β 15% β Pact, Spring Cloud Contract
β±βββββββββββββββββββββ²
β± Unit Tests β²β 65% β jest, pytest, go test (fast, deterministic)
β±βββββββββββββββββββββββββββ²
GitHub Actions Testing Matrix
# Comprehensive test pipeline with matrix
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
coverage: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ matrix.node }}" }
- run: npm ci
- run: npm test -- --coverage=${{ matrix.coverage || false }}
- uses: codecov/codecov-action@v4
if: matrix.coverage
with: { token: "${{ secrets.CODECOV_TOKEN }}" }
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with: { scan-type: "fs", severity: "CRITICAL,HIGH" }
- uses: github/codeql-action/analyze@v3
with: { languages: "javascript" }
e2e:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test, POSTGRES_DB: app_test }
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with: { name: "playwright-report", path: "playwright-report/" }
Caching: Cache dependency directories (node_modules, .m2, pip, Go module cache)
Parallelism: Run independent jobs in parallel; use matrix for multi-config
Incremental Builds: Use Turborepo/Nx monorepo caching for affected projects only
Docker Layer Caching: Multi-stage builds + BuildKit cache mount
Self-hosted Runners: Reduce queue time for large repos (6+ concurrent jobs)
Artifact Sharding: Split test suites across runners for faster feedback
Skip Unnecessary Work: paths filters to trigger only on relevant changes
12. Best Practices
Pipeline Design
Build once, promote everywhere β same artifact (Docker image, JAR) through all environments.
Pin all action/plugin versions β actions/checkout@eef61447b9 (full SHA) or at minimum @v4.
Fail fast β lint and unit tests before integration tests; timeout every job.
Concurrent groups β concurrency in GitHub Actions to cancel superseded runs.
Minimal permissions β permissions: contents: read by default; elevate only where needed.
Cache aggressively β dependency caches save 50β80% build time.
Artifact hygiene β set retention-days; don't accumulate forever.
Structured logs β JSON-formatted output for scraping and dashboards.
Security
OIDC over static secrets β ephemeral tokens via GitHub OIDC / GitLab JWT.
Pin base images β FROM node:20.11.0-alpine not FROM node:latest.
Scan continuously β SAST on every PR; SCA + container scan on every merge.
Sign artifacts β Cosign for container images; SLSA provenance.
Branch protection β require status checks before merge; no force push.
Least privilege β deploy tokens scoped to namespace; no cluster-admin.
Reliability
Retry flaky steps β retry in GitHub Actions; retry: 2 in GitLab.
Idempotent deployments β re-running a deploy job is safe.
Readiness probes β always wait for rollout/healthcheck before proceeding.
Rollback SOP β document and test rollback for every strategy.
Feature flags β decouple deploy from release.
13. Common Pitfalls
| Pitfall | Solution |
|---|---|
| Unpinned actions/tags | Pin to SHA: actions/checkout@b4ffde65 |
| Secrets in logs | Use masked variables; never echo secrets |
| Giant monolith pipeline | Split into reusable workflows / templates |
| No caching | Add cache for language managers & Docker layers |
| Flaky tests blocking deploys | Quarantine flaky tests; track flake rate |
| Deploying different artifacts per env | Build once, promote the same image |
| Manual deploy steps without audit | Use environment protections & required reviewers |
| Running all tests sequentially | Parallelize with matrix / shard |
| No timeout on jobs | Always set timeout-minutes |
| Stale branches triggering CI | Use concurrency groups; auto-delete merged branches |
| latest tag in production | Use SHA or semver tags; never floating tags |
| Over-fetching secrets | Scope secrets to minimal needed; use OIDC |
14. Quick Reference
GitHub Actions β Useful Snippets
# Conditional step
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: echo "Main branch push"
Reusable workflow output
outputs:
version: ${{ steps.meta.outputs.version }}
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [staging, production]
Matrix with include
strategy:
matrix:
include:
- os: ubuntu-latest
target: linux-x64
- os: macos-latest
target: darwin-arm64
GitLab CI β Useful Snippets
# DAG with needs
test:integration:
needs: [build]
stage: test # can run before other stage-level jobs
rules:
- if: $CI_COMMIT_TAG
Manual job with environment
deploy:prod:
when: manual
environment:
name: production
Inherited variables
variables:
APP_VERSION: $CI_COMMIT_TAG
Jenkins β Useful Snippets
// When conditions
when {
branch 'main'
expression { env.DEPLOY_ENV == 'production' }
}
// Parallel stages
stage('Test') {
parallel {
stage('Unit') { steps { sh 'npm test' } }
stage('Lint') { steps { sh 'npm run lint' } }
}
}
// Credentials binding
withCredentials([string(credentialsId: 'api-key', variable: 'KEY')]) {
sh "curl -H 'Authorization: Bearer ${KEY}' ..."
}
| Feature | GitHub Actions | GitLab CI | Jenkins |
|---|---|---|---|
| Config location | .github/workflows/*.yml | .gitlab-ci.yml | Jenkinsfile |
| Language | YAML | YAML | Groovy |
| Hosted runners | β
Native | β
Native (SaaS) | β Self-hosted |
| Self-hosted runners | β
| β
| β
Native |
| Reusability | Reusable workflows, composite actions | extends, include | Shared libraries |
| Environments | β
Protection rules, reviewers | β
Environments, approvals | β
Input steps |
| OIDC | β
AWS/GCP/Azure | β
JWT tokens | β
With plugins |
| Container registry | ghcr.io | registry.gitlab.com | Any (ECR, ACRβ¦) |
| Caching | actions/cache, GHA cache | Built-in cache: | Plugins |
| Secret scope | Org/Repo/Environment | Group/Project/Instance | Folder/Credentials |
| Cost model | Free for public, minutes for private | Free tier then compute | Infrastructure cost |
| Community | Marketplace (20k+ actions) | Templates | 1800+ plugins |
This skill covers CI/CD fundamentals through production patterns. Adapt examples to your stack, cloud provider, and team size. Always test pipeline changes in a branch before merging to main.