From df7c4ee0d731c128263fef7adf03809294d4e33c Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Mon, 22 Jun 2026 14:48:33 +0800 Subject: [PATCH] chore(release): add runtime image artifact deploy --- scripts/build_release_images.sh | 27 +++++++++ scripts/deploy_release_images_production.sh | 62 +++++++++++++++++++++ tests/unit/test_release_scripts.py | 16 ++++++ 3 files changed, 105 insertions(+) create mode 100755 scripts/build_release_images.sh create mode 100755 scripts/deploy_release_images_production.sh diff --git a/scripts/build_release_images.sh b/scripts/build_release_images.sh new file mode 100755 index 000000000..4bc7e3189 --- /dev/null +++ b/scripts/build_release_images.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +VERSION="${1:-${RELEASE_VERSION:-}}" +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + echo "Example: $0 v0.1.5" + exit 1 +fi + +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +OUTPUT_DIR="${OUTPUT_DIR:-$ROOT_DIR/dist/release-images}" +mkdir -p "$OUTPUT_DIR" + +API_IMAGE="xiaoxia-saas-api:$VERSION" +WORKER_IMAGE="xiaoxia-saas-worker:$VERSION" +API_LATEST="xiaoxia-saas-api:dev" +WORKER_LATEST="xiaoxia-saas-worker:dev" +TAR_PATH="$OUTPUT_DIR/xiaoxia-runtime-images-$VERSION.tar" + +cd "$ROOT_DIR" + +docker build --pull=false -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" . +docker build --pull=false -f infra/docker/worker.Dockerfile -t "$WORKER_IMAGE" -t "$WORKER_LATEST" . +docker save "$API_IMAGE" "$API_LATEST" "$WORKER_IMAGE" "$WORKER_LATEST" -o "$TAR_PATH" + +printf '%s\n' "$TAR_PATH" diff --git a/scripts/deploy_release_images_production.sh b/scripts/deploy_release_images_production.sh new file mode 100755 index 000000000..4129f1040 --- /dev/null +++ b/scripts/deploy_release_images_production.sh @@ -0,0 +1,62 @@ +#!/bin/sh +set -eu + +VERSION="${1:-${RELEASE_VERSION:-}}" +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + +IMAGE_TAR="${2:-${RUNTIME_IMAGE_TAR:-}}" +if [ -z "$IMAGE_TAR" ]; then + echo "Usage: $0 " + exit 1 +fi + +HOST_PREFIX="${HOST_PREFIX-/host}" +PRODUCTION_ROOT="$HOST_PREFIX/var/lib/xiaoxia-saas-production" +REPO_DIR="$PRODUCTION_ROOT/repo" +ENV_FILE="$PRODUCTION_ROOT/.env" +TARGET_TAR="$PRODUCTION_ROOT/runtime-images-$VERSION.tar" + +if [ ! -f "$IMAGE_TAR" ]; then + echo "Missing runtime image tar: $IMAGE_TAR" + exit 1 +fi + +if [ ! -d "$REPO_DIR" ]; then + echo "Missing production repo artifact: $REPO_DIR" + exit 1 +fi + +if [ ! -f "$ENV_FILE" ]; then + echo "Missing production env file: $ENV_FILE" + exit 1 +fi + +mkdir -p "$PRODUCTION_ROOT" +cp "$IMAGE_TAR" "$TARGET_TAR" +docker load -i "$TARGET_TAR" + +cd "$REPO_DIR/infra/docker" +export COMPOSE_PROJECT_NAME=xiaoxia-production-app +export WEB_DOCKERFILE=infra/docker/web-artifact.Dockerfile +export WEB_NGINX_CONF=infra/docker/nginx-production.conf +export API_IMAGE="xiaoxia-saas-api:$VERSION" +export WORKER_IMAGE="xiaoxia-saas-worker:$VERSION" +export ALLOW_PRODUCTION_BUILDS=false + +if [ ! -f "$REPO_DIR/apps/web/dist/index.html" ]; then + echo "Missing prebuilt web artifact: $REPO_DIR/apps/web/dist/index.html" + exit 1 +fi + +cp "$ENV_FILE" "$REPO_DIR/.env" +docker compose --env-file "$ENV_FILE" build --pull=false web +docker compose --env-file "$ENV_FILE" run --rm --no-deps api sh -c ' + cd /app && + python /app/scripts/validate_release_env.py --from-environ --strict-external && + alembic upgrade head +' +docker compose --env-file "$ENV_FILE" up -d api worker web +docker compose --env-file "$ENV_FILE" ps diff --git a/tests/unit/test_release_scripts.py b/tests/unit/test_release_scripts.py index 11e5b2016..5dad950fb 100644 --- a/tests/unit/test_release_scripts.py +++ b/tests/unit/test_release_scripts.py @@ -148,6 +148,22 @@ def test_subscription_api_is_not_wired_to_ui_until_backend_exists(): assert importers == [] +def test_runtime_image_release_scripts_keep_builds_off_production(): + build_script = Path("scripts/build_release_images.sh").read_text(encoding="utf-8") + deploy_script = Path("scripts/deploy_release_images_production.sh").read_text(encoding="utf-8") + + assert "docker build --pull=false -f infra/docker/api.Dockerfile" in build_script + assert "docker build --pull=false -f infra/docker/worker.Dockerfile" in build_script + assert "docker save" in build_script + assert "docker load -i" in deploy_script + assert "API_IMAGE=\"xiaoxia-saas-api:$VERSION\"" in deploy_script + assert "WORKER_IMAGE=\"xiaoxia-saas-worker:$VERSION\"" in deploy_script + assert "ALLOW_PRODUCTION_BUILDS=false" in deploy_script + assert "docker compose --env-file \"$ENV_FILE\" build --pull=false web" in deploy_script + assert "docker compose --env-file \"$ENV_FILE\" build --pull=false api" not in deploy_script + assert "docker compose --env-file \"$ENV_FILE\" build --pull=false worker" not in deploy_script + + def test_backup_postgres_writes_manifest_and_version(): script = Path("scripts/backup_postgres.sh").read_text(encoding="utf-8")