// 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-dev/root', file: '.env', keys: [ 'POSTGRES_DB', 'POSTGRES_PASSWORD', 'POSTGRES_USER', ]], [path: 'ggh-dev/backend', file: 'backend/.env', keys: [ 'BYTESCALE_SECRET_API_KEY', 'CORS_ALLOWED_ORIGINS', 'DATABASE_URL', 'EMAIL_FROM', 'JWT_SECRET', 'PORT', 'POSTMARK_API_KEY', ]], [path: 'ggh-dev/dashboard-frontend', file: 'frontend/.env', keys: [ 'VITE_API_URL', ]], ] pipeline { agent { label 'jagent06' } triggers { GenericTrigger( causeString: 'Triggered by Gitea webhook', tokenCredentialId: 'ggh-dev', printContributedVariables: true, printPostContent: true ) } environment { DOCKER_COMPOSE_FILE = "${WORKSPACE}/docker-compose.dev.yml" DOCKER_BUILDKIT = '1' COMPOSE_DOCKER_CLI_BUILD = '1' } stages { stage('Checkout Code') { steps { checkout([ $class: 'GitSCM', branches: [[name: '*/dev']], userRemoteConfigs: [[ url: 'https://gt.mgsigma.net/GGH/gg-backend.git', credentialsId: 'SYS_BOT_PAT' ]] ]) } } 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('Deploy') { steps { script { if (!fileExists(env.DOCKER_COMPOSE_FILE)) { error "docker-compose.dev.yml not found at ${env.DOCKER_COMPOSE_FILE}" } } sh ''' set -e if [ -n "$(docker compose -f docker-compose.dev.yml ps -q 2>/dev/null)" ]; then echo "[INFO] Stopping existing containers..." docker compose -f docker-compose.dev.yml down --remove-orphans else echo "[INFO] No running containers — skipping stop." fi # Dangling-only prune — keeps tagged images and BuildKit cache so # subsequent builds reuse layers. Do NOT use `docker system prune -af`. docker image prune -f >/dev/null 2>&1 || true echo "[INFO] Building and starting services..." docker compose -f docker-compose.dev.yml 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: "[Jenkins] ${env.JOB_NAME} #${env.BUILD_NUMBER} succeeded", body: renderEmail('Build succeeded', '#16a34a', buildInfo()), mimeType: 'text/html', to: 'admin@msigmagokulam.com, ashir@mgsigma.net' ) } } failure { script { emailext( subject: "[Jenkins] ${env.JOB_NAME} #${env.BUILD_NUMBER} failed", body: renderEmail('Build failed', '#dc2626', buildInfo()), mimeType: 'text/html', to: 'admin@msigmagokulam.com, ashir@mgsigma.net' ) } } } } 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}
${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
""" }