From dda6b75dce9a71bf007429c23011e1ba567e44c8 Mon Sep 17 00:00:00 2001 From: Ashir Ali <32892597+AshirAli@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:33:42 +0530 Subject: [PATCH] feat: Jenkinsfile added --- Jenkinsfile | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..9ddbcba --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,217 @@ +// 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 any + + 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: 'gittea_PAT_aji' + ]] + ]) + } + } + + 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 """\ + + +
+| + + |