// Single source of truth for env injection. // Adding/removing a variable means editing one `keys` list — nothing else. // `path` is the Vault KV path; `file` is the output path relative to ${WORKSPACE}. def ENV_LAYOUT = [ [path: 'ggh-prod/root', file: '.env', keys: [ 'POSTGRES_DB', 'POSTGRES_PASSWORD', 'POSTGRES_USER', ]], [path: 'ggh-prod/backend', file: 'backend/.env', keys: [ 'BYTESCALE_SECRET_API_KEY', 'CORS_ALLOWED_ORIGINS', 'DATABASE_URL', 'EMAIL_FROM', 'JWT_SECRET', 'PORT', 'POSTMARK_API_KEY', ]], [path: 'ggh-prod/dashboard-frontend', file: 'frontend/.env', keys: [ 'VITE_API_URL', ]], ] pipeline { agent { label 'jagent06' } options { disableConcurrentBuilds() } environment { REMOTE_HOST = "root@170.187.237.83" REMOTE_DEPLOY_DIR = "/root/gg-backend" SSH_CREDENTIALS_ID = "ssh_id_ed25519" COMPOSE_FILE = "docker-compose.prod.yml" } stages { stage('Checkout Code') { steps { git credentialsId: 'SYS_BOT_PAT', url: 'https://gt.mgsigma.net/GGH/gg-backend.git', branch: 'main' } } stage('Inject .env from Vault') { steps { script { def vaultSecrets = ENV_LAYOUT.collect { layer -> [ path: layer.path, engineVersion: 2, secretValues: layer.keys.collect { k -> [vaultKey: k, envVar: k] } ] } def writtenFiles = ENV_LAYOUT.collect { it.file }.join(', ') withVault( configuration: [ vaultUrl: 'https://vault.mgsigma.net', vaultCredentialId: 'vault-cicd-01' ], vaultSecrets: vaultSecrets ) { // Single-quoted sh + withEnv keeps secret names out of the // Groovy GString. Values are read at shell runtime via // `printenv`, so Jenkins's static analyzer has nothing to flag. ENV_LAYOUT.each { layer -> withEnv([ "ENV_FILE=${layer.file}", "ENV_KEYS=${layer.keys.join(' ')}" ]) { sh ''' set +x umask 077 mkdir -p "$(dirname "$WORKSPACE/$ENV_FILE")" : > "$WORKSPACE/$ENV_FILE" for k in $ENV_KEYS; do printf '%s=%s\n' "$k" "$(printenv "$k")" >> "$WORKSPACE/$ENV_FILE" done ''' } } echo "[INFO] env files written: ${writtenFiles}" } } } } stage('Sync sources to prod VPS') { steps { sshagent (credentials: ["${env.SSH_CREDENTIALS_ID}"]) { sh """ ssh -o StrictHostKeyChecking=no $REMOTE_HOST 'mkdir -p $REMOTE_DEPLOY_DIR' rsync -az --delete \\ --exclude='.git/' \\ --exclude='node_modules/' \\ --exclude='backend/node_modules/' \\ --exclude='frontend/node_modules/' \\ --exclude='frontend/dist/' \\ -e "ssh -o StrictHostKeyChecking=no" \\ ./ $REMOTE_HOST:$REMOTE_DEPLOY_DIR/ """ } } } stage('Deploy on prod VPS') { steps { sshagent (credentials: ["${env.SSH_CREDENTIALS_ID}"]) { sh """ ssh -o StrictHostKeyChecking=no $REMOTE_HOST ' set -e cd $REMOTE_DEPLOY_DIR if [ -n "\$(docker compose -f $COMPOSE_FILE ps -q 2>/dev/null)" ]; then echo "[INFO] Stopping existing containers..." docker compose -f $COMPOSE_FILE down --remove-orphans else echo "[INFO] No running containers — skipping stop." fi docker image prune -f >/dev/null 2>&1 || true echo "[INFO] Building and starting services..." docker compose -f $COMPOSE_FILE up -d --build --remove-orphans ' """ } } } } post { always { script { env.BUILD_INFO_HASH = sh( script: "git rev-parse --short HEAD 2>/dev/null || echo N/A", returnStdout: true ).trim() env.BUILD_INFO_AUTHOR = sh( script: "git log -1 --pretty=format:'%an' 2>/dev/null || echo N/A", returnStdout: true ).trim() env.BUILD_INFO_MESSAGE = sh( script: "git log -1 --pretty=format:'%s' 2>/dev/null || echo 'No commit message found'", returnStdout: true ).trim() def cause = currentBuild.getBuildCauses()?.getAt(0)?.shortDescription ?: 'Unknown' env.BUILD_INFO_TRIGGER = cause.contains('Started by user') ? 'Manual trigger' : cause } } success { script { emailext( subject: "[PRODUCTION] ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded", body: renderEmail('Build succeeded', '#16a34a', buildInfo()), mimeType: 'text/html', to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com' ) } } failure { script { emailext( subject: "[PRODUCTION] ${env.JOB_NAME} #${env.BUILD_NUMBER} failed", body: renderEmail('Build failed', '#dc2626', buildInfo()), mimeType: 'text/html', to: 'admin@msigmagokulam.com, ashir@mgsigma.net, kailasdevdas@msigmagokulam.com' ) } } } } def buildInfo() { return [ hash: env.BUILD_INFO_HASH ?: 'N/A', author: env.BUILD_INFO_AUTHOR ?: 'N/A', message: env.BUILD_INFO_MESSAGE ?: 'N/A', trigger: env.BUILD_INFO_TRIGGER ?: 'Unknown', duration: currentBuild.durationString.replace(' and counting', '') ] } def renderEmail(String label, String accent, Map info) { return """\
${label} · production
${env.JOB_NAME} · #${env.BUILD_NUMBER}
Commit${info.hash}
Author${info.author}
Message${info.message}
Triggered by${info.trigger}
Duration${info.duration}
View build Console output →
Automated notification from Jenkins
""" }