pipeline { agent any options { skipDefaultCheckout(true) } parameters { booleanParam(name: 'RUN_FULL_E2E', defaultValue: false, description: '是否在部署后额外执行全量 E2E(失败仅标记 UNSTABLE)') } environment { APP_NAME = 'no-whatever' CI_IMAGE = 'mcr.microsoft.com/playwright:v1.51.1-jammy' NPM_CACHE_VOLUME = 'no-whatever-npm-cache' NODE_MODULES_VOLUME = 'no-whatever-node-modules' NPM_LOCK_HASH_FILE = '/npm-cache/no-whatever-package-lock.sha256' NPM_REGISTRY = 'https://registry.npmmirror.com' } triggers { GenericTrigger(tokenCredentialId: 'no-whatever-deploy-token') } stages { stage('Checkout') { steps { checkout scm } } stage('Runtime Check') { steps { sh 'docker --version' } } stage('Prepare Test Images') { options { timeout(time: 15, unit: 'MINUTES') } steps { retry(2) { sh ''' set -e if docker image inspect ${CI_IMAGE} >/dev/null 2>&1; then echo "${CI_IMAGE} already exists, skip pull" else docker pull ${CI_IMAGE} fi ''' } } } stage('CI Gate (Lint + TypeCheck + Unit + Smoke E2E)') { options { timeout(time: 30, unit: 'MINUTES') } steps { sh ''' set -e rm -rf playwright-report test-results cid=$(docker create --ipc=host \ -e HOME=/tmp \ -e NPM_CONFIG_CACHE=/npm-cache \ -e NPM_CONFIG_REGISTRY=${NPM_REGISTRY} \ -e NPM_LOCK_HASH_FILE=${NPM_LOCK_HASH_FILE} \ -v ${NPM_CACHE_VOLUME}:/npm-cache \ -v ${NODE_MODULES_VOLUME}:/workspace/node_modules \ ${CI_IMAGE} \ sh -lc 'set -e; cd /workspace; \ lock_hash=$(sha256sum package-lock.json | cut -d " " -f1); \ cached_hash=$(cat "$NPM_LOCK_HASH_FILE" 2>/dev/null || true); \ if [ "$lock_hash" = "$cached_hash" ] && [ -d node_modules ] && [ "$(ls -A node_modules 2>/dev/null)" ]; then \ echo "node_modules cache hit, skip npm ci"; \ else \ npm ci --prefer-offline --no-audit --progress=false; \ printf "%s" "$lock_hash" > "$NPM_LOCK_HASH_FILE"; \ fi; \ npm run lint; npx tsc --noEmit; npm run test:coverage; npm run test:e2e:smoke') cleanup() { docker rm -f "$cid" >/dev/null 2>&1 || true } trap cleanup EXIT git archive --format=tar HEAD | docker cp - "$cid":/workspace set +e docker start -a "$cid" status=$? set -e docker cp "$cid":/workspace/playwright-report ./playwright-report 2>/dev/null || true docker cp "$cid":/workspace/test-results ./test-results 2>/dev/null || true exit $status ''' } post { always { archiveArtifacts artifacts: 'playwright-report/**,test-results/**', allowEmptyArchive: true } } } stage('Build Docker Image') { steps { withCredentials([ string(credentialsId: 'amap-api-key', variable: 'AMAP_KEY') ]) { sh "docker build --build-arg NEXT_PUBLIC_AMAP_API_KEY=${AMAP_KEY} -t ${APP_NAME}:${BUILD_NUMBER} -t ${APP_NAME}:latest ." } } } stage('Deploy') { steps { withCredentials([ string(credentialsId: 'amap-api-key', variable: 'AMAP_KEY'), string(credentialsId: 'deepseek-api-key', variable: 'DEEPSEEK_KEY') ]) { sh """ docker stop ${APP_NAME} || true docker rm ${APP_NAME} || true mkdir -p /data/${APP_NAME} chown 1001:1001 /data/${APP_NAME} docker run -d \ --name ${APP_NAME} \ --network nginx \ -p 3721:3721 \ -v /data/${APP_NAME}:/app/data \ -e DATABASE_URL=file:/app/data/prod.db \ -e AMAP_API_KEY=${AMAP_KEY} \ -e DEEPSEEK_API_KEY=${DEEPSEEK_KEY} \ --restart unless-stopped \ ${APP_NAME}:latest """ } } } stage('Full E2E (Optional, Non-Blocking)') { when { expression { return params.RUN_FULL_E2E } } options { timeout(time: 30, unit: 'MINUTES') } steps { catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') { sh ''' set -e rm -rf playwright-report-full test-results-full cid=$(docker create --ipc=host \ -e HOME=/tmp \ -e NPM_CONFIG_CACHE=/npm-cache \ -e NPM_CONFIG_REGISTRY=${NPM_REGISTRY} \ -e NPM_LOCK_HASH_FILE=${NPM_LOCK_HASH_FILE} \ -v ${NPM_CACHE_VOLUME}:/npm-cache \ -v ${NODE_MODULES_VOLUME}:/workspace/node_modules \ ${CI_IMAGE} \ sh -lc 'set -e; cd /workspace; \ lock_hash=$(sha256sum package-lock.json | cut -d " " -f1); \ cached_hash=$(cat "$NPM_LOCK_HASH_FILE" 2>/dev/null || true); \ if [ "$lock_hash" = "$cached_hash" ] && [ -d node_modules ] && [ "$(ls -A node_modules 2>/dev/null)" ]; then \ echo "node_modules cache hit, skip npm ci"; \ else \ npm ci --prefer-offline --no-audit --progress=false; \ printf "%s" "$lock_hash" > "$NPM_LOCK_HASH_FILE"; \ fi; \ npm run test:e2e') cleanup() { docker rm -f "$cid" >/dev/null 2>&1 || true } trap cleanup EXIT git archive --format=tar HEAD | docker cp - "$cid":/workspace set +e docker start -a "$cid" status=$? set -e docker cp "$cid":/workspace/playwright-report ./playwright-report-full 2>/dev/null || true docker cp "$cid":/workspace/test-results ./test-results-full 2>/dev/null || true exit $status ''' } } post { always { archiveArtifacts artifacts: 'playwright-report-full/**,test-results-full/**', allowEmptyArchive: true } } } } post { success { echo "Deployed ${APP_NAME} build #${BUILD_NUMBER} successfully" } failure { echo "Build #${BUILD_NUMBER} failed" } } }