// ============================================================================= // Multibranch Pipeline - react-mysql // Docker Hub: subkamble/react-mysql-backend, subkamble/react-mysql-frontend // Kubernetes Namespaces: react-mysql // Overlays: k8s/overlays/minikube | k8s/overlays/onpremise // // Branch Strategy: // main → deploy to production (onpremise overlay) // staging → deploy to staging (minikube overlay, or staging namespace) // develop → build + test only (no deploy) // feature/** → build + test only (no deploy) // PR branches → build + test only (no deploy) // ============================================================================= pipeline { agent any environment { DOCKER_HUB_USER = 'subkamble' BACKEND_IMAGE = "${DOCKER_HUB_USER}/react-mysql-backend" FRONTEND_IMAGE = "${DOCKER_HUB_USER}/react-mysql-frontend" // Jenkins credential IDs (configure in Jenkins > Credentials) DOCKER_CREDENTIALS = 'dockerhub-credentials' // kind: Username/Password KUBECONFIG_SECRET = 'kubeconfig-secret' // kind: Secret File // Derived from branch name IS_MAIN = "${env.BRANCH_NAME == 'main'}" IS_STAGING = "${env.BRANCH_NAME == 'staging'}" IS_DEPLOYABLE = "${env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'staging'}" // Image tag: use git short SHA for traceability GIT_SHORT_SHA = "${env.GIT_COMMIT ? env.GIT_COMMIT.take(8) : 'unknown'}" IMAGE_TAG = "${env.BRANCH_NAME == 'main' ? 'latest' : env.BRANCH_NAME.replaceAll('/', '-') + '-' + GIT_SHORT_SHA}" } options { buildDiscarder(logRotator(numToKeepStr: '20')) timeout(time: 30, unit: 'MINUTES') disableConcurrentBuilds() //timestamps() } stages { stage('Checkout') { steps { checkout scm script { echo "Branch : ${env.BRANCH_NAME}" echo "Commit SHA : ${env.GIT_COMMIT}" echo "Image Tag : ${env.IMAGE_TAG}" echo "Deployable : ${env.IS_DEPLOYABLE}" } } } stage('Install & Lint') { parallel { stage('Backend - Install') { steps { dir('backend') { sh 'npm ci --prefer-offline' } } } stage('Frontend - Install') { steps { dir('frontend') { sh 'npm ci --prefer-offline' } } } } } stage('Test') { parallel { stage('Backend - Test') { steps { dir('backend') { // Replace with your actual test command sh 'npm test --if-present || echo "No backend tests configured"' } } } stage('Frontend - Test') { steps { dir('frontend') { // CI=true prevents interactive watch mode in React sh 'CI=true npm test --if-present || echo "No frontend tests configured"' } } } } } stage('Docker Build') { parallel { stage('Build Backend') { steps { dir('backend') { sh "docker build -t ${BACKEND_IMAGE}:${IMAGE_TAG} ." } } } stage('Build Frontend') { steps { dir('frontend') { sh "docker build -t ${FRONTEND_IMAGE}:${IMAGE_TAG} ." } } } } } stage('Docker Push') { when { expression { env.IS_DEPLOYABLE == 'true' } } steps { withCredentials([usernamePassword( credentialsId: "${DOCKER_CREDENTIALS}", usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { sh ''' echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin docker push ${BACKEND_IMAGE}:${IMAGE_TAG} docker push ${FRONTEND_IMAGE}:${IMAGE_TAG} docker logout ''' } // Also tag + push as 'latest' on main script { if (env.BRANCH_NAME == 'main') { withCredentials([usernamePassword( credentialsId: "${DOCKER_CREDENTIALS}", usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS' )]) { sh ''' echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin docker tag ${BACKEND_IMAGE}:${IMAGE_TAG} ${BACKEND_IMAGE}:latest docker tag ${FRONTEND_IMAGE}:${IMAGE_TAG} ${FRONTEND_IMAGE}:latest docker push ${BACKEND_IMAGE}:latest docker push ${FRONTEND_IMAGE}:latest docker logout ''' } } } } } stage('Deploy') { when { expression { env.IS_DEPLOYABLE == 'true' } } steps { script { def overlay = (env.BRANCH_NAME == 'main') ? 'onpremise' : 'minikube' echo "Deploying to overlay: ${overlay}" withCredentials([file( credentialsId: "${KUBECONFIG_SECRET}", variable: 'KUBECONFIG' )]) { sh """ export KUBECONFIG=\$KUBECONFIG # Update image tags in kustomize overlays in-place cd k8s/base kustomize edit set image \\ ${BACKEND_IMAGE}=${BACKEND_IMAGE}:${IMAGE_TAG} \\ ${FRONTEND_IMAGE}=${FRONTEND_IMAGE}:${IMAGE_TAG} cd - # Apply the overlay kubectl apply -k k8s/overlays/${overlay} # Wait for rollout kubectl rollout status deployment/backend -n react-mysql --timeout=120s kubectl rollout status deployment/frontend -n react-mysql --timeout=120s """ } } } } stage('Smoke Test') { when { expression { env.IS_DEPLOYABLE == 'true' } } steps { script { withCredentials([file( credentialsId: "${KUBECONFIG_SECRET}", variable: 'KUBECONFIG' )]) { sh ''' export KUBECONFIG=$KUBECONFIG # Verify pods are running kubectl get pods -n react-mysql # Quick readiness check kubectl wait --for=condition=Ready pods -l app=backend -n react-mysql --timeout=60s kubectl wait --for=condition=Ready pods -l app=frontend -n react-mysql --timeout=60s echo "Smoke test passed." ''' } } } } } post { always { // Clean up dangling local Docker images to save disk space sh ''' docker rmi ${BACKEND_IMAGE}:${IMAGE_TAG} || true docker rmi ${FRONTEND_IMAGE}:${IMAGE_TAG} || true docker image prune -f || true ''' } success { echo "Pipeline SUCCESS on branch '${env.BRANCH_NAME}' — image tag: ${env.IMAGE_TAG}" } failure { echo "Pipeline FAILED on branch '${env.BRANCH_NAME}'. Check logs above." // Uncomment to send email notifications: // emailext( // subject: "FAILED: ${env.JOB_NAME} [${env.BUILD_NUMBER}]", // body: "Branch: ${env.BRANCH_NAME}\nBuild URL: ${env.BUILD_URL}", // to: 'your-team@example.com' // ) } } } // GITOPS / ARGOCD WORKFLOW (commented out — activate when ArgoCD is installed) // ============================================================================= // // Overview: // Instead of Jenkins calling kubectl directly (push model), ArgoCD watches // a Git repository for k8s manifests and syncs the cluster (pull model). // // Jenkins responsibility becomes: // 1. Build & push Docker image // 2. Update the image tag in the GitOps repo (PR or direct commit) // 3. ArgoCD detects the change and syncs the cluster automatically // // Prerequisites: // - ArgoCD installed in cluster: kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml // - argocd CLI available on Jenkins agent // - Jenkins credentials: // 'argocd-auth-token' → Secret Text (ArgoCD API token) // 'gitops-ssh-key' → SSH Key (write access to GitOps repo) // - A dedicated GitOps repo (can be this same repo or a separate one) // // ============================================================================= // // ---- ARGOCD APPLICATION MANIFEST (apply once, not in pipeline) -------------- // // # argocd/application-production.yaml // apiVersion: argoproj.io/v1alpha1 // kind: Application // metadata: // name: react-mysql-production // namespace: argocd // spec: // project: default // source: // repoURL: https://github.com/YOUR_ORG/react-mysql.git // targetRevision: main // path: k8s/overlays/onpremise // destination: // server: https://kubernetes.default.svc // namespace: react-mysql // syncPolicy: // automated: // prune: true # Remove resources no longer in Git // selfHeal: true # Revert manual cluster changes // syncOptions: // - CreateNamespace=true // // --- // // # argocd/application-staging.yaml // apiVersion: argoproj.io/v1alpha1 // kind: Application // metadata: // name: react-mysql-staging // namespace: argocd // spec: // project: default // source: // repoURL: https://github.com/YOUR_ORG/react-mysql.git // targetRevision: staging // path: k8s/overlays/minikube // destination: // server: https://kubernetes.default.svc // namespace: react-mysql // syncPolicy: // automated: // prune: true // selfHeal: true // syncOptions: // - CreateNamespace=true // // ============================================================================= // // ---- GITOPS-ENABLED PIPELINE STAGES (replace stages 5-7 above) -------------- // // // stage('Docker Push') { ... } ← keep as-is // // // stage('Update GitOps Manifest') { // // when { expression { env.IS_DEPLOYABLE == 'true' } } // // steps { // // script { // // def overlay = (env.BRANCH_NAME == 'main') ? 'onpremise' : 'minikube' // // withCredentials([sshUserPrivateKey( // // credentialsId: 'gitops-ssh-key', // // keyFileVariable: 'SSH_KEY' // // )]) { // // sh """ // // # Configure git SSH // // GIT_SSH_COMMAND="ssh -i \$SSH_KEY -o StrictHostKeyChecking=no" // // export GIT_SSH_COMMAND // // // // # Clone the GitOps repo (or use current repo) // // git clone git@github.com:YOUR_ORG/react-mysql.git gitops-repo // // cd gitops-repo // // // // git config user.email "jenkins@ci.local" // // git config user.name "Jenkins CI" // // // // # Bump image tags using kustomize // // cd k8s/base // // kustomize edit set image \\ // // ${BACKEND_IMAGE}=${BACKEND_IMAGE}:${IMAGE_TAG} \\ // // ${FRONTEND_IMAGE}=${FRONTEND_IMAGE}:${IMAGE_TAG} // // cd ../.. // // // // git add k8s/ // // git commit -m "ci: bump images to ${IMAGE_TAG} [${env.BRANCH_NAME}] [skip ci]" // // git push origin ${env.BRANCH_NAME} // // """ // // } // // } // // } // // } // // // stage('ArgoCD Sync') { // // when { expression { env.IS_DEPLOYABLE == 'true' } } // // steps { // // script { // // def appName = (env.BRANCH_NAME == 'main') ? 'react-mysql-production' : 'react-mysql-staging' // // withCredentials([string( // // credentialsId: 'argocd-auth-token', // // variable: 'ARGOCD_TOKEN' // // )]) { // // sh """ // // # Login to ArgoCD (token-based) // // argocd login YOUR_ARGOCD_SERVER \\ // // --auth-token \$ARGOCD_TOKEN \\ // // --grpc-web \\ // // --insecure // // // // # Trigger sync (ArgoCD auto-detects git changes too, // // # but this forces an immediate sync) // // argocd app sync ${appName} --prune --timeout 120 // // // // # Wait for sync to complete // // argocd app wait ${appName} --health --timeout 180 // // // // echo "ArgoCD sync complete for ${appName}" // // """ // // } // // } // // } // // } // // // stage('Smoke Test') { ... } ← keep as-is (or query ArgoCD health instead) // // ============================================================================= // END GITOPS / ARGOCD SECTION // =============================================================================