diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index f34cc2e8e..dd619e769 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -110,9 +110,9 @@ jobs: env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} CI_WEBHOOK_URL: ${{ secrets.CI_WEBHOOK_URL }} - run: 'set +e + run: | + set +e FAILED_JOB="Validate Code Quality And Tests" python3 scripts/ci_notify_failure.py - ' - name: Job duration summary if: always() shell: sh @@ -123,9 +123,9 @@ jobs: shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Validate Code Quality And Tests" python3 scripts/ci_notify.py - ' unit-tests: needs: check-frontend-only if: always() && needs.check-frontend-only.outputs.skip_backend != 'true' @@ -162,9 +162,9 @@ jobs: env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} CI_WEBHOOK_URL: ${{ secrets.CI_WEBHOOK_URL }} - run: 'set +e + run: | + set +e FAILED_JOB="Unit Tests" python3 scripts/ci_notify_failure.py - ' - name: Job duration summary if: always() shell: sh @@ -175,9 +175,9 @@ jobs: shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Unit Tests" python3 scripts/ci_notify.py - ' integration-tests: name: Integration Tests @@ -213,9 +213,9 @@ jobs: env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} CI_WEBHOOK_URL: ${{ secrets.CI_WEBHOOK_URL }} - run: 'set +e + run: | + set +e FAILED_JOB="Integration Tests" python3 scripts/ci_notify_failure.py - ' - name: Job duration summary if: always() shell: sh @@ -226,9 +226,9 @@ jobs: shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Integration Tests" python3 scripts/ci_notify.py - ' frontend-lint: name: Frontend Lint @@ -269,9 +269,9 @@ jobs: shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Frontend Lint" python3 scripts/ci_notify.py - ' frontend-unit-test: name: Frontend Unit Tests @@ -305,9 +305,9 @@ jobs: shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Frontend Unit Tests" python3 scripts/ci_notify.py - ' build-staging: name: Build Staging ${{ matrix.service_display }} Image @@ -360,43 +360,46 @@ jobs: echo "Docker login successful" - name: Setup cache strategy shell: sh - run: "set -eu -if [ \"${GITHUB_REF_NAME}\" = \"develop\" ] || [ \"${GITHUB_REF_NAME}\" = \"main\" ]; then - echo \"CACHE_MODE=read-write\" >> $GITHUB_ENV - echo \"Cache mode: read-write (will push cache)\" -else - echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV - echo \"Cache mode: read-only\" -fi -" + run: | + set -eu + if [ "${GITHUB_REF_NAME}" = "develop" ] || [ "${GITHUB_REF_NAME}" = "main" ]; then + echo "CACHE_MODE=read-write" >> $GITHUB_ENV + echo "Cache mode: read-write (will push cache)" + else + echo "CACHE_MODE=read-only" >> $GITHUB_ENV + echo "Cache mode: read-only" + fi + - name: Setup buildx builder shell: sh - run: "set -eu -if ! docker buildx inspect ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} > /dev/null 2>&1; then - docker buildx create --use --name ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} --driver docker-container - echo \"Created ci-builder (docker-container driver)\" -else - docker buildx use ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} - echo \"Using existing ci-builder\" -fi -docker buildx inspect --bootstrap -" + run: | + set -eu + if ! docker buildx inspect ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} > /dev/null 2>&1; then + docker buildx create --use --name ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} --driver docker-container + echo "Created ci-builder (docker-container driver)" + else + docker buildx use ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} + echo "Using existing ci-builder" + fi + docker buildx inspect --bootstrap + - name: Build and push ${{ matrix.service_display }} image shell: sh - run: "set -eu -REGISTRY=\"xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji\" -IMAGE_TAG=\"${REGISTRY}/${{ matrix.image_name }}:${GITHUB_SHA}\" -CACHE_REF=\"${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}\" + run: | + set -eu + REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji" + IMAGE_TAG="${REGISTRY}/${{ matrix.image_name }}:${GITHUB_SHA}" + CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}" -EXTRA_BUILD_ARGS=\"APP_VERSION=\\\"${GITHUB_SHA}\\\"\" -if [ \"${{ matrix.service }}\" = \"web\" ]; then - EXTRA_BUILD_ARGS=\"$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf\" -fi + EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\"" + if [ "${{ matrix.service }}" = "web" ]; then + EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf" + fi -bash scripts/ci/docker_build_push.sh ${{ matrix.dockerfile }} \"${IMAGE_TAG}\" \"${CACHE_REF}\" $EXTRA_BUILD_ARGS + bash scripts/ci/docker_build_push.sh ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}" $EXTRA_BUILD_ARGS -echo -echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\"" + echo + echo "${{ matrix.service_display }} image pushed: ${IMAGE_TAG}" - name: Job duration summary if: always() shell: sh @@ -407,9 +410,9 @@ echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\"" shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Build Staging ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py - ' deploy-staging: name: Deploy Staging (Watchtower auto-deploy) @@ -436,9 +439,9 @@ echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\"" shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=start JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py - ' - name: Docker login to Registry shell: sh env: @@ -454,10 +457,11 @@ echo \"${{ matrix.service_display }} image pushed: ${IMAGE_TAG}\"" - name: Install SSH client if: success() shell: sh - run: "set -eu -apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1 -echo \"openssh-client installed\" -" + run: | + set -eu + apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1 + echo "openssh-client installed" + - name: Deploy staging over SSH (Registry pull) if: success() shell: sh @@ -468,42 +472,43 @@ echo \"openssh-client installed\" STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - run: "set -eux -staging_host=\"${STAGING_SSH_HOST:-47.98.113.167}\" -staging_user=\"${STAGING_SSH_USER:-root}\" -staging_port=\"${STAGING_SSH_PORT:-22222}\" -echo \"Host: $staging_host\" -echo \"Port: $staging_port\" + run: | + set -eux + staging_host="${STAGING_SSH_HOST:-47.98.113.167}" + staging_user="${STAGING_SSH_USER:-root}" + staging_port="${STAGING_SSH_PORT:-22222}" + echo "Host: $staging_host" + echo "Port: $staging_port" -mkdir -p ~/.ssh + mkdir -p ~/.ssh -key_path=\"\" -if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then - key_path=\"/root/.ssh/xiaoxia_runtime_builder\" - echo \"Using key: $key_path (builder key)\" -elif [ -f \"$HOME/.ssh/xiaoxia_runtime_builder\" ]; then - key_path=\"$HOME/.ssh/xiaoxia_runtime_builder\" - echo \"Using key: $key_path (home key)\" -elif [ -n \"${STAGING_SSH_KEY:-}\" ]; then - key_path=\"$HOME/.ssh/id_ed25519\" - printf '%s\\n' \"$STAGING_SSH_KEY\" > \"$key_path\" - chmod 600 \"$key_path\" - echo \"Using key from STAGING_SSH_KEY secret\" -else - echo \"ERROR: No SSH key available\" - ls -la ~/.ssh/ 2>/dev/null || true - ls -la /root/.ssh/ 2>/dev/null || true - exit 1 -fi + key_path="" + if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then + key_path="/root/.ssh/xiaoxia_runtime_builder" + echo "Using key: $key_path (builder key)" + elif [ -f "$HOME/.ssh/xiaoxia_runtime_builder" ]; then + key_path="$HOME/.ssh/xiaoxia_runtime_builder" + echo "Using key: $key_path (home key)" + elif [ -n "${STAGING_SSH_KEY:-}" ]; then + key_path="$HOME/.ssh/id_ed25519" + printf '%s\n' "$STAGING_SSH_KEY" > "$key_path" + chmod 600 "$key_path" + echo "Using key from STAGING_SSH_KEY secret" + else + echo "ERROR: No SSH key available" + ls -la ~/.ssh/ 2>/dev/null || true + ls -la /root/.ssh/ 2>/dev/null || true + exit 1 + fi -ssh-keyscan -p \"$staging_port\" -H \"$staging_host\" >> ~/.ssh/known_hosts 2>/dev/null -echo \"SSH keyscan done\" + ssh-keyscan -p "$staging_port" -H "$staging_host" >> ~/.ssh/known_hosts 2>/dev/null + echo "SSH keyscan done" -ssh -p \"$staging_port\" -i \"$key_path\" -o StrictHostKeyChecking=no \"${staging_user}@${staging_host}\" \"echo SSH_CONNECTION_OK && hostname\" -echo \"SSH connection verified\" + ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname" + echo "SSH connection verified" + + cat scripts/ci_staging_deploy.sh | ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "IMAGE_TAG='${GITHUB_SHA}' ACR_USERNAME='${ACR_USERNAME}' ACR_PASSWORD='${ACR_PASSWORD}' sh" -cat scripts/ci_staging_deploy.sh | ssh -p \"$staging_port\" -i \"$key_path\" -o StrictHostKeyChecking=no \"${staging_user}@${staging_host}\" \"IMAGE_TAG='${GITHUB_SHA}' ACR_USERNAME='${ACR_USERNAME}' ACR_PASSWORD='${ACR_PASSWORD}' sh\" -" - name: Staging health check + auto rollback if: success() shell: sh @@ -515,13 +520,13 @@ cat scripts/ci_staging_deploy.sh | ssh -p \"$staging_port\" -i \"$key_path\" -o STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }} ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - run: 'set -eu + run: | + set -eu echo "==========================================" echo " Staging 健康检查(Watchtower 模式)" echo "==========================================" echo "" bash scripts/ci_staging_healthcheck.sh - ' - name: Job duration summary if: always() shell: sh @@ -532,18 +537,18 @@ cat scripts/ci_staging_deploy.sh | ssh -p \"$staging_port\" -i \"$key_path\" -o shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=success JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py - ' - name: Notify on failure continue-on-error: true if: failure() shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py - ' staging-e2e: name: Staging E2E Tests @@ -563,17 +568,18 @@ cat scripts/ci_staging_deploy.sh | ssh -p \"$staging_port\" -i \"$key_path\" -o run: bash scripts/ci/step_timer_start.sh - name: Run Playwright E2E on staging shell: sh - run: "set -eu -docker run --rm --ipc=host \\ - -e E2E_BASE_URL=https://staging.xiaoxiajianji.com \\ - -e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \\ - -e E2E_BROWSER_CHANNEL=chromium \\ - -e PLAYWRIGHT_HEADLESS=1 \\ - -v \"$PWD:/workspace\" \\ - -w /workspace/apps/web \\ - git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \\ - sh -lc \"npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts\" -" + run: | + set -eu + docker run --rm --ipc=host \ + -e E2E_BASE_URL=https://staging.xiaoxiajianji.com \ + -e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \ + -e E2E_BROWSER_CHANNEL=chromium \ + -e PLAYWRIGHT_HEADLESS=1 \ + -v "$PWD:/workspace" \ + -w /workspace/apps/web \ + git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \ + sh -lc "npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts" + - name: Job duration summary if: always() shell: sh @@ -584,9 +590,9 @@ docker run --rm --ipc=host \\ shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Staging E2E Tests" python3 scripts/ci_notify.py - ' staging-api-tests: name: Staging API Integration Tests @@ -606,15 +612,16 @@ docker run --rm --ipc=host \\ run: bash scripts/ci/step_timer_start.sh - name: Run API integration tests on staging shell: sh - run: "set -eu -docker run --rm \\ - -e E2E_BASE_URL=https://staging.xiaoxiajianji.com \\ - -e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \\ - -v \"$PWD:/workspace\" \\ - -w /workspace/apps/web \\ - git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \\ - sh -lc 'npm ci && npx playwright test --reporter=line e2e/test_auth.spec.ts e2e/test_asset.spec.ts e2e/test_project.spec.ts' -" + run: | + set -eu + docker run --rm \ + -e E2E_BASE_URL=https://staging.xiaoxiajianji.com \ + -e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \ + -v "$PWD:/workspace" \ + -w /workspace/apps/web \ + git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \ + sh -lc 'npm ci && npx playwright test --reporter=line e2e/test_auth.spec.ts e2e/test_asset.spec.ts e2e/test_project.spec.ts' + - name: Job duration summary if: always() shell: sh @@ -625,9 +632,9 @@ docker run --rm \\ shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Staging API Integration Tests" python3 scripts/ci_notify.py - ' build-production: name: Build Production ${{ matrix.service_display }} Image @@ -681,38 +688,41 @@ docker run --rm \\ echo "Docker login successful" - name: Setup cache strategy shell: sh - run: "set -eu -echo \"CACHE_MODE=read-only\" >> $GITHUB_ENV -echo \"Cache mode: read-only (production build uses cached layers)\" -" + run: | + set -eu + echo "CACHE_MODE=read-only" >> $GITHUB_ENV + echo "Cache mode: read-only (production build uses cached layers)" + - name: Setup buildx builder shell: sh - run: "set -eu -if ! docker buildx inspect ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} > /dev/null 2>&1; then - docker buildx create --use --name ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} --driver docker-container - echo \"Created ci-builder\" -else - docker buildx use ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} - echo \"Using existing ci-builder\" -fi -docker buildx inspect --bootstrap -" + run: | + set -eu + if ! docker buildx inspect ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} > /dev/null 2>&1; then + docker buildx create --use --name ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} --driver docker-container + echo "Created ci-builder" + else + docker buildx use ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} + echo "Using existing ci-builder" + fi + docker buildx inspect --bootstrap + - name: Build and push production ${{ matrix.service_display }} image shell: sh - run: "set -eu -REGISTRY=\"xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji\" -IMAGE_TAG=\"${REGISTRY}/${{ matrix.image_name }}:${GITHUB_REF_NAME}\" -CACHE_REF=\"${REGISTRY}/${{ matrix.cache_name }}:main\" + run: | + set -eu + REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji" + IMAGE_TAG="${REGISTRY}/${{ matrix.image_name }}:${GITHUB_REF_NAME}" + CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:main" -EXTRA_BUILD_ARGS=\"APP_VERSION=\\\"${GITHUB_REF_NAME}\\\"\" -if [ \"${{ matrix.service }}\" = \"web\" ]; then - EXTRA_BUILD_ARGS=\"$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-production.conf\" -fi + EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_REF_NAME}\"" + if [ "${{ matrix.service }}" = "web" ]; then + EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-production.conf" + fi -bash scripts/ci/docker_build_push.sh ${{ matrix.dockerfile }} \"${IMAGE_TAG}\" \"${CACHE_REF}\" $EXTRA_BUILD_ARGS + bash scripts/ci/docker_build_push.sh ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}" $EXTRA_BUILD_ARGS -echo -echo \"${{ matrix.service_display }} production image pushed: ${IMAGE_TAG}\"" + echo + echo "${{ matrix.service_display }} production image pushed: ${IMAGE_TAG}" - name: Job duration summary if: always() shell: sh @@ -723,9 +733,9 @@ echo \"${{ matrix.service_display }} production image pushed: ${IMAGE_TAG}\"" shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Build Production ${{ matrix.service_display }} Image" python3 scripts/ci_notify.py - ' deploy-production: name: Deploy Production @@ -752,15 +762,15 @@ echo \"${{ matrix.service_display }} production image pushed: ${IMAGE_TAG}\"" shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=start JOB_NAME="Deploy Production" python3 scripts/ci_notify.py - ' - name: Install SSH client shell: sh - run: 'set -eu + run: | + set -eu apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1 echo "openssh-client installed" - ' - name: Deploy production over SSH (Registry pull) shell: sh env: @@ -769,42 +779,43 @@ echo \"${{ matrix.service_display }} production image pushed: ${IMAGE_TAG}\"" PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }} ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - run: "set -eux -production_host=\"${PRODUCTION_SSH_HOST:-47.98.113.167}\" -production_user=\"${PRODUCTION_SSH_USER:-root}\" -echo \"Host: $production_host\" -echo \"User: $production_user\" + run: | + set -eux + production_host="${PRODUCTION_SSH_HOST:-47.98.113.167}" + production_user="${PRODUCTION_SSH_USER:-root}" + echo "Host: $production_host" + echo "User: $production_user" -mkdir -p ~/.ssh -key_path=\"\" -if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then - key_path=\"/root/.ssh/xiaoxia_runtime_builder\" - echo \"Using key: $key_path (builder key)\" -elif [ -f \"$HOME/.ssh/xiaoxia_runtime_builder\" ]; then - key_path=\"$HOME/.ssh/xiaoxia_runtime_builder\" - echo \"Using key: $key_path (home key)\" -elif [ -n \"${PRODUCTION_SSH_KEY:-}\" ]; then - key_path=\"$HOME/.ssh/id_ed25519\" - printf '%s\\n' \"$PRODUCTION_SSH_KEY\" > \"$key_path\" - chmod 600 \"$key_path\" - echo \"Using key from PRODUCTION_SSH_KEY secret\" -else - echo \"ERROR: No SSH key available\" - ls -la ~/.ssh/ 2>/dev/null || true - ls -la /root/.ssh/ 2>/dev/null || true - exit 1 -fi + mkdir -p ~/.ssh + key_path="" + if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then + key_path="/root/.ssh/xiaoxia_runtime_builder" + echo "Using key: $key_path (builder key)" + elif [ -f "$HOME/.ssh/xiaoxia_runtime_builder" ]; then + key_path="$HOME/.ssh/xiaoxia_runtime_builder" + echo "Using key: $key_path (home key)" + elif [ -n "${PRODUCTION_SSH_KEY:-}" ]; then + key_path="$HOME/.ssh/id_ed25519" + printf '%s\n' "$PRODUCTION_SSH_KEY" > "$key_path" + chmod 600 "$key_path" + echo "Using key from PRODUCTION_SSH_KEY secret" + else + echo "ERROR: No SSH key available" + ls -la ~/.ssh/ 2>/dev/null || true + ls -la /root/.ssh/ 2>/dev/null || true + exit 1 + fi -ssh-keyscan -p 22222 -H \"$production_host\" >> ~/.ssh/known_hosts 2>/dev/null -echo \"SSH keyscan done\" + ssh-keyscan -p 22222 -H "$production_host" >> ~/.ssh/known_hosts 2>/dev/null + echo "SSH keyscan done" -ssh -p 22222 -i \"$key_path\" -o StrictHostKeyChecking=no \"${production_user}@${production_host}\" \"echo SSH_CONNECTION_OK && hostname\" -echo \"SSH connection verified\" + ssh -p 22222 -i "$key_path" -o StrictHostKeyChecking=no "${production_user}@${production_host}" "echo SSH_CONNECTION_OK && hostname" + echo "SSH connection verified" + + # Production deploy script (base64 encoded) + DEPLOY_B64="IyEvYmluL3NoCnNldCAtZXUKCklNQUdFX1RBRz0iJHtJTUFHRV9UQUc6LX0iClJFR0lTVFJZPSIke1JFR0lTVFJZOi1naXQueGlhb3hpYWppYW5qaS5jb20veGlhb3hpYS94aWFveGlhLXNhYXN9IgpSRUdJU1RSWV9VU0U9IiR7UkVHSVNUUllfVVNFUjp-eGlhb3hpYX0iClJFR0lTVFJZX1RPS0VOPSIke1JFR0lTVFJZX1RPS0VOOi19IgpFTlZfRklMRT0iJHtFTlZfRklMRTotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vLmVudn0iCkdFTkVSQVRFRF9ESVI9IiR7R0VORVJBVEVEX0RJUjotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vZ2VuZXJhdGVkfSIKTEVHQUNZX0FTU0VUU19ESVI9IiR7TEVHQUNZX0FTU0VUU19ESVI6LS92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL2xlZ2FjeS1hc3NldHN9IgoKaWYgWyAteiAiJElNQUdFX1RBRyIgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBJTUFHRV9UQUcgaXMgcmVxdWlyZWQiCiAgZXhpdCAxCmZpCgp0ZXN0IC1mICIkRU5WX0ZJTEUiCm1rZGlyIC1wICIkR0VORVJBVEVEX0RJUiIKbWtkaXIgLXAgIiRMRUdBQ1lfQVNTRVRTX0RJUiIKCiMgTG9naW4gdG8gcmVnaXN0cnkKaWYgWyAtbiAiJFJFR0lTVFJZX1RPS0VOIiBdOyB0aGVuCiAgUkVHSVNUUllfSE9TVD0kKGVjaG8gIiRSRUdJU1RSWSIgfCBjdXQgLWQvIC1mMSkKICBwcmludGYgJXMgIiRSRUdJU1RSWV9UT0tFTiIgfCBkb2NrZXIgbG9naW4gIiRSRUdJU1RSWV9IT1NUIiAtdSAiJFJFR0lTVFJZX1VTRVIiIC0tcGFzc3dvcmQtc3RkaW4gMj4vZGV2L251bGwgfHwgewogICAgZWNobyAiV0FSTjogZG9ja2VyIGxvZ2luIGZhaWxlZCwgd2lsbCB0cnkgdG8gcHVsbCBhbnl3YXkiCiAgfQpmaQoKIyBQdWxsIGltYWdlcwpSRUdJU1RSWV9BUEk9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy1hcGk6JHtJTUFHRV9UQUd9IgpSRUdJU1RSWV9XT1JLRVI9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy13b3JrZXI6JHtJTUFHRV9UQUd9IgpSRUdJU1RSWV9XRUI9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy13ZWI6JHtJTUFHRV9UQUd9IgoKTE9DQUxfQVBJPSJ4aWFveGlhLXNhYXMtYXBpOiR7SU1BR0VfVEFHfSIKTE9DQUxfV09SS0VSPSJ4aWFveGlhLXNhYXMtd29ya2VyOiR7SU1BR0VfVEFHfSIKTE9DQUxfV0VCPSJ4aWFveGlhLXNhYXMtd2ViOiR7SU1BR0VfVEFHfSIKCmVjaG8gIlB1bGxpbmcgQVBJIGltYWdlLi4uIgpkb2NrZXIgcHVsbCAiJFJFR0lTVFJZX0FQSSIKZWNobyAiUHVsbGluZyBXb3JrZXIgaW1hZ2UuLi4iCmRvY2tlciBwdWxsICIkUkVHSVNUUllfV09SS0VSCmVjaG8gIlB1bGxpbmcgV2ViIGltYWdlLi4uIgpkb2NrZXIgcHVsbCAiJFJFR0lTVFJZX1dFQiIKCiMgUmUtdGFnCmRvY2tlciB0YWcgIiRSRUdJU1RSWV9BUEkiICIkTE9DQUxfQVBJIgpkb2NrZXIgdGFnICIkUkVHSVNUUllfV09SS0VSIiAiJExPQ0FMX1dPUktFUiIKZG9ja2VyIHRhZyAiJFJFR0lTVFJZX1dFQiAiJExPQ0FMX1dFQiIKZWNobyAiQWxsIGltYWdlcyBwdWxsZWQgYW5kIHRhZ2dlZC4iCgojIEJhY2sgdXAgbGVnYWN5IGFzc2V0cwplY2hvICJCYWNraW5nIHVwIGxlZ2FjeSBhc3NldHMgZnJvbSBjdXJyZW50IHdlYiBjb250YWluZXIuLi4iCmlmIGRvY2tlciBpbnNwZWN0IHhhb3hpYS13ZWItcHJvZHVjdGlvbiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICBfdG1wZGlyPSIvdG1wL2xlZ2FjeS1hc3NldHMtJCQiCiAgcm0gLXJmICIkX3RtcGRpciIKICBta2RpciAtcCAiJF90bXBkaXIiCiAgZG9ja2VyIGNwIHhhb3hpYS13ZWItcHJvZHVjdGlvbjovdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLy4gIiRfdG1wZGlyLyIgMj4vZGV2L251bGwgfHwgdHJ1ZQogIGlmIFsgLWQgIiRfdG1wZGlyIl0gJiYgWyAiJChscyAtQSAiJF90bXBkaXIiIDI+L2Rldi9udWxsKSIgXTsgdGhlbgogICAgY3AgLWFuICIkX3RtcGRpciIvLiAiJExFR0FDWV9BU1NFVFNfRElSIi8gMj4vZGV2L251bGwgfHwgdHJ1ZQogICAgZWNobyAiTGVnYWN5IGFzc2V0cyBiYWNrZWQgdXA6ICQobHMgIiRfdG1wZGlyIiB8IHdjIC1sKSBmaWxlcyIKICBmaQogIHJtIC1yZiAiJF90bXBkaXIiCmVsc2UKICBlY2hvICJObyBleGlzdGluZyB3ZWIgY29udGFpbmVyLCBza2lwcGluZyBsZWdhY3kgYXNzZXRzIGJhY2t1cCIKZmkKCiMgQ2xlYW4gb2xkZXIgbGVnYWN5IGFzc2V0cyAoNyBkYXlzKQppZiBbIC1kICIkTEVHQUNZX0FTU0VUU19ESVIiBdOyB0aGVuCiAgZmluZCAiJExFR0FDWV9BU1NFVFNfRElSIiAtdHlwZSBmIC1tdGltZSArNyAtZGVsZXRlIDI+L2Rldi9udWxsIHx8IHRydWUKICBlY2hvICJMZWdhY3kgYXNzZXRzIGNsZWFudXAgZG9uZSAocmV0YWluIDcgZGF5cykiCmZpCgojIENoZWNrIGluZnJhc3RydWN0dXJlCmZvciBjIGluIHhhb3hpYS1wb3N0Z3Jlcy1wcm9kdWN0aW9uIHhhb3hpYS1yZWRpcy1wcm9kdWN0aW9uOyBkbwogIGlmICEgZG9ja2VyIGluc3BlY3QgIiRjIiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkVSUk9SOiBSZXF1aXJlZCBjb250YWluZXIgbm90IGZvdW5kOiAkYyIKICAgIGV4aXQgMQogIGZpCmRvbmUKCmRvY2tlciBuZXR3b3JrIGNyZWF0ZSB4aWFveGlhLW5ldC1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKCiMgUnVuIG1pZ3JhdGlvbnMKZWNobyAiUnVubmluZyBkYXRhYmFzZSBtaWdyYXRpb25zLi4uIgpkb2NrZXIgcnVuIC0tcm0gXAogIC0tZW52LWZpbGUgIiRFTlZfRklMRSIgXAogIC0tbmV0d29yayB4aWFveGlhLW5ldC1wcm9kdWN0aW9uIFwKICAtZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogICIkTE9DQUxfQVBJIihzaCAtYyAiY2QgL2FwcCAmJiBhbGVtYmljIHVwZ3JhZGUgaGVhZCIKZWNobyAiTWlncmF0aW9ucyBjb21wbGV0ZWQuIgoKIyBTdG9wIG9sZCBjb250YWluZXJzCmRvY2tlciBybSAtZiB4aWFveGlhLWFwaS1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKZG9ja2VyIHJtIC1mIHhhb3hpYS13b3JrZXItcHJvZHVjdGlvbiAyPi9kZXYvbnVsbCB8fCB0cnVlCmRvY2tlciBybSAtZiB4aWFveGlhLXdlYi1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKCiMgTG9nIG9wdGlvbnMKTE9HX09QVFM9Ii0tbG9nLWRyaXZlciBqc29uLWZpbGUgLS1sb2ctb3B0IG1heC1zaXplPTUwbSAtLWxvZy1vcHQgbWF4LWZpbGU9MyIKCiMgU3RhcnQgQVBJCmVjaG8gIlN0YXJ0aW5nIEFQSSBjb250YWluZXIuLi4iCmRvY2tlciBydW4gLWQgXAogIC0tbmFtZSB4aWFveGlhLWFwaS1wcm9kdWN0aW9uIFwKICAtLWVudi1maWxlICIkRU5WX0ZJTEUiIFwKICAtLW5ldHdvcmsgeGlhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgLXAgMTI3LjAuMC4xOjgwMDE6ODAwMCBcCiAgZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogIC1lIEFQUF9WRVJTSU9OPSIkSU1BR0VfVEFHIiBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX0RJUj0vYXBwL2dlbmVyYXRlZCBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX1VSTF9QUkVGSVg9L2dlbmVyYXRlZC1maWxlcyBcCiAgLWBQVUJMSUNfQVBJX0JBU0VfVVJMPWh0dHBzOi8vYXBpLnhpYW94aWFqaWFuamkuY29tIFwKICAtdiAiJEdFTkVSQVRFRF9ESVI6L2FwcC9nZW5lcmF0ZWQiIFwKICAtLXJlc3RhcnQgdW5sZXNzLXN0b3BwZWQgXAogIC0tY3B1cyAyIFwKIC0tbWVtb3J5IDJnIFwKIC0taGVhbHRoLWNtZCAicHl0aG9uIC1jIFwiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJywgdGltZW91dD01KVwiIiBcCiAgLS1oZWFsdGctaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDEwcyBcCiAgLS1oZWFsdGgtcmV0cmllcyAzIFwKIC0taGVhbHRoLXN0YXJ0LXBlcmlvZCA0MHMgXAogICRMTE9HX09QVFMgXAogICIkTE9DQUxfQVBJIgoKIyBTdGFydCBXb3JrZXIKZWNobyAiU3RhcnRpbmcgV29ya2VyIGNvbnRhaW5lci4uLiIKZG9ja2VyIHJ1biAtZCBcCiAgLS1uYW1lIHhhb3hpYS13b3JrZXItcHJvZHVjdGlvbiBcCiAgLS1lbnYtZmlsZSAiJEVOVl9GSUxFIiBcCiAgLS1uZXR3b3JrIHhhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogIC1lIEFQUF9WRVJTSU9OPSIkSU1BR0VfVEFHIiBcCiAgLWUgV09SS0VSX0NPTkNVUlJFTkN5PTEgXAogIC1lIFdPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPTEwMCBcCiAgLWVuZGVkX0ZJTEVTX0RJUj0vYXBwL2dlbmVyYXRlZCBcCiAgLWVfRklMRVNfVVJMX1BSRUZJWD0vZ2VuZXJhdGVkLWZpbGVzIFwKICAtZSBQVUJMSUNfQVBJX0JBU0VfVVJMPWh0dHBzOi8vYXBpLnhpYW94aWFqaWFuamkuY29tIFwKICAtdiAiJEdFTkVSQVRFRF9ESVI6L2FwcC9nZW5lcmF0ZWQiIFwKICAtLXJlc3RhcnQgdW5sZXNzLXN0b3BwZWQgXAogIC0tY3B1cyAyIFwKIC0tbWVtb3J5IDJnIFwKIC0taGVhbHRoLWNtZCAic2ggLWMgXCJncmVwIC1xIGNsZXJ5IC9wcm9jLzEvY21kbGluZSB8fCBleGl0IDFcIiIgXAogIC0taGVhbHRoLWludGVydmFsIDMwcyBcCiAgLS1oZWFsdGgtdGltZW91dCAxMHMgXAogIC0taGVhbHRoLXJldHJpZXMgMyBcCiAgLS1oZWFsdGgtc3RhcnQtcGVyaW9kIDMwcyBcCiAgJExPR19PUFRTIFwKICAiJExPQ0FMX1dPUktFUiIKCiMgU3RhcnQgV2ViCiMgTGVnYWN5IGFzc2V0cyBtb3VudDogdW5kZXIgbmdpbnggYXNzZXRzLWxlZ2FjeS9hc3NldHMKTEVHQUNZX1ZPTFVNRT0iIgppZiBbIC1kICIkTEVHQUNZX0FTU0VUU19ESVIiIF0gJiYgWyAiJChscyAtQSAiJExFR0FDWV9BU1NFVFNfRElSIiAyPi9kZXYvbnVsbCkiIF07IHRoZW4KICBMRUdBQ1lfVk9MVU1FPSItdiAke0xFR0FDWV9BU1NFVFNfRElSfTovdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLWxlZ2FjeS9hc3NldHM6cm8iCiAgZWNobyAiV2ViIGNvbnRhaW5lcjogbGVnYWN5IGFzc2V0cyBtb3VudGVkIChmYWxsYmFjaykiCmVsc2UKICBlY2hvICJXZWIgY29udGFpbmVyOiBubyBsZWdhY3kgYXNzZXRzIHRvIG1vdW50IgpmaQoKZWNobyAiU3RhcnRpbmcgV2ViIGNvbnRhaW5lci4uLiIKZG9ja2VyIHJ1biAtZCBcCiAgLS1uYW1lIHhhb3hpYS13ZWItcHJvZHVjdGlvbiBcCiAgLS1uZXR3b3JrIHhhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgLXAgMTI3LjAuMC4xOjMwMDI6ODAgXAogIC0tcmVzdGFydCB1bmxlc3Mtc3RvcHBlZCBcCiAgLS1jcHVzIDAuNSBcCiAgLS1tZW1vcnkgNTEybSBcCiAgJExFR0FDWV9WT0xVTUUgXAogIC0taGVhbHRoLWNtZCAid2dldCAtLXNwaWRlciAtcSBodHRwOi8vMTI3LjAuMC4xOjgwIiBcCiAgLS1oZWFsdGhtaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDVzIFwKIC0taGVhbHRoLXJldHJpZXMgMyBcCiAgJExPR19PUFRTIFwKICAiJExPQ0FMX1dFQiIKCiMgV2FpdCBmb3IgaGVhbHRoCmVjaG8gIldhaXRpbmcgZm9yIEFQSSB0byBiZWNvbWUgaGVhbHRoeS4uLiIKaT0wCndoaWxlIFsgIiRpIiAtbHQgNDAgXTsgZG8KICBpZiBjdXJsIC1zZiAtLW1heC10aW1lIDUgaHR0cDovLzEyNy4wLjAuMTo4MDAxL2hlYWx0aCA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkFQSSBpcyBoZWFsdGh5ISIKICAgIGJyZWFrCiAgZmkKICBpPSQoKGkgKyAxKSkKICBlY2hvICIgIFdhaXRpbmcuLi4gKCQpL0QwKSIKICBzbGVlcCAzCmRvbmUKCmlmIFsgIiRpIiAtZ2UgNDAgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBBUEkgZGlkIG5vdCBiZWNvbWUgaGVhbHRoeSB3aXRoaW4gMTIwc3MiCiAgZG9ja2VyIGxvZ3MgLS10YWlsIDUwIHhhb3hpYS1hcGktcHJvZHVjdGlvbgogIGV4aXQgMQpmaQoKZWNobyAiV2FpdGluZyBmb3IgV2ViIHRvIGJlY29tZSBoZWFsdGh5Li4uIgppPTAKd2hpbGUgWyAiJGkiIC1sdCAxNSBdOyBkbwogIGlmIGN1cmwgLXNmIC0tbWF4LXRpbWUgNSBodHRwOi8vMTI3LjAuMC4xOjMwMDIvID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgZWNobyAiV2ViIGlzIGhlYWx0aHkhIgogICAgYnJlYWsKICBmaQogIGk9JCgoaSArIDEpKQogIGVjaG8gIiAgV2FpdGluZy4uLiAoJGkvMTUpIgogIHNsZWVwIDIKZG9uZQoKaWYgWyAiJGkiIC1nZSAxNSBdOyB0aGVuCiAgZWNobyAiRVJST1I6IFdlYiBkaWQgbm90IGJlY29tZSBoZWFsdGh5IHdpdGhpbiAzMHMiCiAgZG9ja2VyIGxvZ3MgLS10YWlsIDMwIHhhb3hpYS13ZWItcHJvZHVjdGlvbgogIGV4aXQgMQpmaQoKIyBDbGVhbiB1cA==" + echo "$DEPLOY_B64" | base64 -d | ssh -p 22222 -i "$key_path" "${production_user}@${production_host}" "IMAGE_TAG='${GITHUB_REF_NAME}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh" -# Production deploy script (base64 encoded) -DEPLOY_B64=\"IyEvYmluL3NoCnNldCAtZXUKCklNQUdFX1RBRz0iJHtJTUFHRV9UQUc6LX0iClJFR0lTVFJZPSIke1JFR0lTVFJZOi1naXQueGlhb3hpYWppYW5qaS5jb20veGlhb3hpYS94aWFveGlhLXNhYXN9IgpSRUdJU1RSWV9VU0U9IiR7UkVHSVNUUllfVVNFUjp-eGlhb3hpYX0iClJFR0lTVFJZX1RPS0VOPSIke1JFR0lTVFJZX1RPS0VOOi19IgpFTlZfRklMRT0iJHtFTlZfRklMRTotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vLmVudn0iCkdFTkVSQVRFRF9ESVI9IiR7R0VORVJBVEVEX0RJUjotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vZ2VuZXJhdGVkfSIKTEVHQUNZX0FTU0VUU19ESVI9IiR7TEVHQUNZX0FTU0VUU19ESVI6LS92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL2xlZ2FjeS1hc3NldHN9IgoKaWYgWyAteiAiJElNQUdFX1RBRyIgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBJTUFHRV9UQUcgaXMgcmVxdWlyZWQiCiAgZXhpdCAxCmZpCgp0ZXN0IC1mICIkRU5WX0ZJTEUiCm1rZGlyIC1wICIkR0VORVJBVEVEX0RJUiIKbWtkaXIgLXAgIiRMRUdBQ1lfQVNTRVRTX0RJUiIKCiMgTG9naW4gdG8gcmVnaXN0cnkKaWYgWyAtbiAiJFJFR0lTVFJZX1RPS0VOIiBdOyB0aGVuCiAgUkVHSVNUUllfSE9TVD0kKGVjaG8gIiRSRUdJU1RSWSIgfCBjdXQgLWQvIC1mMSkKICBwcmludGYgJXMgIiRSRUdJU1RSWV9UT0tFTiIgfCBkb2NrZXIgbG9naW4gIiRSRUdJU1RSWV9IT1NUIiAtdSAiJFJFR0lTVFJZX1VTRVIiIC0tcGFzc3dvcmQtc3RkaW4gMj4vZGV2L251bGwgfHwgewogICAgZWNobyAiV0FSTjogZG9ja2VyIGxvZ2luIGZhaWxlZCwgd2lsbCB0cnkgdG8gcHVsbCBhbnl3YXkiCiAgfQpmaQoKIyBQdWxsIGltYWdlcwpSRUdJU1RSWV9BUEk9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy1hcGk6JHtJTUFHRV9UQUd9IgpSRUdJU1RSWV9XT1JLRVI9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy13b3JrZXI6JHtJTUFHRV9UQUd9IgpSRUdJU1RSWV9XRUI9IiR7UkVHSVNUUll9L3hpYW94aWEtc2Fhcy13ZWI6JHtJTUFHRV9UQUd9IgoKTE9DQUxfQVBJPSJ4aWFveGlhLXNhYXMtYXBpOiR7SU1BR0VfVEFHfSIKTE9DQUxfV09SS0VSPSJ4aWFveGlhLXNhYXMtd29ya2VyOiR7SU1BR0VfVEFHfSIKTE9DQUxfV0VCPSJ4aWFveGlhLXNhYXMtd2ViOiR7SU1BR0VfVEFHfSIKCmVjaG8gIlB1bGxpbmcgQVBJIGltYWdlLi4uIgpkb2NrZXIgcHVsbCAiJFJFR0lTVFJZX0FQSSIKZWNobyAiUHVsbGluZyBXb3JrZXIgaW1hZ2UuLi4iCmRvY2tlciBwdWxsICIkUkVHSVNUUllfV09SS0VSCmVjaG8gIlB1bGxpbmcgV2ViIGltYWdlLi4uIgpkb2NrZXIgcHVsbCAiJFJFR0lTVFJZX1dFQiIKCiMgUmUtdGFnCmRvY2tlciB0YWcgIiRSRUdJU1RSWV9BUEkiICIkTE9DQUxfQVBJIgpkb2NrZXIgdGFnICIkUkVHSVNUUllfV09SS0VSIiAiJExPQ0FMX1dPUktFUiIKZG9ja2VyIHRhZyAiJFJFR0lTVFJZX1dFQiAiJExPQ0FMX1dFQiIKZWNobyAiQWxsIGltYWdlcyBwdWxsZWQgYW5kIHRhZ2dlZC4iCgojIEJhY2sgdXAgbGVnYWN5IGFzc2V0cwplY2hvICJCYWNraW5nIHVwIGxlZ2FjeSBhc3NldHMgZnJvbSBjdXJyZW50IHdlYiBjb250YWluZXIuLi4iCmlmIGRvY2tlciBpbnNwZWN0IHhhb3hpYS13ZWItcHJvZHVjdGlvbiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICBfdG1wZGlyPSIvdG1wL2xlZ2FjeS1hc3NldHMtJCQiCiAgcm0gLXJmICIkX3RtcGRpciIKICBta2RpciAtcCAiJF90bXBkaXIiCiAgZG9ja2VyIGNwIHhhb3hpYS13ZWItcHJvZHVjdGlvbjovdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLy4gIiRfdG1wZGlyLyIgMj4vZGV2L251bGwgfHwgdHJ1ZQogIGlmIFsgLWQgIiRfdG1wZGlyIl0gJiYgWyAiJChscyAtQSAiJF90bXBkaXIiIDI+L2Rldi9udWxsKSIgXTsgdGhlbgogICAgY3AgLWFuICIkX3RtcGRpciIvLiAiJExFR0FDWV9BU1NFVFNfRElSIi8gMj4vZGV2L251bGwgfHwgdHJ1ZQogICAgZWNobyAiTGVnYWN5IGFzc2V0cyBiYWNrZWQgdXA6ICQobHMgIiRfdG1wZGlyIiB8IHdjIC1sKSBmaWxlcyIKICBmaQogIHJtIC1yZiAiJF90bXBkaXIiCmVsc2UKICBlY2hvICJObyBleGlzdGluZyB3ZWIgY29udGFpbmVyLCBza2lwcGluZyBsZWdhY3kgYXNzZXRzIGJhY2t1cCIKZmkKCiMgQ2xlYW4gb2xkZXIgbGVnYWN5IGFzc2V0cyAoNyBkYXlzKQppZiBbIC1kICIkTEVHQUNZX0FTU0VUU19ESVIiBdOyB0aGVuCiAgZmluZCAiJExFR0FDWV9BU1NFVFNfRElSIiAtdHlwZSBmIC1tdGltZSArNyAtZGVsZXRlIDI+L2Rldi9udWxsIHx8IHRydWUKICBlY2hvICJMZWdhY3kgYXNzZXRzIGNsZWFudXAgZG9uZSAocmV0YWluIDcgZGF5cykiCmZpCgojIENoZWNrIGluZnJhc3RydWN0dXJlCmZvciBjIGluIHhhb3hpYS1wb3N0Z3Jlcy1wcm9kdWN0aW9uIHhhb3hpYS1yZWRpcy1wcm9kdWN0aW9uOyBkbwogIGlmICEgZG9ja2VyIGluc3BlY3QgIiRjIiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkVSUk9SOiBSZXF1aXJlZCBjb250YWluZXIgbm90IGZvdW5kOiAkYyIKICAgIGV4aXQgMQogIGZpCmRvbmUKCmRvY2tlciBuZXR3b3JrIGNyZWF0ZSB4aWFveGlhLW5ldC1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKCiMgUnVuIG1pZ3JhdGlvbnMKZWNobyAiUnVubmluZyBkYXRhYmFzZSBtaWdyYXRpb25zLi4uIgpkb2NrZXIgcnVuIC0tcm0gXAogIC0tZW52LWZpbGUgIiRFTlZfRklMRSIgXAogIC0tbmV0d29yayB4aWFveGlhLW5ldC1wcm9kdWN0aW9uIFwKICAtZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogICIkTE9DQUxfQVBJIihzaCAtYyAiY2QgL2FwcCAmJiBhbGVtYmljIHVwZ3JhZGUgaGVhZCIKZWNobyAiTWlncmF0aW9ucyBjb21wbGV0ZWQuIgoKIyBTdG9wIG9sZCBjb250YWluZXJzCmRvY2tlciBybSAtZiB4aWFveGlhLWFwaS1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKZG9ja2VyIHJtIC1mIHhhb3hpYS13b3JrZXItcHJvZHVjdGlvbiAyPi9kZXYvbnVsbCB8fCB0cnVlCmRvY2tlciBybSAtZiB4aWFveGlhLXdlYi1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKCiMgTG9nIG9wdGlvbnMKTE9HX09QVFM9Ii0tbG9nLWRyaXZlciBqc29uLWZpbGUgLS1sb2ctb3B0IG1heC1zaXplPTUwbSAtLWxvZy1vcHQgbWF4LWZpbGU9MyIKCiMgU3RhcnQgQVBJCmVjaG8gIlN0YXJ0aW5nIEFQSSBjb250YWluZXIuLi4iCmRvY2tlciBydW4gLWQgXAogIC0tbmFtZSB4aWFveGlhLWFwaS1wcm9kdWN0aW9uIFwKICAtLWVudi1maWxlICIkRU5WX0ZJTEUiIFwKICAtLW5ldHdvcmsgeGlhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgLXAgMTI3LjAuMC4xOjgwMDE6ODAwMCBcCiAgZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogIC1lIEFQUF9WRVJTSU9OPSIkSU1BR0VfVEFHIiBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX0RJUj0vYXBwL2dlbmVyYXRlZCBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX1VSTF9QUkVGSVg9L2dlbmVyYXRlZC1maWxlcyBcCiAgLWBQVUJMSUNfQVBJX0JBU0VfVVJMPWh0dHBzOi8vYXBpLnhpYW94aWFqaWFuamkuY29tIFwKICAtdiAiJEdFTkVSQVRFRF9ESVI6L2FwcC9nZW5lcmF0ZWQiIFwKICAtLXJlc3RhcnQgdW5sZXNzLXN0b3BwZWQgXAogIC0tY3B1cyAyIFwKIC0tbWVtb3J5IDJnIFwKIC0taGVhbHRoLWNtZCAicHl0aG9uIC1jIFwiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJywgdGltZW91dD01KVwiIiBcCiAgLS1oZWFsdGctaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDEwcyBcCiAgLS1oZWFsdGgtcmV0cmllcyAzIFwKIC0taGVhbHRoLXN0YXJ0LXBlcmlvZCA0MHMgXAogICRMTE9HX09QVFMgXAogICIkTE9DQUxfQVBJIgoKIyBTdGFydCBXb3JrZXIKZWNobyAiU3RhcnRpbmcgV29ya2VyIGNvbnRhaW5lci4uLiIKZG9ja2VyIHJ1biAtZCBcCiAgLS1uYW1lIHhhb3hpYS13b3JrZXItcHJvZHVjdGlvbiBcCiAgLS1lbnYtZmlsZSAiJEVOVl9GSUxFIiBcCiAgLS1uZXR3b3JrIHhhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogIC1lIEFQUF9WRVJTSU9OPSIkSU1BR0VfVEFHIiBcCiAgLWUgV09SS0VSX0NPTkNVUlJFTkN5PTEgXAogIC1lIFdPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPTEwMCBcCiAgLWVuZGVkX0ZJTEVTX0RJUj0vYXBwL2dlbmVyYXRlZCBcCiAgLWVfRklMRVNfVVJMX1BSRUZJWD0vZ2VuZXJhdGVkLWZpbGVzIFwKICAtZSBQVUJMSUNfQVBJX0JBU0VfVVJMPWh0dHBzOi8vYXBpLnhpYW94aWFqaWFuamkuY29tIFwKICAtdiAiJEdFTkVSQVRFRF9ESVI6L2FwcC9nZW5lcmF0ZWQiIFwKICAtLXJlc3RhcnQgdW5sZXNzLXN0b3BwZWQgXAogIC0tY3B1cyAyIFwKIC0tbWVtb3J5IDJnIFwKIC0taGVhbHRoLWNtZCAic2ggLWMgXCJncmVwIC1xIGNsZXJ5IC9wcm9jLzEvY21kbGluZSB8fCBleGl0IDFcIiIgXAogIC0taGVhbHRoLWludGVydmFsIDMwcyBcCiAgLS1oZWFsdGgtdGltZW91dCAxMHMgXAogIC0taGVhbHRoLXJldHJpZXMgMyBcCiAgLS1oZWFsdGgtc3RhcnQtcGVyaW9kIDMwcyBcCiAgJExPR19PUFRTIFwKICAiJExPQ0FMX1dPUktFUiIKCiMgU3RhcnQgV2ViCiMgTGVnYWN5IGFzc2V0cyBtb3VudDogdW5kZXIgbmdpbnggYXNzZXRzLWxlZ2FjeS9hc3NldHMKTEVHQUNZX1ZPTFVNRT0iIgppZiBbIC1kICIkTEVHQUNZX0FTU0VUU19ESVIiIF0gJiYgWyAiJChscyAtQSAiJExFR0FDWV9BU1NFVFNfRElSIiAyPi9kZXYvbnVsbCkiIF07IHRoZW4KICBMRUdBQ1lfVk9MVU1FPSItdiAke0xFR0FDWV9BU1NFVFNfRElSfTovdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLWxlZ2FjeS9hc3NldHM6cm8iCiAgZWNobyAiV2ViIGNvbnRhaW5lcjogbGVnYWN5IGFzc2V0cyBtb3VudGVkIChmYWxsYmFjaykiCmVsc2UKICBlY2hvICJXZWIgY29udGFpbmVyOiBubyBsZWdhY3kgYXNzZXRzIHRvIG1vdW50IgpmaQoKZWNobyAiU3RhcnRpbmcgV2ViIGNvbnRhaW5lci4uLiIKZG9ja2VyIHJ1biAtZCBcCiAgLS1uYW1lIHhhb3hpYS13ZWItcHJvZHVjdGlvbiBcCiAgLS1uZXR3b3JrIHhhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgLXAgMTI3LjAuMC4xOjMwMDI6ODAgXAogIC0tcmVzdGFydCB1bmxlc3Mtc3RvcHBlZCBcCiAgLS1jcHVzIDAuNSBcCiAgLS1tZW1vcnkgNTEybSBcCiAgJExFR0FDWV9WT0xVTUUgXAogIC0taGVhbHRoLWNtZCAid2dldCAtLXNwaWRlciAtcSBodHRwOi8vMTI3LjAuMC4xOjgwIiBcCiAgLS1oZWFsdGhtaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDVzIFwKIC0taGVhbHRoLXJldHJpZXMgMyBcCiAgJExPR19PUFRTIFwKICAiJExPQ0FMX1dFQiIKCiMgV2FpdCBmb3IgaGVhbHRoCmVjaG8gIldhaXRpbmcgZm9yIEFQSSB0byBiZWNvbWUgaGVhbHRoeS4uLiIKaT0wCndoaWxlIFsgIiRpIiAtbHQgNDAgXTsgZG8KICBpZiBjdXJsIC1zZiAtLW1heC10aW1lIDUgaHR0cDovLzEyNy4wLjAuMTo4MDAxL2hlYWx0aCA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkFQSSBpcyBoZWFsdGh5ISIKICAgIGJyZWFrCiAgZmkKICBpPSQoKGkgKyAxKSkKICBlY2hvICIgIFdhaXRpbmcuLi4gKCQpL0QwKSIKICBzbGVlcCAzCmRvbmUKCmlmIFsgIiRpIiAtZ2UgNDAgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBBUEkgZGlkIG5vdCBiZWNvbWUgaGVhbHRoeSB3aXRoaW4gMTIwc3MiCiAgZG9ja2VyIGxvZ3MgLS10YWlsIDUwIHhhb3hpYS1hcGktcHJvZHVjdGlvbgogIGV4aXQgMQpmaQoKZWNobyAiV2FpdGluZyBmb3IgV2ViIHRvIGJlY29tZSBoZWFsdGh5Li4uIgppPTAKd2hpbGUgWyAiJGkiIC1sdCAxNSBdOyBkbwogIGlmIGN1cmwgLXNmIC0tbWF4LXRpbWUgNSBodHRwOi8vMTI3LjAuMC4xOjMwMDIvID4vZGV2L251bGwgMj4mMTsgdGhlbgogICAgZWNobyAiV2ViIGlzIGhlYWx0aHkhIgogICAgYnJlYWsKICBmaQogIGk9JCgoaSArIDEpKQogIGVjaG8gIiAgV2FpdGluZy4uLiAoJGkvMTUpIgogIHNsZWVwIDIKZG9uZQoKaWYgWyAiJGkiIC1nZSAxNSBdOyB0aGVuCiAgZWNobyAiRVJST1I6IFdlYiBkaWQgbm90IGJlY29tZSBoZWFsdGh5IHdpdGhpbiAzMHMiCiAgZG9ja2VyIGxvZ3MgLS10YWlsIDMwIHhhb3hpYS13ZWItcHJvZHVjdGlvbgogIGV4aXQgMQpmaQoKIyBDbGVhbiB1cA==\" -echo \"$DEPLOY_B64\" | base64 -d | ssh -p 22222 -i \"$key_path\" \"${production_user}@${production_host}\" \"IMAGE_TAG='${GITHUB_REF_NAME}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh\" -" - name: Production health check + auto rollback if: success() shell: sh @@ -816,13 +827,13 @@ echo \"$DEPLOY_B64\" | base64 -d | ssh -p 22222 -i \"$key_path\" \"${production_ PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }} ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - run: 'set -eu + run: | + set -eu echo "==========================================" echo " Production 健康检查 + 自动回滚" echo "==========================================" echo "" bash scripts/ci_production_healthcheck.sh - ' - name: Job duration summary if: always() shell: sh @@ -833,18 +844,18 @@ echo \"$DEPLOY_B64\" | base64 -d | ssh -p 22222 -i \"$key_path\" \"${production_ shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=success JOB_NAME="Deploy Production" python3 scripts/ci_notify.py - ' - name: Notify on failure continue-on-error: true if: failure() shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Deploy Production" python3 scripts/ci_notify.py - ' production-e2e: name: Production Browser E2E @@ -864,17 +875,18 @@ echo \"$DEPLOY_B64\" | base64 -d | ssh -p 22222 -i \"$key_path\" \"${production_ run: bash scripts/ci/step_timer_start.sh - name: Run production browser E2E shell: sh - run: "set -eu -docker run --rm --ipc=host \\ - -e E2E_BASE_URL=https://saas.xiaoxiajianji.com \\ - -e E2E_API_BASE=https://api.xiaoxiajianji.com/api/v1 \\ - -e E2E_BROWSER_CHANNEL=chromium \\ - -e PLAYWRIGHT_HEADLESS=1 \\ - -v \"$PWD:/workspace\" \\ - -w /workspace/apps/web \\ - git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \\ - sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts' -" + run: | + set -eu + docker run --rm --ipc=host \ + -e E2E_BASE_URL=https://saas.xiaoxiajianji.com \ + -e E2E_API_BASE=https://api.xiaoxiajianji.com/api/v1 \ + -e E2E_BROWSER_CHANNEL=chromium \ + -e PLAYWRIGHT_HEADLESS=1 \ + -v "$PWD:/workspace" \ + -w /workspace/apps/web \ + git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \ + sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts' + - name: Job duration summary if: always() shell: sh @@ -885,9 +897,9 @@ docker run --rm --ipc=host \\ shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="Production Browser E2E" python3 scripts/ci_notify.py - ' acr-cleanup: name: ACR Image Cleanup @@ -914,16 +926,17 @@ docker run --rm --ipc=host \\ env: ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} - run: "set -eu -python3 scripts/ci/acr_cleanup.py \\ - --registry \"$ACR_REGISTRY\" \\ - --namespace \"$ACR_NAMESPACE\" \\ - --username \"$ACR_USERNAME\" \\ - --password \"$ACR_PASSWORD\" \\ - --keep 20 \\ - --pr-days 7 \\ - --execute -" + run: | + set -eu + python3 scripts/ci/acr_cleanup.py \ + --registry "$ACR_REGISTRY" \ + --namespace "$ACR_NAMESPACE" \ + --username "$ACR_USERNAME" \ + --password "$ACR_PASSWORD" \ + --keep 20 \ + --pr-days 7 \ + --execute + - name: Job duration summary if: always() shell: sh @@ -934,6 +947,6 @@ python3 scripts/ci/acr_cleanup.py \\ shell: sh env: CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }} - run: 'set +e + run: | + set +e NOTIFY_MODE=failure JOB_NAME="ACR Image Cleanup" python3 scripts/ci_notify.py - '