🔧 DevOps
CI/CD Pipelines
Last updated: 2025-09-25 02:29:54
Continuous Integration/Continuous Deployment
CI/CD automates the process of integrating code changes and deploying applications.
GitHub Actions Workflow
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
release:
types: [published]
env:
NODE_VERSION: '18'
DOCKER_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
- name: Run security audit
run: npm audit --audit-level moderate
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'release'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_ENV=production
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
deploy-staging:
needs: [test, build]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy to staging
uses: azure/k8s-deploy@v1
with:
manifests: |
k8s/staging/deployment.yaml
k8s/staging/service.yaml
k8s/staging/ingress.yaml
images: ${{ needs.build.outputs.image-tag }}
kubeconfig: ${{ secrets.KUBE_CONFIG_STAGING }}
- name: Run integration tests
run: |
npm ci
npm run test:integration
env:
TEST_URL: https://staging.myapp.com
API_KEY: ${{ secrets.STAGING_API_KEY }}
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
if: always()
deploy-production:
needs: [test, build]
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: production
steps:
- name: Deploy to production
uses: azure/k8s-deploy@v1
with:
manifests: |
k8s/production/deployment.yaml
k8s/production/service.yaml
k8s/production/ingress.yaml
images: ${{ needs.build.outputs.image-tag }}
kubeconfig: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
- name: Update deployment status
uses: bobheadxi/deployments@v1
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
env: production
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
- name: Create deployment
id: deployment
uses: bobheadxi/deployments@v1
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: production
GitLab CI/CD Pipeline
# .gitlab-ci.yml
stages:
- test
- build
- security
- deploy-staging
- deploy-production
variables:
NODE_VERSION: '18'
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
cache:
paths:
- node_modules/
key:
files:
- package-lock.json
# Test stage
test:unit:
stage: test
image: node:$NODE_VERSION-alpine
services:
- postgres:13-alpine
- redis:6-alpine
variables:
DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
REDIS_URL: redis://redis:6379
before_script:
- npm ci
script:
- npm run lint
- npm run test:unit
- npm run test:integration
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
junit: junit.xml
paths:
- coverage/
expire_in: 1 week
only:
- merge_requests
- main
- develop
test:e2e:
stage: test
image: cypress/included:12.0.0
services:
- name: postgres:13-alpine
- name: redis:6-alpine
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
alias: app
variables:
CYPRESS_baseUrl: http://app:3000
DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
before_script:
- npm ci
script:
- npx cypress run --record --key $CYPRESS_RECORD_KEY
artifacts:
when: always
paths:
- cypress/videos/
- cypress/screenshots/
expire_in: 1 week
only:
- merge_requests
- main
# Build stage
build:docker:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- docker build
--build-arg NODE_ENV=production
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
--build-arg VCS_REF=$CI_COMMIT_SHA
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--tag $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
- tags
# Security scanning
security:dependency-scan:
stage: security
image: node:$NODE_VERSION-alpine
before_script:
- npm ci
script:
- npm audit --audit-level moderate
- npx snyk test --severity-threshold=high
allow_failure: true
only:
- merge_requests
- main
security:container-scan:
stage: security
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
-v $PWD:/tmp/.cache/ aquasec/trivy:latest
image --exit-code 0 --no-progress --format table
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
dependencies:
- build:docker
only:
- main
- develop
# Deployment stages
deploy:staging:
stage: deploy-staging
image: bitnami/kubectl:latest
environment:
name: staging
url: https://staging.myapp.com
before_script:
- echo $KUBE_CONFIG_STAGING | base64 -d > kubeconfig
- export KUBECONFIG=kubeconfig
script:
- kubectl set image deployment/myapp-deployment
myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--namespace=staging
- kubectl rollout status deployment/myapp-deployment --namespace=staging
- kubectl get services --namespace=staging
after_script:
- rm -f kubeconfig
dependencies:
- build:docker
only:
- develop
when: manual
deploy:production:
stage: deploy-production
image: bitnami/kubectl:latest
environment:
name: production
url: https://myapp.com
before_script:
- echo $KUBE_CONFIG_PRODUCTION | base64 -d > kubeconfig
- export KUBECONFIG=kubeconfig
script:
- kubectl set image deployment/myapp-deployment
myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--namespace=production
- kubectl rollout status deployment/myapp-deployment --namespace=production
- kubectl get services --namespace=production
after_script:
- rm -f kubeconfig
# Send notification
- 'curl -X POST -H "Content-type: application/json"
--data "{\"text\":\"🚀 Deployed $CI_PROJECT_NAME to production: $CI_COMMIT_MESSAGE\"}"
$SLACK_WEBHOOK_URL'
dependencies:
- build:docker
only:
- tags
when: manual
Jenkins Pipeline
// Jenkinsfile
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins: agent
spec:
containers:
- name: node
image: node:18-alpine
command:
- cat
tty: true
- name: docker
image: docker:20.10.16
command:
- cat
tty: true
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
"""
}
}
environment {
DOCKER_REGISTRY = 'your-registry.com'
IMAGE_NAME = 'myapp'
KUBECONFIG = credentials('kubeconfig')
SLACK_WEBHOOK = credentials('slack-webhook')
SONARQUBE_TOKEN = credentials('sonarqube-token')
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 45, unit: 'MINUTES')
timestamps()
}
triggers {
githubPush()
cron(env.BRANCH_NAME == 'main' ? '0 2 * * *' : '') // Nightly builds on main
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.BUILD_VERSION = "${env.BRANCH_NAME}-${env.GIT_COMMIT_SHORT}-${env.BUILD_NUMBER}"
}
}
}
stage('Install Dependencies') {
steps {
container('node') {
sh 'npm ci'
}
}
}
stage('Code Quality') {
parallel {
stage('Lint') {
steps {
container('node') {
sh 'npm run lint'
}
}
}
stage('Test') {
steps {
container('node') {
sh 'npm run test:coverage'
}
publishTestResults testResultsPattern: 'junit.xml'
publishCoverageGoberturaReport 'coverage/cobertura-coverage.xml'
}
}
stage('Security Scan') {
steps {
container('node') {
sh 'npm audit --audit-level moderate'
sh 'npx snyk test --severity-threshold=high || true'
}
}
}
}
}
stage('SonarQube Analysis') {
when {
anyOf {
branch 'main'
changeRequest()
}
}
steps {
container('node') {
withSonarQubeEnv('SonarQube') {
sh """
npx sonar-scanner \
-Dsonar.projectKey=myapp \
-Dsonar.sources=src \
-Dsonar.tests=src \
-Dsonar.test.inclusions='**/*.test.js' \
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \
-Dsonar.testExecutionReportPaths=test-report.xml
"""
}
}
}
}
stage('Build Docker Image') {
when {
anyOf {
branch 'main'
branch 'develop'
buildingTag()
}
}
steps {
container('docker') {
script {
def image = docker.build(
"${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION}",
"--build-arg NODE_ENV=production ."
)
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
image.push()
image.push('latest')
}
}
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
container('kubectl') {
sh """
kubectl set image deployment/myapp-deployment \
myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION} \
--namespace=staging
kubectl rollout status deployment/myapp-deployment --namespace=staging --timeout=300s
"""
}
}
}
stage('Integration Tests') {
when {
branch 'develop'
}
steps {
container('node') {
sh 'npm run test:integration'
}
}
}
stage('Deploy to Production') {
when {
buildingTag()
}
steps {
input message: 'Deploy to production?', ok: 'Deploy'
container('kubectl') {
sh """
kubectl set image deployment/myapp-deployment \
myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_VERSION} \
--namespace=production
kubectl rollout status deployment/myapp-deployment --namespace=production --timeout=600s
"""
}
}
}
}
post {
always {
cleanWs()
}
success {
script {
if (env.BRANCH_NAME == 'main' || env.TAG_NAME) {
sh """
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"✅ Successfully deployed ${IMAGE_NAME}:${BUILD_VERSION}"}' \
${SLACK_WEBHOOK}
"""
}
}
}
failure {
sh """
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"❌ Build failed for ${IMAGE_NAME}:${BUILD_VERSION} - ${BUILD_URL}"}' \
${SLACK_WEBHOOK}
"""
}
}
}