/** * alien_py_cloud 统一部署流水线(dev / sit / uat / produ 共用) * * 设计原则: * - 无 parameters,Build Now 直接跑 * - APP_ENV 由 Jenkins SCM 的 GIT_BRANCH 自动推断(分支 dev / sit / uat / produ) * - 端口、SSH 目标等环境差异全部来自 .env.${APP_ENV},代码与 Jenkinsfile 四分支一致 * - dev/sit/uat:Jenkins 本机 docker build + run * - produ:SSH 到 DEPLOY_SSH_TARGET 远程 build + run(目标见 .env.produ) */ def PROD_SSH_CREDENTIALS_ID = 'e611a045-2fdc-4613-babd-a72d69bf9814' def readEnvVar(String envFile, String key, String defaultValue = '') { def val = sh( script: "grep -E '^${key}=' ${envFile} | head -n1 | cut -d= -f2- | tr -d '\"' | tr -d \"'\" | xargs || true", returnStdout: true ).trim() return val ?: defaultValue } pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '10')) timestamps() disableConcurrentBuilds() } stages { stage('Resolve Environment') { steps { script { def raw = env.GIT_BRANCH ?: env.BRANCH_NAME ?: '' def branch = raw.replaceFirst('^origin/', '').replaceFirst('^refs/heads/', '').trim() if (!(branch in ['dev', 'sit', 'uat', 'produ'])) { error """ ============ 无法识别当前部署环境 ============ Jenkins 注入的 GIT_BRANCH = '${env.GIT_BRANCH}' 解析后的分支名 = '${branch}' 期望分支必须是 dev / sit / uat / produ 之一。 请检查 Job 配置中的 Branch Specifier: DEV → origin/dev 或 dev SIT → origin/sit 或 sit UAT → origin/uat 或 uat PRODU → origin/produ 或 produ ============================================= """.stripIndent() } env.APP_ENV = branch env.BRANCH = branch def envFile = ".env.${env.APP_ENV}" if (!fileExists(envFile)) { error "缺少环境配置文件: ${envFile}" } env.STORE_PORT = readEnvVar(envFile, 'STORE_PORT', '8001') env.GATEWAY_PORT = readEnvVar(envFile, 'GATEWAY_PORT', '33333') env.CONTRACT_PORT = readEnvVar(envFile, 'CONTRACT_PORT', '8002') env.GATEWAY_HOST_PORT = readEnvVar(envFile, 'GATEWAY_HOST_PORT', env.GATEWAY_PORT) env.CONTRACT_HOST_PORT = readEnvVar(envFile, 'CONTRACT_HOST_PORT', '') env.DEPLOY_MODE = (env.APP_ENV == 'produ') ? 'ssh' : 'local' env.IMAGE_TAG = "${env.APP_ENV}-${env.BUILD_NUMBER}" env.IMAGE_STORE = "alien_store:${env.IMAGE_TAG}" env.IMAGE_GATEWAY = "alien_gateway:${env.IMAGE_TAG}" env.IMAGE_CONTRACT = "alien_contract:${env.IMAGE_TAG}" env.CONTAINER_NAME_STORE = "alien_store_py-${env.APP_ENV}" env.CONTAINER_NAME_GATEWAY = "alien_gateway_py-${env.APP_ENV}" env.CONTAINER_NAME_CONTRACT = "alien_contract_py-${env.APP_ENV}" env.DOCKER_NET = "alien_net_${env.APP_ENV}" if (env.DEPLOY_MODE == 'ssh') { env.SSH_TARGET = readEnvVar(envFile, 'DEPLOY_SSH_TARGET') env.CODE_DIR_REMOTE = readEnvVar(envFile, 'DEPLOY_CODE_DIR') env.LOG_ROOT = readEnvVar(envFile, 'DEPLOY_LOG_ROOT') env.ENV_FILE_REMOTE = "${env.CODE_DIR_REMOTE}/.env.${env.APP_ENV}" if (!env.SSH_TARGET || !env.CODE_DIR_REMOTE) { error ".env.produ 缺少 DEPLOY_SSH_TARGET 或 DEPLOY_CODE_DIR" } if (!env.LOG_ROOT) { env.LOG_ROOT = "${env.CODE_DIR_REMOTE}/logs" } } else { env.LOG_ROOT = "/docker/python-${env.APP_ENV}/logs" } echo "APP_ENV=${env.APP_ENV} DEPLOY_MODE=${env.DEPLOY_MODE}" echo "端口 store=${env.STORE_PORT} gateway=${env.GATEWAY_PORT} host=${env.GATEWAY_HOST_PORT} contract=${env.CONTRACT_PORT}" } } } stage('Show Build Info') { steps { script { echo "============================================================" echo " 部署环境 : ${env.APP_ENV} (GIT_BRANCH=${env.GIT_BRANCH})" echo " 部署模式 : ${env.DEPLOY_MODE}" echo " 镜像 TAG : ${env.IMAGE_TAG}" echo " 容器网络 : ${env.DOCKER_NET}" echo " 日志目录 : ${env.LOG_ROOT}" if (env.DEPLOY_MODE == 'ssh') { echo " SSH 目标 : ${env.SSH_TARGET}" echo " 远程目录 : ${env.CODE_DIR_REMOTE}" } echo "============================================================" } } } stage('Verify SSH') { when { expression { env.DEPLOY_MODE == 'ssh' } } steps { sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) { sh """ set -e ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\ 'test -f ${env.ENV_FILE_REMOTE} && sudo docker info >/dev/null' echo ">>> SSH OK: ${env.SSH_TARGET}" """ } } } stage('Sync Code') { when { expression { env.DEPLOY_MODE == 'ssh' } } steps { sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) { sh """ set -e ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF' set -e cd '${env.CODE_DIR_REMOTE}' if [ ! -d .git ]; then echo "ERROR: ${env.CODE_DIR_REMOTE} is not a git repo" exit 1 fi git fetch origin git checkout '${env.BRANCH}' git reset --hard origin/'${env.BRANCH}' echo ">>> git at \$(git rev-parse --short HEAD) on \$(hostname)" REMOTE_EOF """ } } } stage('Build Images') { steps { script { def buildCmds = """ sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\ -f alien_store/Dockerfile -t ${env.IMAGE_STORE} . sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\ -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} . sudo docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\ -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} . """.stripIndent() if (env.DEPLOY_MODE == 'ssh') { sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) { sh """ set -e ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF' set -e cd '${env.CODE_DIR_REMOTE}' ${buildCmds} echo ">>> images built on \$(hostname)" REMOTE_EOF """ } } else { sh """ set -e docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg STORE_PORT=${env.STORE_PORT} \\ -f alien_store/Dockerfile -t ${env.IMAGE_STORE} . docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg GATEWAY_PORT=${env.GATEWAY_PORT} \\ -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} . docker build --build-arg APP_ENV=${env.APP_ENV} --build-arg CONTRACT_PORT=${env.CONTRACT_PORT} \\ -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} . """ } } } } stage('Deploy') { steps { script { def dockerBin = (env.DEPLOY_MODE == 'ssh') ? 'sudo docker' : 'docker' def sshEnvLines = (env.DEPLOY_MODE == 'ssh') ? " --env-file ${env.ENV_FILE_REMOTE} \\\n -v ${env.ENV_FILE_REMOTE}:/app/.env.${env.APP_ENV}:ro \\\n" : '' def contractPublishLine = env.CONTRACT_HOST_PORT?.trim() ? " -p ${env.CONTRACT_HOST_PORT}:${env.CONTRACT_PORT} \\\n" : '' def legacyCleanup = (env.APP_ENV == 'produ') ? """ ${dockerBin} rm -f py_esign_produ py_gateway_produ py_contract_produ esign alien_gateway_py alien_contract_py \\ alien_store_produ alien_gateway_produ alien_contract_produ 2>/dev/null || true """ : '' def deployScript = """ set -e ${dockerBin} network create ${env.DOCKER_NET} 2>/dev/null || true mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract ${dockerBin} rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true ${legacyCleanup} ${dockerBin} run -d --name ${env.CONTAINER_NAME_STORE} \\ --network ${env.DOCKER_NET} \\ ${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\ -e STORE_PORT=${env.STORE_PORT} \\ -v ${env.LOG_ROOT}/store:/app/common/logs/alien_store \\ --restart unless-stopped \\ ${env.IMAGE_STORE} ${dockerBin} run -d --name ${env.CONTAINER_NAME_CONTRACT} \\ --network ${env.DOCKER_NET} \\ ${contractPublishLine}${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\ -e CONTRACT_PORT=${env.CONTRACT_PORT} \\ -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\ --restart unless-stopped \\ ${env.IMAGE_CONTRACT} ${dockerBin} run -d --name ${env.CONTAINER_NAME_GATEWAY} \\ --network ${env.DOCKER_NET} \\ -p ${env.GATEWAY_HOST_PORT}:${env.GATEWAY_PORT} \\ ${sshEnvLines} -e APP_ENV=${env.APP_ENV} \\ -e GATEWAY_PORT=${env.GATEWAY_PORT} \\ -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:${env.STORE_PORT} \\ -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:${env.CONTRACT_PORT} \\ -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\ --restart unless-stopped \\ ${env.IMAGE_GATEWAY} """ if (env.DEPLOY_MODE == 'ssh') { sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) { sh """ set -e ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' bash -s <<'REMOTE_EOF' ${deployScript} REMOTE_EOF """ } } else { sh deployScript } } } } stage('Smoke Test') { steps { script { sleep(time: 5, unit: 'SECONDS') def containers = [ env.CONTAINER_NAME_STORE, env.CONTAINER_NAME_CONTRACT, env.CONTAINER_NAME_GATEWAY ] if (env.DEPLOY_MODE == 'ssh') { sshagent(credentials: [PROD_SSH_CREDENTIALS_ID]) { def allRunning = true containers.each { c -> def status = sh( returnStdout: true, script: """ ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\ "sudo docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing" """ ).trim() echo " ${c}: ${status}" if (status != 'running') { allRunning = false } } if (!allRunning) { error "存在容器未处于 running 状态,部署失败" } def rc = sh(returnStatus: true, script: """ ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new '${env.SSH_TARGET}' \\ "sudo docker exec ${env.CONTAINER_NAME_GATEWAY} python -c \\"import urllib.request as u; r=u.urlopen('http://localhost:${env.GATEWAY_PORT}/health', timeout=3); print('HTTP', r.status)\\"" """) if (rc == 0) { echo "✓ gateway /health 通过" } else { echo "⚠️ gateway /health 未通过,请手动验证:curl http://:${env.GATEWAY_HOST_PORT}/health" } } } else { def allRunning = true containers.each { c -> def status = sh( returnStdout: true, script: "docker inspect -f '{{.State.Status}}' ${c} 2>/dev/null || echo missing" ).trim() echo " ${c}: ${status}" if (status != 'running') { allRunning = false } } if (!allRunning) { error "存在容器未处于 running 状态,部署失败" } echo "✓ 3 个业务容器均在 running 状态" def rc = sh(returnStatus: true, script: """ docker exec ${env.CONTAINER_NAME_GATEWAY} python -c 'import urllib.request as u; r=u.urlopen("http://localhost:${env.GATEWAY_PORT}/health", timeout=3); print("HTTP", r.status, r.read(200).decode())' """) if (rc == 0) { echo "✓ gateway /health 通过" } else { echo "⚠️ gateway /health 未通过,请手动验证:curl http://:${env.GATEWAY_HOST_PORT}/health" } } } } } } post { success { echo "[${env.APP_ENV}] 部署成功!访问 http://:${env.GATEWAY_HOST_PORT}/health" } failure { echo "[${env.APP_ENV}] 部署失败,请检查日志。" } always { cleanWs() } } }