pipeline { agent any options { buildDiscarder(logRotator(numToKeepStr: '10')) timestamps() disableConcurrentBuilds() } // 设计原则: // - 无 parameters 块,所以"Build Now"直接跑、不弹窗 // - 环境(APP_ENV) 与 分支(BRANCH) 都从 Jenkins 自动注入的 GIT_BRANCH 推断 // GIT_BRANCH 来自每个 Job 的 SCM 配置(Branch Specifier *​/dev、*​/sit、*​/uat 之一) // - 因此每台机器上 Job 唯一要配的就是 Branch Specifier,三台机器的 Jenkinsfile 内容完全一致 stages { stage('Resolve Environment') { // 从 Jenkins 注入的 GIT_BRANCH 推断 APP_ENV // GIT_BRANCH 形如 "origin/uat" / "uat" / "refs/heads/uat",统一去前缀 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'])) { error """ ============ 无法识别当前部署环境 ============ Jenkins 注入的 GIT_BRANCH = '${env.GIT_BRANCH}' 解析后的分支名 = '${branch}' 期望分支必须是 dev / sit / uat 之一。 请检查 Job 配置中的 Branch Specifier 是否正确: DEV 服务器 → */dev SIT 服务器 → */sit UAT 服务器 → */uat ============================================= """.stripIndent() } // 把推断结果写回 env,后续 stage 用 env.APP_ENV 而不是 params.APP_ENV env.APP_ENV = branch env.BRANCH = branch // 派生命名规则(容器名/网络/日志/镜像 TAG 全部以 APP_ENV 区分) 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}" env.LOG_ROOT = "/docker/python-${env.APP_ENV}/logs" } } } stage('Show Build Info') { steps { echo "============================================================" echo " 部署环境 : ${env.APP_ENV} (来自 GIT_BRANCH=${env.GIT_BRANCH})" echo " 部署分支 : ${env.BRANCH}" echo " 镜像 TAG : ${env.IMAGE_TAG}" echo " 容器网络 : ${env.DOCKER_NET}" echo " 日志根目录: ${env.LOG_ROOT}" echo "============================================================" } } stage('Load Env Port Mapping') { // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用 steps { script { def envFile = ".env.${env.APP_ENV}" if (!fileExists(envFile)) { error "缺少环境配置文件: ${envFile}(仓库里应该有这份;请检查 .gitignore 是否误屏蔽)" } def gatewayPort = sh( script: "grep -E '^GATEWAY_PORT=' ${envFile} | head -n1 | cut -d= -f2 | tr -d '\"' | tr -d \"'\"", returnStdout: true ).trim() if (!gatewayPort) { gatewayPort = "33333" } env.GATEWAY_PORT = gatewayPort echo "从 ${envFile} 解析到 GATEWAY_PORT=${env.GATEWAY_PORT}" } } } stage('Build Images') { steps { script { def buildArgs = "--build-arg APP_ENV=${env.APP_ENV}" sh "docker build ${buildArgs} -f alien_store/Dockerfile -t ${env.IMAGE_STORE} ." sh "docker build ${buildArgs} -f alien_gateway/Dockerfile -t ${env.IMAGE_GATEWAY} ." sh "docker build ${buildArgs} -f alien_contract/Dockerfile -t ${env.IMAGE_CONTRACT} ." } } } stage('Deploy') { steps { script { echo ">>> [${env.APP_ENV}] 部署镜像: ${env.IMAGE_STORE} / ${env.IMAGE_GATEWAY} / ${env.IMAGE_CONTRACT}" sh """ set -e docker network create ${env.DOCKER_NET} 2>/dev/null || true mkdir -p ${env.LOG_ROOT}/store ${env.LOG_ROOT}/gateway ${env.LOG_ROOT}/contract docker rm -f ${env.CONTAINER_NAME_STORE} ${env.CONTAINER_NAME_GATEWAY} ${env.CONTAINER_NAME_CONTRACT} 2>/dev/null || true # 1) 下游:store # APP_ENV=${env.APP_ENV} 让 config.py 加载镜像内的 .env.${env.APP_ENV} docker run -d --name ${env.CONTAINER_NAME_STORE} \\ --network ${env.DOCKER_NET} \\ -e APP_ENV=${env.APP_ENV} \\ -v ${env.LOG_ROOT}/store:/app/common/logs/alien_store \\ --restart unless-stopped \\ ${env.IMAGE_STORE} # 2) 下游:contract docker run -d --name ${env.CONTAINER_NAME_CONTRACT} \\ --network ${env.DOCKER_NET} \\ -e APP_ENV=${env.APP_ENV} \\ -v ${env.LOG_ROOT}/contract:/app/common/logs/alien_contract \\ --restart unless-stopped \\ ${env.IMAGE_CONTRACT} # 3) 网关:gateway(唯一对外端口) # -e GATEWAY_PORT=... 是关键:必须覆盖 Dockerfile 的默认 33333, # 否则容器内 uvicorn 会监听默认端口,导致与宿主机映射端口对不上 # pydantic-settings 也是环境变量优先于 .env 文件 docker run -d --name ${env.CONTAINER_NAME_GATEWAY} \\ --network ${env.DOCKER_NET} \\ -p ${env.GATEWAY_PORT}:${env.GATEWAY_PORT} \\ -e APP_ENV=${env.APP_ENV} \\ -e GATEWAY_PORT=${env.GATEWAY_PORT} \\ -e STORE_BASE_URL=http://${env.CONTAINER_NAME_STORE}:8001 \\ -e CONTRACT_BASE_URL=http://${env.CONTAINER_NAME_CONTRACT}:8002 \\ -v ${env.LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\ --restart unless-stopped \\ ${env.IMAGE_GATEWAY} """ } } } stage('Smoke Test') { // 注意:Jenkins 本身可能跑在 Docker 容器里,无法直接 curl 宿主机端口。 // 这里采取两层验证: // 1) 硬验证:3 个业务容器必须都在 running 状态(不通过则 fail) // 2) 软验证:通过 docker exec 进入 gateway 容器内部跑 /health 自检 // (HTTP 失败只 warn,不让 Pipeline 失败,因为这只代表 Jenkins 的网络位置看不到, // 不代表服务对外不可用,需要人工从其他位置验证) steps { script { sleep(time: 5, unit: 'SECONDS') def containers = [ env.CONTAINER_NAME_STORE, env.CONTAINER_NAME_CONTRACT, env.CONTAINER_NAME_GATEWAY ] 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 sh "docker logs --tail 50 ${c} || true" } } if (!allRunning) { error "存在容器未处于 running 状态,部署失败" } echo "✓ 3 个业务容器均在 running 状态" def healthCmd = """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())'""" def rc = sh(returnStatus: true, script: healthCmd) if (rc == 0) { echo "✓ gateway /health 通过" } else { echo "⚠️ gateway /health 未通过(容器在跑但 HTTP 自检失败,请从外部手动验证:curl http://:${env.GATEWAY_PORT}/health)" } } } } } post { success { echo "[${env.APP_ENV}] 环境部署成功!请从外部访问 http://:${env.GATEWAY_PORT}/health 验证" } failure { echo "[${env.APP_ENV}] 环境部署失败,请检查上面日志。" } always { cleanWs() } } }