// Jenkinsfile —— jp-rakuten(抓取服务 + 有状态端) // // 一次构建出两个镜像,两者用途不同、不能互相替代: // git.jerryyan.net/jp/rakuten-api ← Dockerfile 抓取服务(无状态可多开) // git.jerryyan.net/jp/rakuten-trading ← Dockerfile.trading 有状态端(下单购买+订单查询) // 有状态端单独出镜像是因为下单/结算必须用**有头** Chromium(headless 会让结算 SPA // 失灵),镜像里要带 Xvfb + 日文字体 + 可选 x11vnc,抓取镜像没有这些。 // 网关(app.gateway.main)复用 rakuten-trading 镜像,只是换 command。 // // 构建参数说明见各 stage 顶部;首次使用前请在 Jenkins 里配置凭据: // - git-credentials : 拉代码的 SSH / HTTPS 凭据 // - gitea-registry : docker login git.jerryyan.net 的凭据(username/password) pipeline { agent any options { timestamps() timeout(time: 30, unit: 'MINUTES') disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '20', daysToKeepStr: '14')) } parameters { string(name: 'IMAGE_NAME', defaultValue: 'git.jerryyan.net/jp/rakuten-api', description: '抓取服务镜像完整名(含 registry host)') string(name: 'TRADING_IMAGE_NAME', defaultValue: 'git.jerryyan.net/jp/rakuten-trading', description: '有状态端(下单+订单查询,含网关)镜像完整名') string(name: 'IMAGE_TAG', defaultValue: '', description: '自定义 tag;留空则用 -。两个镜像共用同一 tag') booleanParam(name: 'BUILD_SCRAPING', defaultValue: true, description: '构建并推送抓取服务镜像') booleanParam(name: 'BUILD_TRADING', defaultValue: true, description: '构建并推送有状态端镜像(体积大:含 Chromium + Xvfb,约 +700MB)') booleanParam(name: 'SKIP_TEST', defaultValue: false, description: '跳过单测(紧急发版用,正常构建不要勾)') } environment { // 让 docker build 看到 BuildKit 缓存挂载等特性 DOCKER_BUILDKIT = '1' // 禁掉 pip 警告、Jenkins 写 .pyc PYTHONUNBUFFERED = '1' PYTHONDONTWRITEBYTECODE = '1' // 国内镜像:uv / pip 拉包走阿里云 PyPI 镜像,避免拉取缓慢 UV_INDEX_URL = 'https://mirrors.aliyun.com/pypi/simple/' } stages { stage('Checkout') { steps { checkout scm script { env.GIT_SHA = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() env.GIT_TAG = sh(script: 'git describe --tags --always --dirty 2>/dev/null || echo ""', returnStdout: true).trim() if (!params.IMAGE_TAG?.trim()) { env.IMAGE_TAG = "${env.BUILD_NUMBER}-${env.GIT_SHA}" } echo "镜像目标: ${env.IMAGE_NAME}:${env.IMAGE_TAG}(git=${env.GIT_SHA})" } } } stage('Test') { // 跳过逻辑:SKIP_TEST=true 时整个 stage 不跑 when { expression { return !params.SKIP_TEST } } // 在官方 Python 镜像里跑测试,避免污染宿主机;只装 dev extra,不装 browser // 用 -u root 是因为 Jenkins 默认把容器用户映射到 jenkins uid,HOME=/, // pip user-site 会解析到 /.local 直接报错;测试容器跑完即销毁,跑 root 无副作用 agent { docker { image 'registry.jerryyan.top/library/python:3.13-slim' reuseNode true args '-u root:root' } } steps { sh ''' set -e export HOME=/root export PIP_CACHE_DIR=/root/.cache/pip # 阿里云 PyPI 镜像,UV_INDEX_URL 由 pipeline environment 注入 python -m pip install --quiet --upgrade pip -i "$UV_INDEX_URL" pip install --quiet uv -i "$UV_INDEX_URL" uv sync --frozen --extra dev .venv/bin/python -m pytest -q ''' } } stage('Build image: scraping') { when { expression { return params.BUILD_SCRAPING } } steps { sh """ docker build \ -t ${env.IMAGE_NAME}:${env.IMAGE_TAG} \ -t ${env.IMAGE_NAME}:latest \ --label org.opencontainers.image.revision=${env.GIT_SHA} \ --label org.opencontainers.image.version=${env.IMAGE_TAG} \ -f Dockerfile . """ } } stage('Build image: trading') { when { expression { return params.BUILD_TRADING } } // 有状态端镜像:Chromium + Xvfb + 日文字体,比抓取镜像明显大也明显慢, // 首次构建(无缓存)拉 Chromium 二进制约 300MB steps { sh """ docker build \ -t ${params.TRADING_IMAGE_NAME}:${env.IMAGE_TAG} \ -t ${params.TRADING_IMAGE_NAME}:latest \ --label org.opencontainers.image.revision=${env.GIT_SHA} \ --label org.opencontainers.image.version=${env.IMAGE_TAG} \ -f Dockerfile.trading . """ } } stage('Push images') { when { expression { return params.BUILD_SCRAPING || params.BUILD_TRADING } } steps { withCredentials([usernamePassword( credentialsId: 'gitea-registry', usernameVariable: 'REGISTRY_USER', passwordVariable: 'REGISTRY_PASS', )]) { sh 'echo "$REGISTRY_PASS" | docker login git.jerryyan.net -u "$REGISTRY_USER" --password-stdin' script { if (params.BUILD_SCRAPING) { sh """ docker push ${env.IMAGE_NAME}:${env.IMAGE_TAG} docker push ${env.IMAGE_NAME}:latest """ } if (params.BUILD_TRADING) { sh """ docker push ${params.TRADING_IMAGE_NAME}:${env.IMAGE_TAG} docker push ${params.TRADING_IMAGE_NAME}:latest """ } } } } } } post { always { // 清理本次构建产生的镜像,避免 Jenkins agent 磁盘膨胀 sh """ docker rmi -f ${env.IMAGE_NAME}:${env.IMAGE_TAG} 2>/dev/null || true docker rmi -f ${env.IMAGE_NAME}:latest 2>/dev/null || true docker rmi -f ${params.TRADING_IMAGE_NAME}:${env.IMAGE_TAG} 2>/dev/null || true docker rmi -f ${params.TRADING_IMAGE_NAME}:latest 2>/dev/null || true """ } success { echo "构建成功:tag=${env.IMAGE_TAG}" + (params.BUILD_SCRAPING ? " ${env.IMAGE_NAME}" : "") + (params.BUILD_TRADING ? " ${params.TRADING_IMAGE_NAME}" : "") } failure { echo "构建失败,请查看上方日志" } } }