@@ -1,3 +1,10 @@
|
||||
# Only used by compose.deploy.yaml (image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}).
|
||||
# Jenkins publishes IMAGE_TAG as "<BUILD_NUMBER>-<git short sha>"; match the
|
||||
# tag of the build you intend to deploy.
|
||||
REGISTRY=git.jerryyan.net
|
||||
IMAGE_NAME=q792602257/agentic-mobile-control
|
||||
IMAGE_TAG=latest
|
||||
|
||||
POSTGRES_DB=device_cloud
|
||||
POSTGRES_USER=device_cloud
|
||||
POSTGRES_PASSWORD=change-me-database-password
|
||||
|
||||
@@ -7,6 +7,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . .
|
||||
RUN uv sync --locked --all-packages --no-dev
|
||||
|
||||
|
||||
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
// 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')
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
// 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.14-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 {
|
||||
def img = docker.build("${FULL_IMAGE}:${IMAGE_TAG}", '.')
|
||||
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.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
restart: unless-stopped
|
||||
|
||||
cloud-api:
|
||||
image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
alembic -c packages/cloud-platform/cloud/migrations/alembic.ini upgrade head
|
||||
&& exec device-cloud-api --host 0.0.0.0 --port 8001
|
||||
environment:
|
||||
CLOUD_ENVIRONMENT: production
|
||||
CLOUD_DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
||||
CLOUD_PUBLIC_CREDENTIALS_JSON: ${CLOUD_PUBLIC_CREDENTIALS_JSON}
|
||||
CLOUD_HOST_CREDENTIALS_JSON: ${CLOUD_HOST_CREDENTIALS_JSON}
|
||||
CLOUD_SCHEDULER_INTERVAL_SECONDS: ${CLOUD_SCHEDULER_INTERVAL_SECONDS:-1}
|
||||
CLOUD_LEASE_REAPER_INTERVAL_SECONDS: ${CLOUD_LEASE_REAPER_INTERVAL_SECONDS:-5}
|
||||
CLOUD_LEASE_DURATION_SECONDS: ${CLOUD_LEASE_DURATION_SECONDS:-60}
|
||||
CLOUD_MAX_TASK_ATTEMPTS: ${CLOUD_MAX_TASK_ATTEMPTS:-3}
|
||||
ports:
|
||||
- "${CLOUD_API_PORT:-8001}:8001"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8001/health/ready"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
restart: unless-stopped
|
||||
|
||||
host-agent:
|
||||
image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
|
||||
command: ["device-host-agent"]
|
||||
environment:
|
||||
HOST_AGENT_CONTROL_PLANE_URL: http://cloud-api:8001
|
||||
HOST_AGENT_HOST_ID: ${HOST_AGENT_HOST_ID}
|
||||
HOST_AGENT_TOKEN: ${HOST_AGENT_TOKEN}
|
||||
HOST_AGENT_HEARTBEAT_INTERVAL_SECONDS: ${HOST_AGENT_HEARTBEAT_INTERVAL_SECONDS:-30}
|
||||
HOST_AGENT_POLL_TIMEOUT_SECONDS: ${HOST_AGENT_POLL_TIMEOUT_SECONDS:-20}
|
||||
HOST_AGENT_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_RETRY_BACKOFF_SECONDS:-1}
|
||||
HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS: ${HOST_AGENT_MAX_RETRY_BACKOFF_SECONDS:-30}
|
||||
HOST_AGENT_MAX_RETRY_ATTEMPTS: ${HOST_AGENT_MAX_RETRY_ATTEMPTS:-5}
|
||||
AI_PLANNER_ENABLED: ${AI_PLANNER_ENABLED:-false}
|
||||
AI_PLANNER_PROVIDER: ${AI_PLANNER_PROVIDER:-anthropic}
|
||||
AI_PLANNER_MODEL: ${AI_PLANNER_MODEL:-}
|
||||
AI_PLANNER_TIMEOUT_SECONDS: ${AI_PLANNER_TIMEOUT_SECONDS:-30}
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
||||
volumes:
|
||||
- ${HOST_AGENT_TASKS_PATH:-./tasks}:/app/tasks
|
||||
depends_on:
|
||||
cloud-api:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
+1
-7
@@ -38,13 +38,7 @@ services:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- python
|
||||
- -c
|
||||
- >-
|
||||
import urllib.request;
|
||||
urllib.request.urlopen('http://127.0.0.1:8001/health/ready', timeout=2)
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8001/health/ready"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
|
||||
@@ -59,6 +59,26 @@ migrations, then starts the API on port `8001`. Readiness is available at:
|
||||
GET http://127.0.0.1:8001/health/ready
|
||||
```
|
||||
|
||||
## Deploying A Jenkins-Built Image
|
||||
|
||||
`docker compose up --build` above builds the image from source on the target
|
||||
host. For environments that pull a pre-built image instead, `Jenkinsfile` runs
|
||||
the pytest suite (`-m "not integration"`), builds the image from the same
|
||||
`Dockerfile`, smoke-tests both console scripts (`device-cloud-api --help`,
|
||||
`device-host-agent --help`), and pushes `<REGISTRY>/<IMAGE_NAME>:<BUILD_NUMBER>-<git short sha>`
|
||||
plus `:latest` to the configured registry.
|
||||
|
||||
`compose.deploy.yaml` is the same three-service stack as `compose.yaml`
|
||||
except `cloud-api` and `host-agent` reference `image:` instead of `build:`.
|
||||
Set `REGISTRY`, `IMAGE_NAME`, and `IMAGE_TAG` (see `.env.example`) to the tag
|
||||
Jenkins published, then deploy without a local build step:
|
||||
|
||||
```bash
|
||||
docker compose -f compose.deploy.yaml pull
|
||||
docker compose -f compose.deploy.yaml up -d
|
||||
docker compose -f compose.deploy.yaml ps
|
||||
```
|
||||
|
||||
For a deployment that manages processes outside Compose, apply migrations
|
||||
before starting the new Cloud API version:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user