// Jenkinsfile —— jp-rakuten(乐天 / ラクマ 抓取服务) // // 默认 registry: git.jerryyan.net/jp/rakuten-api // 构建参数说明见各 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: 'IMAGE_TAG', defaultValue: '', description: '自定义 tag;留空则用 -') booleanParam(name: 'SKIP_TEST', defaultValue: false, description: '跳过单测(紧急发版用,正常构建不要勾)') booleanParam(name: 'PUSH_LATEST', defaultValue: true, description: '同时打 :latest 并推送') } 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 agent { docker { image 'registry.jerryyan.top/library/python:3.13-slim' reuseNode true } } steps { sh ''' set -e # 阿里云 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') { steps { script { // 同时打两个 tag:精确 tag + 可选 latest def tags = ["-t ${env.IMAGE_NAME}:${env.IMAGE_TAG}"] if (params.PUSH_LATEST) { tags << "-t ${env.IMAGE_NAME}:latest" } sh """ docker build ${tags.join(' ')} \ --label org.opencontainers.image.revision=${env.GIT_SHA} \ --label org.opencontainers.image.version=${env.IMAGE_TAG} \ -f Dockerfile . """ } } } stage('Push image') { // 只在 master / 手动构建时推送;PR 构建只产镜像不推 when { anyOf { branch 'master' branch 'main' buildingTag() expression { return !params.IMAGE_TAG?.trim().isEmpty() && params.IMAGE_TAG != "${env.BUILD_NUMBER}-${env.GIT_SHA}" } } } 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 docker push ${env.IMAGE_NAME}:${env.IMAGE_TAG} """ script { if (params.PUSH_LATEST) { sh "docker push ${env.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 """ } success { echo "构建成功:${env.IMAGE_NAME}:${env.IMAGE_TAG}" } failure { echo "构建失败,请查看上方日志" } } }