/* * blut24-landing — CI/CD * * Trigger : push to main on git@github.com:younex1/blut24-landing.git * Jenkins : http://192.168.12.65:8080/ * Target : 192.168.12.81, live at /var/www/blut24.com/web/current * * Jenkins only ORCHESTRATES. The install/build happens on the target host, * because better-sqlite3 is a native addon that must be compiled against the * Node ABI, OS and libc it will actually run on. Availability is unaffected — * the previous release keeps serving until deploy.sh has built, health-checked * and atomically swapped the new one. * * Setup for this job is documented in deploy/README.md. */ pipeline { agent any options { // NOTE: disableConcurrentBuilds() was removed deliberately. With it set, every build // after the first sat in the queue as a BlockedItem and never started until Jenkins // was restarted — observed three times on this controller. The race it guards against // is two deploys swapping the same symlink, which is a resource on the TARGET, not on // Jenkins; deploy.sh now takes an flock there instead, which also protects against // deploys started by hand or from another controller. buildDiscarder(logRotator(numToKeepStr: '30')) timeout(time: 25, unit: 'MINUTES') timestamps() } triggers { githubPush() // fires on the GitHub webhook pollSCM('H/5 * * * *') // safety net if the webhook cannot reach Jenkins } environment { DEPLOY_HOST = '192.168.12.81' DEPLOY_USER = 'root' APP_ROOT = '/var/www/blut24.com/web' APP_NAME = 'blut24-landing' // No credential binding: the `ssh-agent` plugin is not installed on this // controller and it is shared with 19 other jobs, so it cannot be restarted // to add one. The jenkins user's own key (~/.ssh/id_rsa) is authorised for // root@192.168.12.81 instead, which needs no plugin at all. SSH_OPTS = '-o StrictHostKeyChecking=accept-new -o BatchMode=yes -o ConnectTimeout=15' } stages { stage('Checkout') { steps { checkout scm script { env.GIT_SHA = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() env.RELEASE_ID = "${env.BUILD_NUMBER}-${env.GIT_SHA}" env.RELEASE_DIR = "${env.APP_ROOT}/releases/${env.RELEASE_ID}" currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.GIT_SHA}" } sh 'echo "Deploying $RELEASE_ID from $(git log -1 --pretty=%s)"' } } /* * Guard rail: main only. The webhook is branch-agnostic, so a push to any * other branch would otherwise deploy to production. */ stage('Guard: main only') { when { not { anyOf { branch 'main'; expression { env.GIT_BRANCH?.endsWith('/main') } } } } steps { script { currentBuild.result = 'NOT_BUILT' error("Branch '${env.GIT_BRANCH}' is not main — nothing deployed.") } } } stage('Ship source to target') { steps { sh ''' set -eu SSH="ssh ${SSH_OPTS}" $SSH ${DEPLOY_USER}@${DEPLOY_HOST} "mkdir -p ${RELEASE_DIR}" # Ship the working tree, not a git clone — the target needs no git and # no GitHub credentials. Excludes are the things the target builds or # owns itself; data/ in particular is a symlink to shared/ on the host # and must never be overwritten by the repo copy. rsync -az --delete \ -e "$SSH" \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.nuxt' \ --exclude '.output' \ --exclude 'data/*.db' \ --exclude 'data/*.db-*' \ --exclude '.env' \ ./ ${DEPLOY_USER}@${DEPLOY_HOST}:${RELEASE_DIR}/ ''' } } stage('Build, health-check and swap') { steps { sh ''' set -eu ssh ${SSH_OPTS} \ ${DEPLOY_USER}@${DEPLOY_HOST} \ "RELEASE_ID=${RELEASE_ID} bash ${RELEASE_DIR}/deploy/deploy.sh" ''' } } stage('Smoke test') { steps { sh ''' set -eu # Independent of deploy.sh's own check: assert the LIVE symlink really # points at this build and that the site answers through it. ssh ${SSH_OPTS} \ ${DEPLOY_USER}@${DEPLOY_HOST} " set -eu live=\\$(basename \\$(readlink -f ${APP_ROOT}/current)) [ \\"\\$live\\" = \\"${RELEASE_ID}\\" ] || { echo \\"web -> \\$live, expected ${RELEASE_ID}\\"; exit 1; } port=\\$(grep -E '^NITRO_PORT=' /var/www/blut24.com/private/blut24-landing/shared/.env | cut -d= -f2 || echo 3002) code=\\$(curl -s -o /dev/null -w '%{http_code}' -m 10 http://127.0.0.1:\\$port/) [ \\"\\$code\\" = \\"200\\" ] || { echo \\"health check returned \\$code\\"; exit 1; } echo \\"smoke OK: \\$live on :\\$port\\" " ''' } } } post { success { echo "Deployed ${env.RELEASE_ID} to ${env.DEPLOY_HOST}:${env.APP_ROOT}/current" } failure { echo "Deploy of ${env.RELEASE_ID} FAILED. deploy.sh rolls the symlink back automatically; " + "the previous release should still be live. Verify with: " + "ssh ${env.DEPLOY_USER}@${env.DEPLOY_HOST} 'readlink -f ${env.APP_ROOT}/current && pm2 list'" } } }