Dev-QA-Stage-Prod-Pipelines

Aditya PatilAditya Patil
9 min read

Dev → QA → Stage → Prod → DR
using a single artifact repository (ECR/JFrog/Nexus) with tagging at each stage, and no rebuilding after Dev.


Flow Overview

Branches:
- dev        → Development deployments
- qa         → QA deployments
- release/*  → Stage & Prod deployments
- main       → Production

Artifact/Image Tags:
- dev-<build_number>      → built from dev branch
- qa-<build_number>       → promoted from dev
- stage-<build_number>    → promoted from qa
- prod-vX.Y.Z             → promoted from stage

1️⃣ Dev Pipeline (Build & Unit Tests)

Here’s a Dev pipeline with:

  • Linting

  • GitLeaks scan

  • SonarQube Quality Gate

  • Trivy Docker image scan

  • Deployment to Dev namespace in Kubernetes

  • Slack notifications:

    • On failure → to committer and #devchannel

    • On success → to #devchannel (build & deploy complete) and #qachannel (ready for testing)

Trigger: Code push to dev branch
Goal: Build image → run unit tests → push dev tag to registry → deploy to Dev namespace.

pipeline {
    agent any

    environment {
        SONARQUBE_SERVER = 'SonarQubeServer'
        DOCKER_IMAGE = "myapp:${env.BUILD_NUMBER}"
        K8S_NAMESPACE = "dev"
        SLACK_DEV_CHANNEL = "#devchannel"
        SLACK_QA_CHANNEL = "#qachannel"
        COMMITTER = sh(script: "git log -1 --pretty=format:'%ae'", returnStdout: true).trim()
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Linting') {
            steps {
                sh 'eslint . || true'  // Or any language-specific linter
            }
        }

        stage('Secrets Scan (GitLeaks)') {
            steps {
                sh 'gitleaks detect --source . --no-git --redact'
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv("${SONARQUBE_SERVER}") {
                    sh 'sonar-scanner'
                }
            }
        }

        stage('SonarQube Quality Gate') {
            steps {
                script {
                    timeout(time: 5, unit: 'MINUTES') {
                        def qg = waitForQualityGate()
                        if (qg.status != 'OK') {
                            sendSlack("${SLACK_DEV_CHANNEL}", "🚫 SonarQube Quality Gate FAILED for build ${env.BUILD_NUMBER}")
                            sendSlackUser("${COMMITTER}", "🚫 Your commit failed SonarQube Quality Gate in build ${env.BUILD_NUMBER}")
                            error "Pipeline aborted due to Quality Gate failure: ${qg.status}"
                        }
                    }
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                sh "docker build -t ${DOCKER_IMAGE} ."
            }
        }

        stage('Trivy Scan Docker Image') {
            steps {
                sh "trivy image --exit-code 1 --severity HIGH,CRITICAL ${DOCKER_IMAGE}"
            }
        }

        stage('Push Image to Registry') {
            steps {
                sh "docker push ${DOCKER_IMAGE}"
            }
        }

        stage('Deploy to Dev Namespace (K8s)') {
            steps {
                sh "kubectl set image deployment/myapp myapp=${DOCKER_IMAGE} -n ${K8S_NAMESPACE}"
            }
        }
    }

    post {
        success {
            script {
                sendSlack("${SLACK_DEV_CHANNEL}", "✅ Build & Deploy successful for build ${env.BUILD_NUMBER}")
                sendSlack("${SLACK_QA_CHANNEL}", "🧪 Dev build ${env.BUILD_NUMBER} is ready for QA testing.")
            }
        }
        failure {
            script {
                sendSlack("${SLACK_DEV_CHANNEL}", "❌ Build FAILED for build ${env.BUILD_NUMBER}")
                sendSlackUser("${COMMITTER}", "❌ Your commit caused build ${env.BUILD_NUMBER} to fail. Please check Jenkins.")
            }
        }
    }
}

def sendSlack(channel, message) {
    slackSend(channel: channel, color: '#FF0000', message: message)
}

def sendSlackUser(email, message) {
    slackSend(channel: "@${email.split('@')[0]}", color: '#FF0000', message: message)
}

🔹 How It Works

  1. Checkout code from dev branch.

  2. Linting → catches syntax/style issues.

  3. GitLeaks → detects secrets in code.

  4. SonarQube → runs code analysis + waits for Quality Gate pass.

  5. Docker build → builds image with build number tag.

  6. Trivy scan → blocks build on HIGH/CRITICAL vulnerabilities.

  7. Push image → to single shared registry (no per-env repos).

  8. Deploy to Dev namespace in Kubernetes.

  9. Slack Notifications

    • Failure: committer + dev channel.

    • Success: dev channel + QA channel.


2️⃣ QA Pipeline (Promote & Regression Tests)

Trigger: Merge devqa
Goal: Pull Dev image → retag as QA → push → deploy to QA namespace.

pipeline {
    agent any
    environment {
        IMAGE_REPO = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp"
        QA_TAG = "qa-${BUILD_NUMBER}"
        DEV_TAG = "dev-${params.DEV_BUILD_NUMBER}"
    }
    parameters {
        string(name: 'DEV_BUILD_NUMBER', description: 'Build number from Dev pipeline')
    }
    stages {
        stage('Promote Image') {
            steps {
                sh """
                docker pull ${IMAGE_REPO}:${DEV_TAG}
                docker tag ${IMAGE_REPO}:${DEV_TAG} ${IMAGE_REPO}:${QA_TAG}
                docker push ${IMAGE_REPO}:${QA_TAG}
                """
            }
        }
        stage('Deploy to QA') {
            steps {
                sh "kubectl set image deployment/myapp myapp=${IMAGE_REPO}:${QA_TAG} -n qa"
            }
        }
        stage('Regression Tests') {
            steps {
                sh "pytest tests/regression"
            }
        }
    }
}

3️⃣ Stage Pipeline (Pre-Prod)

Trigger: Merge qarelease/x.y.z
Goal: Promote QA image → Stage → deploy → run load tests.

pipeline {
    agent any
    environment {
        IMAGE_REPO = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp"
        STAGE_TAG = "stage-${BUILD_NUMBER}"
        QA_TAG = "qa-${params.QA_BUILD_NUMBER}"
    }
    parameters {
        string(name: 'QA_BUILD_NUMBER', description: 'Build number from QA pipeline')
    }
    stages {
        stage('Promote Image') {
            steps {
                sh """
                docker pull ${IMAGE_REPO}:${QA_TAG}
                docker tag ${IMAGE_REPO}:${QA_TAG} ${IMAGE_REPO}:${STAGE_TAG}
                docker push ${IMAGE_REPO}:${STAGE_TAG}
                """
            }
        }
        stage('Deploy to Stage') {
            steps {
                sh "kubectl set image deployment/myapp myapp=${IMAGE_REPO}:${STAGE_TAG} -n stage"
            }
        }
        stage('Load Tests') {
            steps {
                sh "locust -f load_tests.py"
            }
        }
    }
}

4️⃣ Prod Pipeline

Q. So in prod do we run prod pipeline manually or by tag ?

In most organizations, Prod pipelines are triggered by tag creation, not manual branch pushes.

Flow:

  1. Stage is fully tested.

  2. Release branch is merged into main (or master).

  3. A Git tag is created on main (e.g., v1.2.3).

  4. Jenkins detects the tag → triggers the Prod pipeline automatically.

  5. Pipeline pauses at manual approval before deployment.

  6. After approval → deploys the already built Stage image to Prod.

✅ Manual triggering is generally avoided for Prod to maintain traceability and consistency.

  • The only manual step is the approval inside the pipeline, not starting it.

You want me to modify your Prod Jenkinsfile to auto-capture the Git tag instead of passing it manually?

Prod Jenkinsfile updated with Slack notifications for:

  • Approval request

  • Deployment success

  • Deployment failure

Trigger: Merge release/x.y.zmain + tag (e.g., v1.2.3)
Goal: Promote Stage image → Prod → smoke tests.

pipeline {
    agent any
    environment {
        IMAGE_REPO = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp"
        K8S_NAMESPACE = "prod"
        SLACK_DEV_CHANNEL = "#devchannel"
        SLACK_RELEASE_CHANNEL = "#release-channel"
    }

    stages {
        stage('Set Variables') {
            steps {
                script {
                    // Capture Git tag that triggered the build
                    PROD_TAG = env.GIT_TAG_NAME ?: error("No Git tag found. Trigger the pipeline by creating a tag on main.")
                    STAGE_IMAGE = "${IMAGE_REPO}:${PROD_TAG}"
                    echo "Using Stage image: ${STAGE_IMAGE}"
                }
            }
        }

        stage('Approval') {
            steps {
                script {
                    slackSend(channel: "${SLACK_DEV_CHANNEL}", color: "#FFA500",
                              message: "⚠️ Approval required for deploying ${PROD_TAG} to Production. Please review and approve.")
                    input message: "Do you approve deploying ${PROD_TAG} to Production?", ok: "Deploy"
                }
            }
        }

        stage('Deploy to Prod') {
            steps {
                sh """
                docker pull ${STAGE_IMAGE}
                kubectl set image deployment/myapp myapp=${STAGE_IMAGE} -n ${K8S_NAMESPACE}
                """
            }
        }

        stage('Smoke Tests') {
            steps {
                sh "pytest tests/smoke"
            }
        }
    }

    post {
        success {
            script {
                slackSend(channel: "${SLACK_DEV_CHANNEL}", color: "#36A64F",
                          message: "✅ Prod deployment SUCCESS for ${PROD_TAG}")
                slackSend(channel: "${SLACK_RELEASE_CHANNEL}", color: "#36A64F",
                          message: "🚀 ${PROD_TAG} deployed to Production successfully.")
            }
        }
        failure {
            script {
                slackSend(channel: "${SLACK_DEV_CHANNEL}", color: "#FF0000",
                          message: "❌ Prod deployment FAILED for ${PROD_TAG}")
                slackSend(channel: "${SLACK_RELEASE_CHANNEL}", color: "#FF0000",
                          message: "⚠️ Deployment FAILED for ${PROD_TAG}. Check Jenkins logs.")
            }
        }
    }
}

✅ Flow

  1. Jenkins triggers automatically on Git tag push to main.

  2. Slack notification is sent to #devchannel asking for approval.

  3. Pipeline pauses for manual approval.

  4. Once approved:

    • Stage/release image is pulled and deployed to Prod.

    • Smoke tests run.

  5. Slack notifications are sent to #devchannel and #release-channel for success/failure.

This keeps Prod deployment safe, traceable, and fully communicated.

🔹 How it works

  1. Jenkins job is configured to build when a Git tag is pushed (refspec: +refs/tags/*:refs/remotes/origin/tags/*).

  2. Pipeline automatically captures the tag via env.GIT_TAG_NAME.

  3. Manual approval stage ensures safe deployment.

  4. Deploys the Stage image with the same tag to Prod namespace.

  5. Runs post-deployment smoke tests.

This way, there’s no manual parameter entry, and the pipeline is fully Git tag-driven.

Q. So for above prod Jenkinsfile we have manual approval for deployment to production right ?

Yes ✅ — in most setups, the Prod Jenkins pipeline includes a manual approval step before deploying, even if it’s triggered by a tag.

Why:

  • Safety: prevent accidental deployment of a release tag.

  • Ensures DevOps or Release Manager verifies everything (Stage tests, release notes, etc.) before Prod.

How it fits:

  1. Tag is pushed on main → Jenkins pipeline triggers.

  2. Pipeline pauses at “Input/Approval” stage.

  3. DevOps/Release Manager reviews details → clicks “Proceed” in Jenkins.

  4. Pipeline continues → deploys the already tested release image to Prod.

If you want, I can modify the Prod Jenkinsfile to include a proper manual approval/input step before deployment.

Q. Why we deploy the image even though we already have it

  • “Deployment” ≠ build — the image exists in the registry, but Kubernetes doesn’t know about it until you update your deployment.

  • kubectl set image tells K8s:

    • “Use this specific image in this deployment/pod.”
  • Without deployment, the image is just sitting in your registry — no containers are running in Prod.

  • Deployment ensures:

    1. Pods are updated to the new image.

    2. Rolling update or restart happens safely.

    3. Prod environment actually reflects the release.

So even though the artifact/image exists, deployment is the step that makes it live in the target environment.

Q. So prod triggers when tag is created on main branch right ?:

Yes — in most CI/CD setups, Prod deployment is triggered when a version tag is created on the main (or master) branch.

Typical flow:

  1. Release branch is fully tested in Stage.

  2. Release branch is merged into main.

  3. Version tag is created (e.g., v1.3.0).

  4. CI/CD pipeline detects this tag push → pulls the already tested Stage/Release image from the registry → deploys to Production.

The key point is: we don’t rebuild the image for Prod — we promote the tested image.

Do you want me to also add the Prod pipeline YAML/Jenkinsfile matching this tagging trigger?

Q. And how dev/qa/stage pipelines runs by tag or push ?

In short —

  • Dev pipeline → triggered by push (or PR merge) to dev branch.

  • QA pipeline → triggered by merge to qa branch (or sometimes by promoting a Dev image tag).

  • Stage pipeline → triggered by merge to release branch (or promoting a QA image tag).

  • Prod pipeline → triggered by tag creation on main branch.

So:
Push → Dev
Merge → QA
Merge → Stage
Tag → Prod


5️⃣ DR Pipeline

Trigger: Auto after Prod deployment
Goal: Deploy same prod image to DR.

pipeline {
    agent any
    environment {
        IMAGE_REPO = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp"
        PROD_TAG = "v${params.VERSION}"
    }
    parameters {
        string(name: 'VERSION', description: 'Prod version to deploy')
    }
    stages {
        stage('Deploy to DR') {
            steps {
                sh "kubectl set image deployment/myapp myapp=${IMAGE_REPO}:${PROD_TAG} -n dr"
            }
        }
        stage('Smoke Tests') {
            steps {
                sh "pytest tests/smoke"
            }
        }
    }
}

Key Points

  • One artifact repository (ECR/JFrog/Nexus) with different tags per stage.

  • No rebuild after Dev → just retag & push.

  • Every environment has its own namespace in Kubernetes (dev, qa, stage, prod, dr).

  • Merges control promotion, not code changes per environment.

  • Main branch always reflects Prod → after release is merged, merge main → dev to keep sync.


    Q. So prod triggers when tag is created on main branch right ?

    Yes — in most CI/CD setups, Prod deployment is triggered when a version tag is created on the main (or master) branch.

    Typical flow:

    1. Release branch is fully tested in Stage.

    2. Release branch is merged into main.

    3. Version tag is created (e.g., v1.3.0).

    4. CI/CD pipeline detects this tag push → pulls the already tested Stage/Release image from the registry → deploys to Production.

The key point is: we don’t rebuild the image for Prod — we promote the tested image.

  • Q. How dev/qa/stage pipelines runs by tag or push in short

    In short —

    • Dev pipeline → triggered by push (or PR merge) to dev branch.

    • QA pipeline → triggered by merge to qa branch (or sometimes by promoting a Dev image tag).

    • Stage pipeline → triggered by merge to release branch (or promoting a QA image tag).

    • Prod pipeline → triggered by tag creation on main branch.

So:
Push → Dev
Merge → QA
Merge → Stage
Tag → Prod


0
Subscribe to my newsletter

Read articles from Aditya Patil directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aditya Patil
Aditya Patil

Hi, I'm Aditya — a Cloud & DevOps Engineer passionate about automating everything from CI/CD pipelines to multi-cloud infrastructure. I specialize in AWS, Kubernetes, Terraform, and GitOps tools like Argo CD. I’ve helped teams scale applications, cut cloud costs by 90%, and build disaster-ready infra. I love sharing real-world DevOps lessons, cloud cost optimization tips, and infrastructure design patterns. Let’s connect and simplify the cloud — one YAML file at a time ☁️⚙️