# GitHub: branch protection via API or repository settings
# Require -weight: 500;">status checks before merging:
required_status_checks: strict: true # require branch to be up to date contexts: - "ci/unit-tests" - "ci/lint" - "ci/security-scan" # Require pull request reviews:
required_pull_request_reviews: required_approving_review_count: 1 dismiss_stale_reviews: true # Enforce for admins too — no emergency bypasses:
enforce_admins: true
# GitHub: branch protection via API or repository settings
# Require -weight: 500;">status checks before merging:
required_status_checks: strict: true # require branch to be up to date contexts: - "ci/unit-tests" - "ci/lint" - "ci/security-scan" # Require pull request reviews:
required_pull_request_reviews: required_approving_review_count: 1 dismiss_stale_reviews: true # Enforce for admins too — no emergency bypasses:
enforce_admins: true
# GitHub: branch protection via API or repository settings
# Require -weight: 500;">status checks before merging:
required_status_checks: strict: true # require branch to be up to date contexts: - "ci/unit-tests" - "ci/lint" - "ci/security-scan" # Require pull request reviews:
required_pull_request_reviews: required_approving_review_count: 1 dismiss_stale_reviews: true # Enforce for admins too — no emergency bypasses:
enforce_admins: true
# GitLab: protected branch settings in .gitlab-ci.yml context
# Configure via Settings > Repository > Protected Branches:
# Push: No one (merge requests only)
# Merge: Maintainers
# Code owner approval: Required
# GitLab: protected branch settings in .gitlab-ci.yml context
# Configure via Settings > Repository > Protected Branches:
# Push: No one (merge requests only)
# Merge: Maintainers
# Code owner approval: Required
# GitLab: protected branch settings in .gitlab-ci.yml context
# Configure via Settings > Repository > Protected Branches:
# Push: No one (merge requests only)
# Merge: Maintainers
# Code owner approval: Required
# GitHub Actions: staged test execution
jobs: fast-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Lint run: -weight: 500;">npm run lint - name: Type check run: -weight: 500;">npm run type-check - name: Unit tests run: -weight: 500;">npm test -- --coverage --ci integration-tests: needs: fast-checks # only run if fast checks pass runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test steps: - uses: actions/checkout@v4 - name: Integration tests run: -weight: 500;">npm run test:integration e2e-tests: needs: integration-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: E2E tests run: npx playwright test --project=chromium
# GitHub Actions: staged test execution
jobs: fast-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Lint run: -weight: 500;">npm run lint - name: Type check run: -weight: 500;">npm run type-check - name: Unit tests run: -weight: 500;">npm test -- --coverage --ci integration-tests: needs: fast-checks # only run if fast checks pass runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test steps: - uses: actions/checkout@v4 - name: Integration tests run: -weight: 500;">npm run test:integration e2e-tests: needs: integration-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: E2E tests run: npx playwright test --project=chromium
# GitHub Actions: staged test execution
jobs: fast-checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Lint run: -weight: 500;">npm run lint - name: Type check run: -weight: 500;">npm run type-check - name: Unit tests run: -weight: 500;">npm test -- --coverage --ci integration-tests: needs: fast-checks # only run if fast checks pass runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test steps: - uses: actions/checkout@v4 - name: Integration tests run: -weight: 500;">npm run test:integration e2e-tests: needs: integration-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: E2E tests run: npx playwright test --project=chromium
# GitLab CI equivalent:
stages: - fast-checks - integration - e2e lint-and-unit: stage: fast-checks script: - -weight: 500;">npm run lint - -weight: 500;">npm test -- --ci --coverage integration: stage: integration needs: ["lint-and-unit"] services: - postgres:16 script: - -weight: 500;">npm run test:integration e2e: stage: e2e needs: ["integration"] script: - npx playwright test
# GitLab CI equivalent:
stages: - fast-checks - integration - e2e lint-and-unit: stage: fast-checks script: - -weight: 500;">npm run lint - -weight: 500;">npm test -- --ci --coverage integration: stage: integration needs: ["lint-and-unit"] services: - postgres:16 script: - -weight: 500;">npm run test:integration e2e: stage: e2e needs: ["integration"] script: - npx playwright test
# GitLab CI equivalent:
stages: - fast-checks - integration - e2e lint-and-unit: stage: fast-checks script: - -weight: 500;">npm run lint - -weight: 500;">npm test -- --ci --coverage integration: stage: integration needs: ["lint-and-unit"] services: - postgres:16 script: - -weight: 500;">npm run test:integration e2e: stage: e2e needs: ["integration"] script: - npx playwright test
# package.json or jest.config.js
coverageThreshold: global: branches: 70 functions: 80 lines: 80 statements: 80
# package.json or jest.config.js
coverageThreshold: global: branches: 70 functions: 80 lines: 80 statements: 80
# package.json or jest.config.js
coverageThreshold: global: branches: 70 functions: 80 lines: 80 statements: 80
# GitHub Actions: Terraform validation pipeline
jobs: terraform-validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: "~1.7" - name: Terraform format check run: terraform fmt -check -recursive working-directory: ./infrastructure - name: Terraform validate run: | terraform init -backend=false terraform validate working-directory: ./infrastructure - name: Terraform plan (PR only) if: github.event_name == 'pull_request' run: terraform plan -no-color working-directory: ./infrastructure env: TF_VAR_environment: staging - name: tfsec security scan uses: aquasecurity/tfsec-action@v1.0.0 with: working-directory: ./infrastructure
# GitHub Actions: Terraform validation pipeline
jobs: terraform-validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: "~1.7" - name: Terraform format check run: terraform fmt -check -recursive working-directory: ./infrastructure - name: Terraform validate run: | terraform init -backend=false terraform validate working-directory: ./infrastructure - name: Terraform plan (PR only) if: github.event_name == 'pull_request' run: terraform plan -no-color working-directory: ./infrastructure env: TF_VAR_environment: staging - name: tfsec security scan uses: aquasecurity/tfsec-action@v1.0.0 with: working-directory: ./infrastructure
# GitHub Actions: Terraform validation pipeline
jobs: terraform-validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: "~1.7" - name: Terraform format check run: terraform fmt -check -recursive working-directory: ./infrastructure - name: Terraform validate run: | terraform init -backend=false terraform validate working-directory: ./infrastructure - name: Terraform plan (PR only) if: github.event_name == 'pull_request' run: terraform plan -no-color working-directory: ./infrastructure env: TF_VAR_environment: staging - name: tfsec security scan uses: aquasecurity/tfsec-action@v1.0.0 with: working-directory: ./infrastructure
# Run terraform plan in "detect drift" mode (no changes allowed)
terraform plan -detailed-exitcode
# Exit code 2 means drift detected — alert the team
# Run terraform plan in "detect drift" mode (no changes allowed)
terraform plan -detailed-exitcode
# Exit code 2 means drift detected — alert the team
# Run terraform plan in "detect drift" mode (no changes allowed)
terraform plan -detailed-exitcode
# Exit code 2 means drift detected — alert the team
# GitHub Actions: comprehensive security scanning stage
jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Dependency vulnerability scanning - name: Dependency audit run: -weight: 500;">npm audit --audit-level=high # SAST: static code analysis - name: CodeQL analysis uses: github/codeql-action/analyze@v3 with: languages: javascript # Secret scanning (prevent secrets from being committed) - name: Gitleaks secret scan uses: gitleaks/gitleaks-action@v2 # Container image scanning - name: Build and scan container run: | -weight: 500;">docker build -t app:${{ github.sha }} . -weight: 500;">docker run --rm \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ aquasec/trivy:latest image \ --exit-code 1 \ --severity CRITICAL \ app:${{ github.sha }}
# GitHub Actions: comprehensive security scanning stage
jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Dependency vulnerability scanning - name: Dependency audit run: -weight: 500;">npm audit --audit-level=high # SAST: static code analysis - name: CodeQL analysis uses: github/codeql-action/analyze@v3 with: languages: javascript # Secret scanning (prevent secrets from being committed) - name: Gitleaks secret scan uses: gitleaks/gitleaks-action@v2 # Container image scanning - name: Build and scan container run: | -weight: 500;">docker build -t app:${{ github.sha }} . -weight: 500;">docker run --rm \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ aquasec/trivy:latest image \ --exit-code 1 \ --severity CRITICAL \ app:${{ github.sha }}
# GitHub Actions: comprehensive security scanning stage
jobs: security: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Dependency vulnerability scanning - name: Dependency audit run: -weight: 500;">npm audit --audit-level=high # SAST: static code analysis - name: CodeQL analysis uses: github/codeql-action/analyze@v3 with: languages: javascript # Secret scanning (prevent secrets from being committed) - name: Gitleaks secret scan uses: gitleaks/gitleaks-action@v2 # Container image scanning - name: Build and scan container run: | -weight: 500;">docker build -t app:${{ github.sha }} . -weight: 500;">docker run --rm \ -v /var/run/-weight: 500;">docker.sock:/var/run/-weight: 500;">docker.sock \ aquasec/trivy:latest image \ --exit-code 1 \ --severity CRITICAL \ app:${{ github.sha }}
# GitLab CI: blue-green with AWS ALB
deploy-green: stage: deploy script: - aws ecs -weight: 500;">update--weight: 500;">service --cluster prod ---weight: 500;">service app-green \ --task-definition app:$CI_PIPELINE_IID - aws ecs wait services-stable --cluster prod --services app-green - # Run smoke tests against green target group - ./scripts/smoke-test.sh $GREEN_URL - # Shift 100% traffic to green - aws elbv2 modify-rule --rule-arn $ALB_RULE_ARN \ --actions Type=forward,TargetGroupArn=$GREEN_TG_ARN only: - main
# GitLab CI: blue-green with AWS ALB
deploy-green: stage: deploy script: - aws ecs -weight: 500;">update--weight: 500;">service --cluster prod ---weight: 500;">service app-green \ --task-definition app:$CI_PIPELINE_IID - aws ecs wait services-stable --cluster prod --services app-green - # Run smoke tests against green target group - ./scripts/smoke-test.sh $GREEN_URL - # Shift 100% traffic to green - aws elbv2 modify-rule --rule-arn $ALB_RULE_ARN \ --actions Type=forward,TargetGroupArn=$GREEN_TG_ARN only: - main
# GitLab CI: blue-green with AWS ALB
deploy-green: stage: deploy script: - aws ecs -weight: 500;">update--weight: 500;">service --cluster prod ---weight: 500;">service app-green \ --task-definition app:$CI_PIPELINE_IID - aws ecs wait services-stable --cluster prod --services app-green - # Run smoke tests against green target group - ./scripts/smoke-test.sh $GREEN_URL - # Shift 100% traffic to green - aws elbv2 modify-rule --rule-arn $ALB_RULE_ARN \ --actions Type=forward,TargetGroupArn=$GREEN_TG_ARN only: - main
# Canary: shift 5% traffic, monitor for 10 minutes, then full rollout
deploy-canary: stage: canary script: - ./scripts/deploy-canary.sh --weight 5 - sleep 600 # 10 minute observation window - ./scripts/check-error-rate.sh --threshold 0.5 # fail if >0.5% errors - ./scripts/deploy-canary.sh --weight 100
# Canary: shift 5% traffic, monitor for 10 minutes, then full rollout
deploy-canary: stage: canary script: - ./scripts/deploy-canary.sh --weight 5 - sleep 600 # 10 minute observation window - ./scripts/check-error-rate.sh --threshold 0.5 # fail if >0.5% errors - ./scripts/deploy-canary.sh --weight 100
# Canary: shift 5% traffic, monitor for 10 minutes, then full rollout
deploy-canary: stage: canary script: - ./scripts/deploy-canary.sh --weight 5 - sleep 600 # 10 minute observation window - ./scripts/check-error-rate.sh --threshold 0.5 # fail if >0.5% errors - ./scripts/deploy-canary.sh --weight 100
# GitHub Actions: parallel test shards
strategy: matrix: shard: [1, 2, 3, 4] # 4 parallel runners
steps: - name: Run test shard run: npx jest --shard=${{ matrix.shard }}/4
# GitHub Actions: parallel test shards
strategy: matrix: shard: [1, 2, 3, 4] # 4 parallel runners
steps: - name: Run test shard run: npx jest --shard=${{ matrix.shard }}/4
# GitHub Actions: parallel test shards
strategy: matrix: shard: [1, 2, 3, 4] # 4 parallel runners
steps: - name: Run test shard run: npx jest --shard=${{ matrix.shard }}/4
# GitHub Actions: intelligent -weight: 500;">npm cache
- name: Cache node modules uses: actions/cache@v4 with: path: ~/.-weight: 500;">npm key: ${{ runner.os }}--weight: 500;">npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}--weight: 500;">npm- # GitLab CI:
cache: key: files: - package-lock.json paths: - node_modules/
# GitHub Actions: intelligent -weight: 500;">npm cache
- name: Cache node modules uses: actions/cache@v4 with: path: ~/.-weight: 500;">npm key: ${{ runner.os }}--weight: 500;">npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}--weight: 500;">npm- # GitLab CI:
cache: key: files: - package-lock.json paths: - node_modules/
# GitHub Actions: intelligent -weight: 500;">npm cache
- name: Cache node modules uses: actions/cache@v4 with: path: ~/.-weight: 500;">npm key: ${{ runner.os }}--weight: 500;">npm-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}--weight: 500;">npm- # GitLab CI:
cache: key: files: - package-lock.json paths: - node_modules/
# Good: dependency layer (changes rarely) before app code layer (changes often)
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # this layer is cached unless package.json changes
COPY src/ ./src/ # this layer rebuilds on every code change
CMD ["node", "src/index.js"]
# Good: dependency layer (changes rarely) before app code layer (changes often)
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # this layer is cached unless package.json changes
COPY src/ ./src/ # this layer rebuilds on every code change
CMD ["node", "src/index.js"]
# Good: dependency layer (changes rarely) before app code layer (changes often)
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci --only=production # this layer is cached unless package.json changes
COPY src/ ./src/ # this layer rebuilds on every code change
CMD ["node", "src/index.js"]
# GitHub Actions: path filtering
on: push: paths-ignore: - '**.md' - 'docs/**'
# GitHub Actions: path filtering
on: push: paths-ignore: - '**.md' - 'docs/**'
# GitHub Actions: path filtering
on: push: paths-ignore: - '**.md' - 'docs/**'
# ArgoCD Application manifest — the pipeline updates this repo,
# ArgoCD deploys it
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: name: api--weight: 500;">service namespace: argocd
spec: project: production source: repoURL: https://github.com/your-org/k8s-manifests targetRevision: main path: apps/api--weight: 500;">service/production destination: server: https://kubernetes.default.svc namespace: api--weight: 500;">service syncPolicy: automated: prune: true selfHeal: true # re-apply if someone manually changes cluster state syncOptions: - CreateNamespace=true
# ArgoCD Application manifest — the pipeline updates this repo,
# ArgoCD deploys it
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: name: api--weight: 500;">service namespace: argocd
spec: project: production source: repoURL: https://github.com/your-org/k8s-manifests targetRevision: main path: apps/api--weight: 500;">service/production destination: server: https://kubernetes.default.svc namespace: api--weight: 500;">service syncPolicy: automated: prune: true selfHeal: true # re-apply if someone manually changes cluster state syncOptions: - CreateNamespace=true
# ArgoCD Application manifest — the pipeline updates this repo,
# ArgoCD deploys it
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: name: api--weight: 500;">service namespace: argocd
spec: project: production source: repoURL: https://github.com/your-org/k8s-manifests targetRevision: main path: apps/api--weight: 500;">service/production destination: server: https://kubernetes.default.svc namespace: api--weight: 500;">service syncPolicy: automated: prune: true selfHeal: true # re-apply if someone manually changes cluster state syncOptions: - CreateNamespace=true
# GitHub Actions: annotate deployment in Datadog
- name: Send deployment event to Datadog run: | -weight: 500;">curl -X POST "https://api.datadoghq.com/api/v1/events" \ -H "Content-Type: application/json" \ -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" \ -d '{ "title": "Deployment: api--weight: 500;">service '${{ github.sha }}'", "text": "Deployed by ${{ github.actor }}", "tags": ["-weight: 500;">service:api--weight: 500;">service", "env:production", "source:ci"], "alert_type": "info" }'
# GitHub Actions: annotate deployment in Datadog
- name: Send deployment event to Datadog run: | -weight: 500;">curl -X POST "https://api.datadoghq.com/api/v1/events" \ -H "Content-Type: application/json" \ -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" \ -d '{ "title": "Deployment: api--weight: 500;">service '${{ github.sha }}'", "text": "Deployed by ${{ github.actor }}", "tags": ["-weight: 500;">service:api--weight: 500;">service", "env:production", "source:ci"], "alert_type": "info" }'
# GitHub Actions: annotate deployment in Datadog
- name: Send deployment event to Datadog run: | -weight: 500;">curl -X POST "https://api.datadoghq.com/api/v1/events" \ -H "Content-Type: application/json" \ -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" \ -d '{ "title": "Deployment: api--weight: 500;">service '${{ github.sha }}'", "text": "Deployed by ${{ github.actor }}", "tags": ["-weight: 500;">service:api--weight: 500;">service", "env:production", "source:ci"], "alert_type": "info" }'
# Deployment script: record the current version before deploying
PREVIOUS_VERSION=$(-weight: 500;">kubectl get deployment api--weight: 500;">service -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "PREVIOUS_VERSION=$PREVIOUS_VERSION" >> $GITHUB_ENV # Automated rollback triggered by error rate threshold
if ./scripts/check-health.sh --timeout 300 --error-threshold 1; then echo "Deploy successful"
else echo "Health check failed — rolling back" -weight: 500;">kubectl set image deployment/api--weight: 500;">service api=$PREVIOUS_VERSION exit 1
fi
# Deployment script: record the current version before deploying
PREVIOUS_VERSION=$(-weight: 500;">kubectl get deployment api--weight: 500;">service -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "PREVIOUS_VERSION=$PREVIOUS_VERSION" >> $GITHUB_ENV # Automated rollback triggered by error rate threshold
if ./scripts/check-health.sh --timeout 300 --error-threshold 1; then echo "Deploy successful"
else echo "Health check failed — rolling back" -weight: 500;">kubectl set image deployment/api--weight: 500;">service api=$PREVIOUS_VERSION exit 1
fi
# Deployment script: record the current version before deploying
PREVIOUS_VERSION=$(-weight: 500;">kubectl get deployment api--weight: 500;">service -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "PREVIOUS_VERSION=$PREVIOUS_VERSION" >> $GITHUB_ENV # Automated rollback triggered by error rate threshold
if ./scripts/check-health.sh --timeout 300 --error-threshold 1; then echo "Deploy successful"
else echo "Health check failed — rolling back" -weight: 500;">kubectl set image deployment/api--weight: 500;">service api=$PREVIOUS_VERSION exit 1
fi - Deployment frequency: How often you can reliably release
- Lead time for changes: Time from code commit to production
- Change failure rate: Percentage of deployments causing incidents
- Mean time to recovery (MTTR): How fast you resolve incidents - Unit tests — fast (milliseconds each), isolated, run on every commit
- Integration tests — test component boundaries, run on every PR
- E2E tests — validate critical paths only, run pre-deploy - [ ] Enable branch protection: require PR reviews and -weight: 500;">status checks
- [ ] Add linting and static analysis to CI (catches the fastest category of bugs)
- [ ] Run unit tests on every commit
- [ ] Add secret scanning (this is cheap to implement and the risk of not having it is severe) - [ ] Add integration tests with test environment services
- [ ] Implement dependency caching
- [ ] Add dependency vulnerability scanning
- [ ] Implement automated deployment to staging on merge to main - [ ] Implement canary releases or blue-green deployment
- [ ] Add container security scanning
- [ ] Set up deployment event tracking in your observability stack
- [ ] Implement GitOps if on Kubernetes
- [ ] Build DORA metrics dashboard - Impact on DORA metrics: Does this directly improve deployment frequency, lead time, failure rate, or MTTR?
- Implementation complexity: How long does it take to implement and maintain? - Mean pipeline duration (trend: should be flat or decreasing)
- Pipeline success rate (trend: should be increasing)
- Flaky test rate (trend: should be decreasing toward zero)
- Time spent waiting for review (identifies bottlenecks in the human parts of the pipeline)