Files
q792602257 25ebc10a8a
Tests / Test passed: 789
feat: downgrade Python baseline to 3.13 for PaddleOCR compatibility
paddlepaddle has no Python 3.14 (cp314) wheel on PyPI, so host-agent
deployments on 3.14 can never install it, causing OCR to fail at
runtime with RuntimeError. Pin the workspace to Python 3.13 across
all pyproject.toml files, the Docker base image, and the Jenkins CI
image; regenerate uv.lock against 3.13.

Also fixes a pre-existing Python-2-style `except X, Y:` syntax error
(invalid in all Python 3.x) in runtime/task.py and
packages/cloud-platform/cloud/{sql_repository,internal_api/api}.py,
introduced in 22d37ca9 and unrelated to this change's scope, which
blocked the full test suite from collecting on any interpreter
version.

openspec change: downgrade-python-3-13-paddleocr
2026-07-14 18:05:49 +08:00

152 lines
6.1 KiB
Groovy

// Jenkins pipeline: test the Device Cloud Platform (Python / uv workspace),
// build the deployable Docker image (Cloud API + Host Agent share one image;
// the entrypoint is selected via the Compose `command`), smoke-test the
// container, and optionally push it to a registry.
//
// Requires the Docker Pipeline plugin and a Docker-capable agent.
pipeline {
agent any
options {
timestamps()
disableConcurrentBuilds()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
string(name: 'IMAGE_NAME', defaultValue: 'q792602257/agentic-mobile-control', description: 'Image repository name')
string(name: 'REGISTRY', defaultValue: 'git.jerryyan.net', description: 'Registry host, e.g. registry.example.com. Empty = local build only.')
string(name: 'UV_INDEX_URL', defaultValue: 'https://mirrors.aliyun.com/pypi/simple', description: 'Optional PyPI mirror index URL for uv sync (empty = uv default)')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip the uv sync/pytest stage (faster builds when only packaging)')
booleanParam(name: 'PUSH', defaultValue: true, description: 'Push the image to REGISTRY after a successful build')
// Mirror overrides passed to `docker build` as --build-arg. Defaults target
// CN networks so Jenkins builds don't time out pulling from npmjs.org /
// deb.debian.org / PyPI. Override or blank any of these to build against
// the official upstreams. Base images (node, uv) are pinned in the
// Dockerfile FROM lines and no longer overridable here.
string(name: 'NPM_REGISTRY', defaultValue: 'https://registry.npmmirror.com', description: 'npm registry URL used by `npm ci`')
string(name: 'APT_MIRROR', defaultValue: 'mirrors.aliyun.com', description: 'Debian apt mirror host (e.g. mirrors.aliyun.com). Empty = deb.debian.org')
}
environment {
// Short commit for a traceable, immutable tag alongside :latest.
GIT_SHA = sh(script: 'git rev-parse --short HEAD 2>/dev/null || echo unknown', returnStdout: true).trim()
IMAGE_TAG = "${env.BUILD_NUMBER}-${GIT_SHA}"
FULL_IMAGE = "${params.REGISTRY?.trim() ? params.REGISTRY.trim() + '/' : ''}${params.IMAGE_NAME}"
}
stages {
stage('Checkout') {
steps {
checkout scm
// Pre-create the host-side uv cache dir as the Jenkins user *before* the
// 'Test' stage's docker agent bind-mounts it. If this dir doesn't exist
// yet, Docker auto-creates it as root when the container starts, which
// then blocks writes from the non-root user the container runs as.
sh 'mkdir -p "$HOME/.cache/uv"'
}
}
// Root, cloud-api, and host-agent suites, excluding tests that need real
// hardware/PostgreSQL/device services (see tests/ pytest marker "integration").
stage('Test') {
when { expression { return !params.SKIP_TESTS } }
agent {
docker {
image 'registry-ghcr.jerryyan.top/astral-sh/uv:python3.13-bookworm-slim'
reuseNode true
// The Docker Pipeline plugin runs the container as the Jenkins host
// user (non-root), so /root isn't writable. Cache under /tmp, which is
// world-writable, and mount the persistent host cache dir there.
args '-v $HOME/.cache/uv:/tmp/uv-cache'
}
}
environment {
// Pin HOME/cache explicitly for the same reason as the cache mount above.
HOME = '/tmp'
UV_CACHE_DIR = '/tmp/uv-cache'
UV_LINK_MODE = 'copy'
UV_INDEX_URL = "${params.UV_INDEX_URL}"
}
steps {
sh 'uv sync --locked --all-packages'
sh '''
uv run pytest tests apps/cloud-api/tests apps/device-host-agent/tests \
-m "not integration" -ra --junit-xml=test-results.xml
'''
}
post {
always {
junit testResults: 'test-results.xml', allowEmptyResults: true
}
}
}
stage('Build image') {
steps {
script {
// Pass every mirror override through as --build-arg. Empty values
// are skipped so the Dockerfile ARG default applies.
def buildArgs = []
["NPM_REGISTRY", "APT_MIRROR", "UV_INDEX_URL"].each { name ->
def v = params[name]?.toString()?.trim()
if (v) {
buildArgs << "--build-arg ${name}=${v}"
}
}
def dockerArgs = (buildArgs.join(' ') + ' .').trim()
def img = docker.build("${FULL_IMAGE}:${IMAGE_TAG}", dockerArgs)
img.tag('latest')
env.BUILT_IMAGE = "${FULL_IMAGE}:${IMAGE_TAG}"
echo "Built ${env.BUILT_IMAGE} (+ :latest)"
}
}
}
// Confirm both packaged console scripts start up before shipping the image.
// A full boot needs PostgreSQL (cloud-api runs Alembic migrations on
// start), so this checks the entrypoints resolve and import cleanly
// instead of booting the real service.
stage('Smoke test') {
steps {
sh '''
set -eu
docker run --rm "${BUILT_IMAGE}" device-cloud-api --help >/dev/null
docker run --rm "${BUILT_IMAGE}" device-host-agent --help >/dev/null
echo "smoke test OK"
'''
}
}
stage('Push') {
when { expression { return params.PUSH } }
steps {
script {
def registryUrl = params.REGISTRY?.trim() ? "https://${params.REGISTRY.trim()}" : ''
docker.withRegistry(registryUrl, 'gitea-registry') {
def img = docker.image("${FULL_IMAGE}:${IMAGE_TAG}")
img.push()
img.push('latest')
}
echo "Pushed ${FULL_IMAGE}:${IMAGE_TAG} and :latest"
}
}
}
}
post {
always {
// Free local disk: drop this build's tag and any dangling layers.
sh '''
[ -n "${BUILT_IMAGE:-}" ] && docker rmi "${BUILT_IMAGE}" >/dev/null 2>&1 || true
docker image prune -f >/dev/null 2>&1 || true
'''
cleanWs()
}
success { echo "OK: ${env.FULL_IMAGE}:${env.IMAGE_TAG}" }
failure { echo 'Build failed — see stage logs above.' }
}
}