This commit is contained in:
428
Jenkinsfile
vendored
Normal file
428
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,428 @@
|
||||
// =============================================================================
|
||||
// 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 variables - sensitive values come from Jenkins Credentials
|
||||
// -------------------------------------------------------------------------
|
||||
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 1: Checkout & Metadata
|
||||
// =====================================================================
|
||||
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 2: Install Dependencies & Lint
|
||||
// =====================================================================
|
||||
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 3: Test
|
||||
// =====================================================================
|
||||
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 4: Docker Build (all branches)
|
||||
// =====================================================================
|
||||
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 5: Docker Push (main + staging branches only)
|
||||
// =====================================================================
|
||||
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 6: Deploy to Kubernetes
|
||||
//
|
||||
// main → onpremise overlay (production)
|
||||
// staging → minikube overlay (staging / dev cluster)
|
||||
// =====================================================================
|
||||
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 7: Smoke Test (post-deploy health check)
|
||||
// =====================================================================
|
||||
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 Actions
|
||||
// =========================================================================
|
||||
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
|
||||
// =============================================================================
|
||||
Reference in New Issue
Block a user