pipeline { agent any options { disableConcurrentBuilds() timestamps() skipDefaultCheckout(true) } environment { REGISTRY = "localhost:5000" SERVER_IP = "192.168.1.95" BACKEND_IMAGE = "${REGISTRY}/the-tip-top/backend" FRONTEND_IMAGE = "${REGISTRY}/the-tip-top/frontend" CI_NETWORK = "tiptop-ci-${JOB_BASE_NAME}-${BUILD_NUMBER}" CI_DATABASE = "tiptop-ci-db-${JOB_BASE_NAME}-${BUILD_NUMBER}" DATABASE_URL = "postgresql://tiptop_ci:tiptop_ci_password@${CI_DATABASE}:5432/the_tip_top_ci" } stages { stage('Checkout') { steps { checkout scm } } stage('Préparer environnement') { steps { script { env.IMAGE_TAG = "${env.BRANCH_NAME}-${env.BUILD_NUMBER}" /* * Stratégie de branches : * * feature/* -> CI uniquement * develop -> DEV automatique * release-* -> PREPROD automatique * main -> PROD avec validation manuelle */ if (env.BRANCH_NAME == 'develop') { env.DEPLOY_ENABLED = 'true' env.DEPLOY_ENV = 'DEV' env.COMPOSE_FILE = 'deploy/dev/docker-compose.yml' env.COMPOSE_PROJECT = 'tiptop-dev' env.DEPLOY_NETWORK = 'tiptop-dev' env.FRONTEND_API_URL = "http://${SERVER_IP}:5001" } else if (env.BRANCH_NAME.startsWith('release-')) { env.DEPLOY_ENABLED = 'true' env.DEPLOY_ENV = 'PREPROD' env.COMPOSE_FILE = 'deploy/preprod/docker-compose.yml' env.COMPOSE_PROJECT = 'tiptop-preprod' env.DEPLOY_NETWORK = 'tiptop-preprod' env.FRONTEND_API_URL = "http://${SERVER_IP}:5002" } else if (env.BRANCH_NAME == 'main') { env.DEPLOY_ENABLED = 'true' env.DEPLOY_ENV = 'PROD' env.COMPOSE_FILE = 'deploy/prod/docker-compose.yml' env.COMPOSE_PROJECT = 'tiptop-prod' env.DEPLOY_NETWORK = 'tiptop-prod' env.FRONTEND_API_URL = "http://${SERVER_IP}:5003" } else { env.DEPLOY_ENABLED = 'false' env.DEPLOY_ENV = 'CI uniquement' env.COMPOSE_FILE = '' env.COMPOSE_PROJECT = '' env.DEPLOY_NETWORK = '' env.FRONTEND_API_URL = '' } echo "Branche : ${env.BRANCH_NAME}" echo "Tag Docker : ${env.IMAGE_TAG}" echo "Environnement : ${env.DEPLOY_ENV}" echo "Déploiement activé : ${env.DEPLOY_ENABLED}" echo "Serveur : ${env.SERVER_IP}" echo "API frontend : ${env.FRONTEND_API_URL}" } } } stage('Vérification du workspace') { steps { sh ''' test -f backend/package.json test -f backend/package-lock.json test -f frontend/package.json test -f frontend/package-lock.json if [ "${DEPLOY_ENABLED}" = "true" ]; then test -f "${COMPOSE_FILE}" fi ''' } } stage('Backend - Tests') { steps { sh ''' docker network create "${CI_NETWORK}" docker run -d \ --name "${CI_DATABASE}" \ --network "${CI_NETWORK}" \ -e POSTGRES_USER=tiptop_ci \ -e POSTGRES_PASSWORD=tiptop_ci_password \ -e POSTGRES_DB=the_tip_top_ci \ postgres:16-alpine echo "Attente de PostgreSQL CI..." for i in $(seq 1 30); do if docker exec "${CI_DATABASE}" \ pg_isready \ -U tiptop_ci \ -d the_tip_top_ci; then echo "PostgreSQL CI est prêt." break fi if [ "$i" -eq 30 ]; then echo "PostgreSQL CI ne répond pas." docker logs "${CI_DATABASE}" exit 1 fi sleep 2 done docker run --rm \ --network "${CI_NETWORK}" \ --volumes-from furious-jenkins \ -v tiptop-npm-cache-backend:/npm-cache \ --user "$(id -u):$(id -g)" \ -e HOME=/tmp \ -e NODE_OPTIONS=--dns-result-order=ipv4first \ -e NPM_CONFIG_CACHE=/npm-cache \ -e NPM_CONFIG_FETCH_RETRIES=5 \ -e NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000 \ -e NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000 \ -e NPM_CONFIG_FETCH_TIMEOUT=600000 \ -e DATABASE_URL="${DATABASE_URL}" \ -e JWT_SECRET=jenkins_ci_secret \ -e NODE_ENV=test \ -w "${WORKSPACE}/backend" \ node:22-alpine \ sh -c " npm ci --prefer-offline --no-audit --no-fund && npx prisma generate && npx prisma migrate deploy && npm test " ''' } post { always { junit( testResults: 'backend/test-results/*.xml', allowEmptyResults: true ) archiveArtifacts( artifacts: 'backend/coverage/**', fingerprint: true, allowEmptyArchive: true ) } } } stage('Frontend - Lint et Build') { steps { sh ''' docker run --rm \ --volumes-from furious-jenkins \ -v tiptop-npm-cache-frontend:/npm-cache \ --user "$(id -u):$(id -g)" \ -e HOME=/tmp \ -e NODE_OPTIONS=--dns-result-order=ipv4first \ -e NPM_CONFIG_CACHE=/npm-cache \ -e NPM_CONFIG_FETCH_RETRIES=5 \ -e NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000 \ -e NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT=120000 \ -e NPM_CONFIG_FETCH_TIMEOUT=600000 \ -w "${WORKSPACE}/frontend" \ node:22-alpine \ sh -c " npm ci --prefer-offline --no-audit --no-fund && npm run lint && npm run build " ''' } } stage('Construction des images Docker') { when { expression { env.DEPLOY_ENABLED == 'true' } } steps { sh ''' docker build \ -t "${BACKEND_IMAGE}:${IMAGE_TAG}" \ backend docker build \ --build-arg VITE_API_URL="${FRONTEND_API_URL}" \ -t "${FRONTEND_IMAGE}:${IMAGE_TAG}" \ frontend ''' } } stage('Publication dans le Registry') { when { expression { env.DEPLOY_ENABLED == 'true' } } steps { sh ''' docker push "${BACKEND_IMAGE}:${IMAGE_TAG}" docker push "${FRONTEND_IMAGE}:${IMAGE_TAG}" ''' } } stage('Validation production') { when { expression { env.DEPLOY_ENV == 'PROD' } } steps { timeout(time: 15, unit: 'MINUTES') { input( message: "Déployer ${IMAGE_TAG} en production ?", ok: "Déployer" ) } } } stage('Déploiement') { when { expression { env.DEPLOY_ENABLED == 'true' } } steps { sh ''' echo "Déploiement de ${BRANCH_NAME} vers ${DEPLOY_ENV}" echo "Image backend : ${BACKEND_IMAGE}:${IMAGE_TAG}" echo "Image frontend : ${FRONTEND_IMAGE}:${IMAGE_TAG}" echo "Compose : ${COMPOSE_FILE}" REGISTRY="${REGISTRY}" \ IMAGE_TAG="${IMAGE_TAG}" \ docker compose \ -p "${COMPOSE_PROJECT}" \ -f "${COMPOSE_FILE}" \ up -d \ --pull always \ --remove-orphans echo "Attente du backend ${DEPLOY_ENV}..." for i in $(seq 1 30); do if docker run --rm \ --network "${DEPLOY_NETWORK}" \ curlimages/curl:8.12.1 \ -fsS \ http://backend:5000/ \ >/dev/null; then echo "Backend ${DEPLOY_ENV} opérationnel." break fi if [ "$i" -eq 30 ]; then echo "Le backend ${DEPLOY_ENV} ne répond pas." docker compose \ -p "${COMPOSE_PROJECT}" \ -f "${COMPOSE_FILE}" \ ps docker compose \ -p "${COMPOSE_PROJECT}" \ -f "${COMPOSE_FILE}" \ logs \ --tail=150 exit 1 fi sleep 5 done echo "Vérification du frontend ${DEPLOY_ENV}..." docker run --rm \ --network "${DEPLOY_NETWORK}" \ curlimages/curl:8.12.1 \ -fsS \ http://frontend/ \ >/dev/null echo "Frontend ${DEPLOY_ENV} opérationnel." ''' } } } post { always { sh ''' docker rm -f "${CI_DATABASE}" 2>/dev/null || true docker network rm "${CI_NETWORK}" 2>/dev/null || true ''' } success { echo """ Pipeline réussi. Branche : ${BRANCH_NAME} Environnement : ${DEPLOY_ENV} Version : ${IMAGE_TAG} """ } failure { echo """ Pipeline en échec. Branche : ${BRANCH_NAME} Environnement : ${DEPLOY_ENV} """ } } }