--- name: cicd-pipelines description: CI/CD pipeline patterns for Jenkins, GitHub Actions, and GitLab CI. Use when setting up continuous integration or deployment pipelines. --- # CI/CD Pipelines Skill ## Jenkins ### Declarative Pipeline ```groovy // Jenkinsfile pipeline { agent any environment { REGISTRY = 'myregistry.azurecr.io' IMAGE_NAME = 'myapp' COVERAGE_THRESHOLD = '80' } options { timeout(time: 30, unit: 'MINUTES') disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '10')) } stages { stage('Checkout') { steps { checkout scm } } stage('Install Dependencies') { parallel { stage('Python') { when { changeset "apps/backend/**" } steps { sh 'uv sync' } } stage('Node') { when { changeset "apps/frontend/**" } steps { sh 'npm ci' } } } } stage('Lint & Type Check') { parallel { stage('Python Lint') { when { changeset "apps/backend/**" } steps { sh 'uv run ruff check apps/backend/' sh 'uv run mypy apps/backend/' } } stage('TypeScript Lint') { when { changeset "apps/frontend/**" } steps { sh 'npm run lint --workspace=frontend' sh 'npm run typecheck --workspace=frontend' } } } } stage('Test') { parallel { stage('Backend Tests') { when { changeset "apps/backend/**" } steps { sh """ uv run pytest apps/backend/ \ --cov=apps/backend/src \ --cov-report=xml \ --cov-fail-under=${COVERAGE_THRESHOLD} \ --junitxml=test-results/backend.xml """ } post { always { junit 'test-results/backend.xml' publishCoverage adapters: [coberturaAdapter('coverage.xml')] } } } stage('Frontend Tests') { when { changeset "apps/frontend/**" } steps { sh """ npm run test --workspace=frontend -- \ --coverage \ --coverageThreshold='{"global":{"branches":${COVERAGE_THRESHOLD},"functions":${COVERAGE_THRESHOLD},"lines":${COVERAGE_THRESHOLD}}}' \ --reporter=junit \ --outputFile=test-results/frontend.xml """ } post { always { junit 'test-results/frontend.xml' } } } } } stage('Security Scan') { steps { sh 'trivy fs --severity HIGH,CRITICAL --exit-code 1 .' } } stage('Build') { when { anyOf { branch 'main' branch 'release/*' } } steps { script { def version = sh(script: 'git describe --tags --always', returnStdout: true).trim() sh """ docker build -t ${REGISTRY}/${IMAGE_NAME}:${version} . docker tag ${REGISTRY}/${IMAGE_NAME}:${version} ${REGISTRY}/${IMAGE_NAME}:latest """ } } } stage('Push') { when { branch 'main' } steps { withCredentials([usernamePassword( credentialsId: 'registry-credentials', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASS' )]) { sh """ echo \$REGISTRY_PASS | docker login ${REGISTRY} -u \$REGISTRY_USER --password-stdin docker push ${REGISTRY}/${IMAGE_NAME}:${version} docker push ${REGISTRY}/${IMAGE_NAME}:latest """ } } } stage('Deploy to Staging') { when { branch 'main' } steps { sh 'kubectl apply -f k8s/staging/' sh 'kubectl rollout status deployment/myapp -n staging' } } stage('Deploy to Production') { when { branch 'release/*' } input { message "Deploy to production?" ok "Deploy" } steps { sh 'kubectl apply -f k8s/production/' sh 'kubectl rollout status deployment/myapp -n production' } } } post { always { cleanWs() } success { slackSend( channel: '#deployments', color: 'good', message: "Build ${env.BUILD_NUMBER} succeeded: ${env.BUILD_URL}" ) } failure { slackSend( channel: '#deployments', color: 'danger', message: "Build ${env.BUILD_NUMBER} failed: ${env.BUILD_URL}" ) } } } ``` ### Shared Library ```groovy // vars/pythonPipeline.groovy def call(Map config = [:]) { pipeline { agent any stages { stage('Test') { steps { sh "uv run pytest ${config.testPath ?: 'tests/'} --cov --cov-fail-under=${config.coverage ?: 80}" } } stage('Lint') { steps { sh "uv run ruff check ${config.srcPath ?: 'src/'}" } } } } } // Usage in Jenkinsfile @Library('my-shared-library') _ pythonPipeline( testPath: 'apps/backend/tests/', srcPath: 'apps/backend/src/', coverage: 85 ) ``` ## GitHub Actions ### Complete Workflow ```yaml # .github/workflows/ci.yml name: CI/CD on: push: branches: [main, 'release/*'] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: detect-changes: runs-on: ubuntu-latest outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} infrastructure: ${{ steps.changes.outputs.infrastructure }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 id: changes with: filters: | backend: - 'apps/backend/**' - 'packages/shared/**' frontend: - 'apps/frontend/**' - 'packages/shared/**' infrastructure: - 'infrastructure/**' backend-test: needs: detect-changes if: needs.detect-changes.outputs.backend == 'true' runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install dependencies run: uv sync - name: Lint run: | uv run ruff check apps/backend/ uv run mypy apps/backend/ - name: Test env: DATABASE_URL: postgresql://test:test@localhost:5432/test run: | uv run pytest apps/backend/ \ --cov=apps/backend/src \ --cov-report=xml \ --cov-fail-under=80 - name: Upload coverage uses: codecov/codecov-action@v4 with: files: coverage.xml flags: backend frontend-test: needs: detect-changes if: needs.detect-changes.outputs.frontend == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - name: Install dependencies run: npm ci - name: Lint & Type Check run: | npm run lint --workspace=frontend npm run typecheck --workspace=frontend - name: Test run: npm run test --workspace=frontend -- --coverage - name: Upload coverage uses: codecov/codecov-action@v4 with: files: apps/frontend/coverage/lcov.info flags: frontend security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' severity: 'CRITICAL,HIGH' exit-code: '1' - name: Run Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-and-push: needs: [backend-test, frontend-test, security-scan] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - name: Log in to Container registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=sha type=ref,event=branch - name: Build and push 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: build-and-push runs-on: ubuntu-latest environment: staging steps: - uses: actions/checkout@v4 - name: Deploy to staging run: | kubectl apply -f k8s/staging/ kubectl rollout status deployment/myapp -n staging deploy-production: needs: deploy-staging runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - name: Deploy to production run: | kubectl apply -f k8s/production/ kubectl rollout status deployment/myapp -n production ``` ### Reusable Workflow ```yaml # .github/workflows/python-ci.yml name: Python CI on: workflow_call: inputs: python-version: required: false type: string default: '3.12' working-directory: required: true type: string coverage-threshold: required: false type: number default: 80 jobs: test: runs-on: ubuntu-latest defaults: run: working-directory: ${{ inputs.working-directory }} steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v4 - run: uv sync - run: uv run ruff check . - run: uv run pytest --cov --cov-fail-under=${{ inputs.coverage-threshold }} ``` ## GitLab CI ```yaml # .gitlab-ci.yml stages: - test - build - deploy variables: REGISTRY: registry.gitlab.com IMAGE_NAME: $CI_PROJECT_PATH .python-base: image: python:3.12 before_script: - pip install uv - uv sync .node-base: image: node:22 before_script: - npm ci test:backend: extends: .python-base stage: test script: - uv run ruff check apps/backend/ - uv run pytest apps/backend/ --cov --cov-fail-under=80 rules: - changes: - apps/backend/** test:frontend: extends: .node-base stage: test script: - npm run lint --workspace=frontend - npm run test --workspace=frontend -- --coverage rules: - changes: - apps/frontend/** build: stage: build image: docker:24 services: - docker:24-dind script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA . - docker push $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA rules: - if: $CI_COMMIT_BRANCH == "main" deploy:staging: stage: deploy script: - kubectl apply -f k8s/staging/ environment: name: staging rules: - if: $CI_COMMIT_BRANCH == "main" deploy:production: stage: deploy script: - kubectl apply -f k8s/production/ environment: name: production rules: - if: $CI_COMMIT_BRANCH == "main" when: manual ``` ## Best Practices ### Pipeline Design Principles 1. **Fail Fast** - Run quick checks (lint, type check) before slow ones (tests) 2. **Parallelize** - Run independent jobs concurrently 3. **Cache** - Cache dependencies between runs 4. **Change Detection** - Only run what's affected 5. **Immutable Artifacts** - Tag images with commit SHA 6. **Environment Parity** - Same process for all environments 7. **Secrets Management** - Never hardcode, use CI/CD secrets ### Quality Gates ```yaml # Minimum checks before merge - Lint passes - Type check passes - Unit tests pass - Coverage threshold met (80%+) - Security scan passes - No secrets detected ``` ### Deployment Strategies ```yaml # Rolling update (default) strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 # Blue-green (via service switch) # Deploy new version alongside old # Switch service selector when ready # Canary (gradual rollout) # Route percentage of traffic to new version # Monitor metrics before full rollout ```