df08161630
CI/CD Pipeline / Frontend Lint (push) Successful in 58s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 1m36s
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 7s
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
1. 给auto-merge.yml的两个merge step添加GITEA_API_TOKEN环境变量,复用REVIEW_GITEA_TOKEN secret 2. 修复tests.yml的checkout步骤GITHUB_TOKEN和Python命令 3. 给main分支ci-cd.yml的单元测试增加Redis服务,修复Celery相关测试失败
658 lines
33 KiB
YAML
Executable File
658 lines
33 KiB
YAML
Executable File
name: CI/CD Pipeline
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- develop
|
|
- 'feature/**'
|
|
- 'bugfix/**'
|
|
- 'hotfix/**'
|
|
- 'release/**'
|
|
tags:
|
|
- 'v*'
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- develop
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
validate:
|
|
name: Validate Code Quality And Tests
|
|
runs-on: ubuntu-22.04
|
|
|
|
env:
|
|
DATABASE_URL: postgresql+psycopg://postgres:postgres@127.0.0.1:5433/xiaoxia_saas
|
|
USE_IN_MEMORY_DB: "false"
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
shell: sh
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
python3 - <<'PY'
|
|
import io, os, tarfile, time, urllib.request, urllib.error
|
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
|
last_err = None
|
|
for attempt in range(5):
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
archive = response.read()
|
|
break
|
|
except urllib.error.HTTPError as e:
|
|
last_err = e
|
|
if e.code >= 500 and attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
except Exception as e:
|
|
last_err = e
|
|
if attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
else:
|
|
raise last_err
|
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
|
for member in tar.getmembers():
|
|
name = member.name
|
|
if name == root_prefix[:-1]:
|
|
continue
|
|
if name.startswith(root_prefix):
|
|
member.name = name[len(root_prefix):]
|
|
if member.name:
|
|
tar.extract(member, '.')
|
|
PY
|
|
|
|
- name: Verify CI environment
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
python --version
|
|
python3 -m pip --version
|
|
echo "CI environment is ready"
|
|
|
|
- name: Install dependencies
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
python3 -m pip install -q -r requirements-base.txt
|
|
python3 -m pip install -q -r requirements.txt
|
|
python3 -m pip install -q -r requirements-dev.txt
|
|
python3 -m black --version
|
|
python3 -m isort --version-number
|
|
python3 -m flake8 --version
|
|
bandit --version
|
|
pytest --version
|
|
|
|
- name: Run code quality checks
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
python3 -m compileall -q alembic apps packages tests scripts
|
|
python3 -m black --check --fast alembic apps packages tests scripts
|
|
python3 -m isort --check-only alembic apps packages tests scripts
|
|
python3 -m flake8 apps packages tests --count --statistics
|
|
|
|
- name: Run security scan
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
bandit -r apps packages -q -ll
|
|
|
|
- name: Validate release scripts syntax
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
bash -n scripts/backup_postgres.sh
|
|
bash -n scripts/restore_postgres_plan.sh
|
|
bash -n scripts/init_production_env.sh
|
|
|
|
- name: Validate Alembic migrations
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
python3 -m alembic upgrade head --sql > /tmp/alembic-upgrade.sql
|
|
test -s /tmp/alembic-upgrade.sql
|
|
grep -q "Running upgrade" /tmp/alembic-upgrade.sql
|
|
python3 scripts/check_schema_metadata.py
|
|
|
|
- name: Start Redis for unit tests
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker rm -f ci-redis-validate 2>/dev/null || true
|
|
docker run -d --name ci-redis-validate \
|
|
-p 6379:6379 \
|
|
--health-cmd "redis-cli ping" \
|
|
--health-interval 2s \
|
|
--health-timeout 2s \
|
|
--health-retries 15 \
|
|
redis:7-alpine
|
|
for i in $(seq 1 20); do
|
|
if docker inspect --format='{{.State.Health.Status}}' ci-redis-validate 2>/dev/null | grep -q healthy; then
|
|
echo "Redis is ready"
|
|
break
|
|
fi
|
|
echo "Waiting for Redis... ($i/20)"
|
|
sleep 1
|
|
done
|
|
docker inspect --format='{{.State.Health.Status}}' ci-redis-validate | grep -q healthy
|
|
- name: Run unit tests
|
|
shell: sh
|
|
env:
|
|
USE_IN_MEMORY_DB: "true"
|
|
run: |
|
|
set -eu
|
|
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/unit -q
|
|
|
|
- name: Start PostgreSQL for integration tests
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker rm -f ci-pg-validate 2>/dev/null || true
|
|
docker run -d --name ci-pg-validate \
|
|
-e POSTGRES_USER=postgres \
|
|
-e POSTGRES_PASSWORD=postgres \
|
|
-e POSTGRES_DB=xiaoxia_saas \
|
|
-p 5433:5432 \
|
|
--health-cmd "pg_isready -U postgres" \
|
|
--health-interval 5s \
|
|
--health-timeout 5s \
|
|
--health-retries 12 \
|
|
postgres:16
|
|
for i in $(seq 1 30); do
|
|
if docker inspect --format='{{.State.Health.Status}}' ci-pg-validate 2>/dev/null | grep -q healthy; then
|
|
echo "PostgreSQL is ready"
|
|
break
|
|
fi
|
|
echo "Waiting for PostgreSQL... ($i/30)"
|
|
sleep 2
|
|
done
|
|
docker inspect --format='{{.State.Health.Status}}' ci-pg-validate | grep -q healthy
|
|
|
|
- name: Apply migrations for integration tests
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m alembic upgrade head
|
|
|
|
- name: Run integration tests
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m pytest tests/integration -q --timeout=60 -x
|
|
|
|
- name: Cleanup PostgreSQL
|
|
if: always()
|
|
shell: sh
|
|
run: |
|
|
docker rm -f ci-pg-validate 2>/dev/null || true
|
|
echo "PostgreSQL container cleaned up"
|
|
|
|
- name: Build summary
|
|
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
echo "Build completed successfully!"
|
|
echo "Branch: ${GITHUB_REF_NAME}"
|
|
echo "Commit: ${GITHUB_SHA}"
|
|
|
|
frontend-lint:
|
|
name: Frontend Lint
|
|
runs-on: ubuntu-22.04
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
shell: sh
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
archive_url="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz"
|
|
for i in 1 2 3 4 5; do
|
|
if wget --header="Authorization: token ${GITHUB_TOKEN}" -O /tmp/repo.tar.gz "$archive_url" 2>&1; then
|
|
break
|
|
fi
|
|
if [ "$i" -lt 5 ]; then
|
|
wait=$((2 ** i))
|
|
echo "Checkout failed (attempt $i/5), retrying in ${wait}s..."
|
|
sleep "$wait"
|
|
else
|
|
echo "Checkout failed after 5 attempts"
|
|
exit 1
|
|
fi
|
|
done
|
|
tar -xzf /tmp/repo.tar.gz --strip-components=1 -C .
|
|
rm -f /tmp/repo.tar.gz
|
|
|
|
- name: Install dependencies
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
docker.m.daocloud.io/library/node:20 \
|
|
sh -lc 'npm ci'
|
|
|
|
- name: Run ESLint
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
docker.m.daocloud.io/library/node:20 \
|
|
sh -lc 'npx eslint src --ext .ts,.tsx --max-warnings 50'
|
|
|
|
- name: Run TypeScript type check
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
docker.m.daocloud.io/library/node:20 \
|
|
sh -lc 'npx tsc --noEmit'
|
|
|
|
- name: Run Prettier check
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
docker.m.daocloud.io/library/node:20 \
|
|
sh -lc 'npx prettier --check "src/**/*.{ts,tsx,md}"'
|
|
|
|
- name: Run Vitest tests
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
docker.m.daocloud.io/library/node:20 \
|
|
sh -lc 'npx vitest run src/test'
|
|
deploy-staging:
|
|
name: Build & Push Staging (Watchtower auto-deploy)
|
|
runs-on: saas
|
|
needs: [validate, frontend-lint]
|
|
|
|
if: github.ref_name == 'main' || github.ref_name == 'develop' || startsWith(github.ref_name, 'feature/')
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
shell: sh
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
python3 - <<'INNERPY'
|
|
import io, os, tarfile, time, urllib.request, urllib.error
|
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
|
last_err = None
|
|
for attempt in range(5):
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
archive = response.read()
|
|
break
|
|
except urllib.error.HTTPError as e:
|
|
last_err = e
|
|
if e.code >= 500 and attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
except Exception as e:
|
|
last_err = e
|
|
if attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
else:
|
|
raise last_err
|
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
|
for member in tar.getmembers():
|
|
name = member.name
|
|
if name == root_prefix[:-1]:
|
|
continue
|
|
if name.startswith(root_prefix):
|
|
member.name = name[len(root_prefix):]
|
|
if member.name:
|
|
tar.extract(member, '.')
|
|
INNERPY
|
|
|
|
- name: Build and push all images to Gitea Registry
|
|
shell: sh
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
chmod +x scripts/build_release_images.sh
|
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
|
scripts/build_release_images.sh "${GITHUB_SHA}"
|
|
|
|
- name: Tag and push :staging images (Watchtower auto-update)
|
|
shell: sh
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
REGISTRY="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas"
|
|
if [ -n "${REGISTRY_TOKEN:-}" ]; then
|
|
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null
|
|
fi
|
|
for svc in api worker web; do
|
|
docker tag "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}" "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
|
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
|
done
|
|
echo "All :staging images pushed. Watchtower will auto-deploy within 60s."
|
|
|
|
- name: Wait for Watchtower update + smoke test
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
echo "Waiting 90s for Watchtower to detect new image and restart containers..."
|
|
sleep 90
|
|
|
|
echo "--- Smoke test 1: Health check ---"
|
|
for i in $(seq 1 12); do
|
|
HEALTH=$(curl -sf --max-time 10 https://staging-api.xiaoxiajianji.com/health) && break
|
|
echo " Attempt $i/12: not ready yet, waiting 5s..."
|
|
sleep 5
|
|
done
|
|
if [ -z "$HEALTH" ]; then
|
|
echo "FAIL: health endpoint unreachable after 60s"
|
|
exit 1
|
|
fi
|
|
echo "Health OK: $HEALTH"
|
|
|
|
echo "--- Smoke test 2: Login API (expect 401) ---"
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -X POST \
|
|
https://staging-api.xiaoxiajianji.com/api/v1/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"email":"smoke@test.com","password":"wrong"}')
|
|
if [ "$HTTP_CODE" != "401" ] && [ "$HTTP_CODE" != "422" ]; then
|
|
echo "FAIL: login returned HTTP $HTTP_CODE (expected 401 or 422)"
|
|
exit 1
|
|
fi
|
|
echo "Login API OK: HTTP $HTTP_CODE"
|
|
|
|
echo "--- Smoke test 3: API docs endpoint ---"
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://staging-api.xiaoxiajianji.com/docs)
|
|
if [ "$HTTP_CODE" != "200" ]; then
|
|
echo "FAIL: /docs returned HTTP $HTTP_CODE (expected 200)"
|
|
exit 1
|
|
fi
|
|
echo "Docs endpoint OK: HTTP $HTTP_CODE"
|
|
|
|
echo "--- Smoke test 4: Web frontend ---"
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://staging.xiaoxiajianji.com/)
|
|
if [ "$HTTP_CODE" != "200" ]; then
|
|
echo "FAIL: web frontend returned HTTP $HTTP_CODE (expected 200)"
|
|
exit 1
|
|
fi
|
|
echo "Web frontend OK: HTTP $HTTP_CODE"
|
|
|
|
echo ""
|
|
echo "=== All smoke tests passed! ==="
|
|
echo "Branch: ${GITHUB_REF_NAME}"
|
|
echo "Commit: ${GITHUB_SHA}"
|
|
|
|
|
|
staging-e2e:
|
|
name: Staging E2E Tests
|
|
runs-on: saas
|
|
if: github.ref_name == 'develop' || github.ref_name == 'main'
|
|
needs: deploy-staging
|
|
|
|
steps:
|
|
- name: Install SSH client
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
apt-get update -qq && apt-get install -y -qq openssh-client >/dev/null 2>&1
|
|
echo "openssh-client installed"
|
|
|
|
- name: Run Playwright E2E on staging server
|
|
shell: sh
|
|
env:
|
|
STAGING_SSH_HOST: ${{ secrets.STAGING_SSH_HOST }}
|
|
STAGING_SSH_USER: ${{ secrets.STAGING_SSH_USER }}
|
|
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
|
|
run: |
|
|
set -eu
|
|
staging_host="${STAGING_SSH_HOST:-47.98.113.167}"
|
|
staging_user="${STAGING_SSH_USER:-root}"
|
|
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
|
|
key_path="/root/.ssh/xiaoxia_runtime_builder"
|
|
elif [ -n "${STAGING_SSH_KEY:-}" ]; then
|
|
key_path="$HOME/.ssh/id_ed25519"
|
|
printf '%s\n' "$STAGING_SSH_KEY" > "$key_path"
|
|
chmod 600 "$key_path"
|
|
else
|
|
echo "ERROR: No SSH key available"
|
|
exit 1
|
|
fi
|
|
ssh-keyscan -H "$staging_host" >> ~/.ssh/known_hosts
|
|
|
|
# 在业务服务器上跑 Playwright E2E(用 host 网络访问 staging 3001/8000 端口)
|
|
ssh -i "$key_path" "$staging_user@$staging_host" '
|
|
cd /var/lib/xiaoxia-saas-staging/repo
|
|
docker run --rm \
|
|
-e E2E_BASE_URL=http://127.0.0.1:3001 \
|
|
-e E2E_API_BASE=http://127.0.0.1:8000/api/v1 \
|
|
-e E2E_BROWSER_CHANNEL=chromium \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
--network host \
|
|
mcr.microsoft.com/playwright:v1.45.0-jammy \
|
|
sh -lc "npm ci && npx playwright test --reporter=line --project=chromium"
|
|
'
|
|
|
|
build-production-runtime-images:
|
|
name: Build Production Runtime Images
|
|
runs-on: saas
|
|
needs: [validate, frontend-lint]
|
|
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
shell: sh
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
python3 - <<'PY'
|
|
import io, os, tarfile, time, urllib.request, urllib.error
|
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
|
last_err = None
|
|
for attempt in range(5):
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
archive = response.read()
|
|
break
|
|
except urllib.error.HTTPError as e:
|
|
last_err = e
|
|
if e.code >= 500 and attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
except Exception as e:
|
|
last_err = e
|
|
if attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
else:
|
|
raise last_err
|
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
|
for member in tar.getmembers():
|
|
name = member.name
|
|
if name == root_prefix[:-1]:
|
|
continue
|
|
if name.startswith(root_prefix):
|
|
member.name = name[len(root_prefix):]
|
|
if member.name:
|
|
tar.extract(member, '.')
|
|
PY
|
|
|
|
- name: Build and push all images (api + worker + web, with buildx cache)
|
|
shell: sh
|
|
env:
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
chmod +x scripts/build_release_images.sh
|
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
|
|
|
- name: Cleanup old Docker images
|
|
if: always()
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
if [ -f scripts/cleanup_old_images.sh ]; then
|
|
chmod +x scripts/cleanup_old_images.sh
|
|
scripts/cleanup_old_images.sh
|
|
else
|
|
echo "Cleanup script not found, doing basic prune..."
|
|
docker image prune -f 2>/dev/null || true
|
|
fi
|
|
echo "Disk usage after cleanup:"
|
|
df -h / | tail -1
|
|
|
|
deploy-production:
|
|
name: Deploy Production
|
|
runs-on: saas
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
needs: build-production-runtime-images
|
|
|
|
steps:
|
|
- name: Install SSH client
|
|
shell: sh
|
|
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:
|
|
PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
|
|
PRODUCTION_SSH_USER: ${{ secrets.PRODUCTION_SSH_USER }}
|
|
PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
production_host="${PRODUCTION_SSH_HOST:-47.98.113.167}"
|
|
production_user="${PRODUCTION_SSH_USER:-root}"
|
|
mkdir -p ~/.ssh
|
|
if [ -f /root/.ssh/xiaoxia_runtime_builder ]; then
|
|
key_path="/root/.ssh/xiaoxia_runtime_builder"
|
|
elif [ -n "${PRODUCTION_SSH_KEY:-}" ]; then
|
|
key_path="$HOME/.ssh/id_ed25519"
|
|
printf '%s\n' "$PRODUCTION_SSH_KEY" > "$key_path"
|
|
chmod 600 "$key_path"
|
|
else
|
|
echo "ERROR: No SSH key available"
|
|
exit 1
|
|
fi
|
|
ssh-keyscan -H "$production_host" >> ~/.ssh/known_hosts
|
|
|
|
# Registry 方式部署脚本(base64 编码避免转义问题)
|
|
DEPLOY_B64="IyEvYmluL3NoCnNldCAtZXUKCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KIyBQcm9kdWN0aW9uIOmDqOe9suiEmuacrCAtIFJlZ2lzdHJ5IOaWueW8jwojIOeUqOazle+8mklNQUdFX1RBRz08dmVyc2lvbj4gUkVHSVNUUllfVE9LRU49PHRva2VuPiBzaCBkZXBsb3ktcHJvZHVjdGlvbi1yZWdpc3RyeS5zaAojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgpJTUFHRV9UQUc9IiR7SU1BR0VfVEFHOi19IgpSRUdJU1RSWT0iJHtSRUdJU1RSWTotZ2l0LnhpYW94aWFqaWFuamkuY29tL3hpYW94aWEveGlhb3hpYS1zYWFzfSIKUkVHSVNUUllfVVNFUj0iJHtSRUdJU1RSWV9VU0VSOi14aWFveGlhfSIKUkVHSVNUUllfVE9LRU49IiR7UkVHSVNUUllfVE9LRU46LX0iCgpFTlZfRklMRT0iJHtFTlZfRklMRTotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vLmVudn0iCkdFTkVSQVRFRF9ESVI9IiR7R0VORVJBVEVEX0RJUjotL3Zhci9saWIveGlhb3hpYS1zYWFzLXByb2R1Y3Rpb24vZ2VuZXJhdGVkfSIKTEVHQUNZX0FTU0VUU19ESVI9IiR7TEVHQUNZX0FTU0VUU19ESVI6LS92YXIvbGliL3hpYW94aWEtc2Fhcy1wcm9kdWN0aW9uL2xlZ2FjeS1hc3NldHN9IgoKaWYgWyAteiAiJElNQUdFX1RBRyIgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBJTUFHRV9UQUcgaXMgcmVxdWlyZWQiCiAgZXhpdCAxCmZpCgp0ZXN0IC1mICIkRU5WX0ZJTEUiCm1rZGlyIC1wICIkR0VORVJBVEVEX0RJUiIKbWtkaXIgLXAgIiRMRUdBQ1lfQVNTRVRTX0RJUiIKCiMgLS0tLSDnmbvlvZUgUmVnaXN0cnkgLS0tLQppZiBbIC1uICIkUkVHSVNUUllfVE9LRU4iIF07IHRoZW4KICBlY2hvICJMb2dnaW5nIGluIHRvIHJlZ2lzdHJ5OiAkUkVHSVNUUlkiCiAgUkVHSVNUUllfSE9TVD0kKGVjaG8gIiRSRUdJU1RSWSIgfCBjdXQgLWQvIC1mMSkKICBwcmludGYgJXMgIiRSRUdJU1RSWV9UT0tFTiIgfCBkb2NrZXIgbG9naW4gIiRSRUdJU1RSWV9IT1NUIiAtdSAiJFJFR0lTVFJZX1VTRVIiIC0tcGFzc3dvcmQtc3RkaW4gMj4vZGV2L251bGwgfHwgewogICAgZWNobyAiV0FSTjogZG9ja2VyIGxvZ2luIGZhaWxlZCwgd2lsbCB0cnkgdG8gcHVsbCBhbnl3YXkiCiAgfQpmaQoKIyAtLS0tIFB1bGwg5LiJ6ZWc5YOPIC0tLS0KUkVHSVNUUllfQVBJPSIke1JFR0lTVFJZfS94aWFveGlhLXNhYXMtYXBpOiR7SU1BR0VfVEFHfSIKUkVHSVNUUllfV09SS0VSPSIke1JFR0lTVFJZfS94aWFveGlhLXNhYXMtd29ya2VyOiR7SU1BR0VfVEFHfSIKUkVHSVNUUllfV0VCPSIke1JFR0lTVFJZfS94aWFveGlhLXNhYXMtd2ViOiR7SU1BR0VfVEFHfSIKCkxPQ0FMX0FQST0ieGlhb3hpYS1zYWFzLWFwaToke0lNQUdFX1RBR30iCkxPQ0FMX1dPUktFUj0ieGlhb3hpYS1zYWFzLXdvcmtlcjoke0lNQUdFX1RBR30iCkxPQ0FMX1dFQj0ieGlhb3hpYS1zYWFzLXdlYjoke0lNQUdFX1RBR30iCgplY2hvICJQdWxsaW5nIEFQSSBpbWFnZS4uLiIKZG9ja2VyIHB1bGwgIiRSRUdJU1RSWV9BUEkiCmVjaG8gIlB1bGxpbmcgV29ya2VyIGltYWdlLi4uIgpkb2NrZXIgcHVsbCAiJFJFR0lTVFJZX1dPUktFUiIKZWNobyAiUHVsbGluZyBXZWIgaW1hZ2UuLi4iCmRvY2tlciBwdWxsICIkUkVHSVNUUllfV0VCIgoKIyAtLS0tIFJlLXRhZyDmiJDmnKzlnLDlkI0gLS0tLQpkb2NrZXIgdGFnICIkUkVHSVNUUllfQVBJIiAiJExPQ0FMX0FQSSIKZG9ja2VyIHRhZyAiJFJFR0lTVFJZX1dPUktFUiIgIiRMT0NBTF9XT1JLRVIiCmRvY2tlciB0YWcgIiRSRUdJU1RSWV9XRUIiICIkTE9DQUxfV0VCIgplY2hvICJBbGwgaW1hZ2VzIHB1bGxlZCBhbmQgdGFnZ2VkLiIKCiMgLS0tLSDlpIfku73ml6fniYggYXNzZXRz77yI6YOo572y5pyf6Ze057yT5a2Y55So5oi35LiNIDQwNO+8iSAtLS0tCmVjaG8gIkJhY2tpbmcgdXAgbGVnYWN5IGFzc2V0cyBmcm9tIGN1cnJlbnQgd2ViIGNvbnRhaW5lci4uLiIKaWYgZG9ja2VyIGluc3BlY3QgeGlhb3hpYS13ZWItcHJvZHVjdGlvbiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICBfdG1wZGlyPSIvdG1wL2xlZ2FjeS1hc3NldHMtJCQiCiAgcm0gLXJmICIkX3RtcGRpciIKICBta2RpciAtcCAiJF90bXBkaXIiCiAgZG9ja2VyIGNwIHhpYW94aWEtd2ViLXByb2R1Y3Rpb246L3Vzci9zaGFyZS9uZ2lueC9odG1sL2Fzc2V0cy8uICIkX3RtcGRpci8iIDI+L2Rldi9udWxsIHx8IHRydWUKICAjIOWQiOW5tuWIsCBMRUdBQ1lfQVNTRVRTX0RJUu+8iOS/neeVmeaJgOacieWOhuWPsueJiOacrOeahCBhc3NldHPvvIkKICBpZiBbIC1kICIkX3RtcGRpciIgXSAmJiBbICIkKGxzIC1BICIkX3RtcGRpciIgMj4vZGV2L251bGwpIiBdOyB0aGVuCiAgICBjcCAtYW4gIiRfdG1wZGlyIi8uICIkTEVHQUNZX0FTU0VUU19ESVIiLyAyPi9kZXYvbnVsbCB8fCB0cnVlCiAgICBlY2hvICJMZWdhY3kgYXNzZXRzIGJhY2tlZCB1cDogJChscyAiJF90bXBkaXIiIHwgd2MgLWwpIGZpbGVzIgogIGZpCiAgcm0gLXJmICIkX3RtcGRpciIKZWxzZQogIGVjaG8gIk5vIGV4aXN0aW5nIHdlYiBjb250YWluZXIsIHNraXBwaW5nIGxlZ2FjeSBhc3NldHMgYmFja3VwIgpmaQoKIyDmuIXnkIbotoXov4cgNyDlpKnnmoTml6cgYXNzZXRzIOaWh+S7tu+8iOmBv+WFjeaXoOmZkOWinumVv++8iQppZiBbIC1kICIkTEVHQUNZX0FTU0VUU19ESVIiIF07IHRoZW4KICBmaW5kICIkTEVHQUNZX0FTU0VUU19ESVIiIC10eXBlIGYgLW10aW1lICs3IC1kZWxldGUgMj4vZGV2L251bGwgfHwgdHJ1ZQogIGVjaG8gIkxlZ2FjeSBhc3NldHMgY2xlYW51cCBkb25lIChyZXRhaW4gNyBkYXlzKSIKZmkKCiMgLS0tLSDnoa7kv53ln7rnoYDorr7mlr3lrrnlmajlnKjov5DooYwgLS0tLQplY2hvICJDaGVja2luZyBpbmZyYXN0cnVjdHVyZSBjb250YWluZXJzLi4uIgpmb3IgYyBpbiB4aWFveGlhLXBvc3RncmVzLXByb2R1Y3Rpb24geGlhb3hpYS1yZWRpcy1wcm9kdWN0aW9uOyBkbwogIGlmICEgZG9ja2VyIGluc3BlY3QgIiRjIiA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIkVSUk9SOiBSZXF1aXJlZCBjb250YWluZXIgbm90IGZvdW5kOiAkYyIKICAgIGV4aXQgMQogIGZpCiAgc3RhdGU9JChkb2NrZXIgaW5zcGVjdCAtZiAne3suU3RhdGUuU3RhdHVzfX0nICIkYyIpCiAgaWYgWyAiJHN0YXRlIiAhPSAicnVubmluZyIgXTsgdGhlbgogICAgZWNobyAiRVJST1I6IENvbnRhaW5lciBub3QgcnVubmluZzogJGMgKCRzdGF0ZSkiCiAgICBleGl0IDEKICBmaQpkb25lCgojIC0tLS0g56Gu5L+d55Sf5Lqn572R57uc5a2Y5ZyoIC0tLS0KZG9ja2VyIG5ldHdvcmsgY3JlYXRlIHhpYW94aWEtbmV0LXByb2R1Y3Rpb24gMj4vZGV2L251bGwgfHwgdHJ1ZQoKIyAtLS0tIOaJp+ihjOaVsOaNruW6kyBNaWdyYXRpb24gLS0tLQplY2hvICJSdW5uaW5nIGRhdGFiYXNlIG1pZ3JhdGlvbnMuLi4iCmRvY2tlciBydW4gLS1ybSBcCiAgLS1lbnYtZmlsZSAiJEVOVl9GSUxFIiBcCiAgLS1uZXR3b3JrIHhpYW94aWEtbmV0LXByb2R1Y3Rpb24gXAogIC1lIEFQUF9FTlY9cHJvZHVjdGlvbiBcCiAgIiRMT0NBTF9BUEkiIHNoIC1jICJjZCAvYXBwICYmIGFsZW1iaWMgdXBncmFkZSBoZWFkIgplY2hvICJNaWdyYXRpb25zIGNvbXBsZXRlZC4iCgojIC0tLS0g5YGc5q2i5pen5a655ZmoIC0tLS0KZWNobyAiU3RvcHBpbmcgb2xkIGNvbnRhaW5lcnMuLi4iCmRvY2tlciBybSAtZiB4aWFveGlhLWFwaS1wcm9kdWN0aW9uIDI+L2Rldi9udWxsIHx8IHRydWUKZG9ja2VyIHJtIC1mIHhpYW94aWEtd29ya2VyLXByb2R1Y3Rpb24gMj4vZGV2L251bGwgfHwgdHJ1ZQpkb2NrZXIgcm0gLWYgeGlhb3hpYS13ZWItcHJvZHVjdGlvbiAyPi9kZXYvbnVsbCB8fCB0cnVlCgojIC0tLS0g5pel5b+X6YWN572u77yI5omA5pyJ5a655Zmo5YWx55So77yJIC0tLS0KTE9HX09QVFM9Ii0tbG9nLWRyaXZlciBqc29uLWZpbGUgLS1sb2ctb3B0IG1heC1zaXplPTUwbSAtLWxvZy1vcHQgbWF4LWZpbGU9MyIKCiMgLS0tLSDlkK/liqggQVBJIC0tLS0KZWNobyAiU3RhcnRpbmcgQVBJIGNvbnRhaW5lci4uLiIKZG9ja2VyIHJ1biAtZCBcCiAgLS1uYW1lIHhpYW94aWEtYXBpLXByb2R1Y3Rpb24gXAogIC0tZW52LWZpbGUgIiRFTlZfRklMRSIgXAogIC0tbmV0d29yayB4aWFveGlhLW5ldC1wcm9kdWN0aW9uIFwKICAtcCAxMjcuMC4wLjE6ODAwMTo4MDAwIFwKICAtZSBBUFBfRU5WPXByb2R1Y3Rpb24gXAogIC1lIEFQUF9WRVJTSU9OPSIkSU1BR0VfVEFHIiBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX0RJUj0vYXBwL2dlbmVyYXRlZCBcCiAgLWUgR0VORVJBVEVEX0ZJTEVTX1VSTF9QUkVGSVg9L2dlbmVyYXRlZC1maWxlcyBcCiAgLWUgUFVCTElDX0FQSV9CQVNFX1VSTD1odHRwczovL2FwaS54aWFveGlhamlhbmppLmNvbSBcCiAgLXYgIiRHRU5FUkFURURfRElSOi9hcHAvZ2VuZXJhdGVkIiBcCiAgLS1yZXN0YXJ0IHVubGVzcy1zdG9wcGVkIFwKICAtLWNwdXMgMiBcCiAgLS1tZW1vcnkgMmcgXAogIC0taGVhbHRoLWNtZCAicHl0aG9uIC1jIFwiaW1wb3J0IHVybGxpYi5yZXF1ZXN0OyB1cmxsaWIucmVxdWVzdC51cmxvcGVuKCdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJywgdGltZW91dD01KVwiIiBcCiAgLS1oZWFsdGgtaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDEwcyBcCiAgLS1oZWFsdGgtcmV0cmllcyAzIFwKICAtLWhlYWx0aC1zdGFydC1wZXJpb2QgNDBzIFwKICAkTE9HX09QVFMgXAogICIkTE9DQUxfQVBJIgoKIyAtLS0tIOWQr+WKqCBXb3JrZXIgLS0tLQplY2hvICJTdGFydGluZyBXb3JrZXIgY29udGFpbmVyLi4uIgpkb2NrZXIgcnVuIC1kIFwKICAtLW5hbWUgeGlhb3hpYS13b3JrZXItcHJvZHVjdGlvbiBcCiAgLS1lbnYtZmlsZSAiJEVOVl9GSUxFIiBcCiAgLS1uZXR3b3JrIHhpYW94aWEtbmV0LXByb2R1Y3Rpb24gXAogIC1lIEFQUF9FTlY9cHJvZHVjdGlvbiBcCiAgLWUgQVBQX1ZFUlNJT049IiRJTUFHRV9UQUciIFwKICAtZSBXT1JLRVJfQ09OQ1VSUkVOQ1k9MSBcCiAgLWUgV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9MTAwIFwKICAtZSBHRU5FUkFURURfRklMRVNfRElSPS9hcHAvZ2VuZXJhdGVkIFwKICAtZSBHRU5FUkFURURfRklMRVNfVVJMX1BSRUZJWD0vZ2VuZXJhdGVkLWZpbGVzIFwKICAtZSBQVUJMSUNfQVBJX0JBU0VfVVJMPWh0dHBzOi8vYXBpLnhpYW94aWFqaWFuamkuY29tIFwKICAtdiAiJEdFTkVSQVRFRF9ESVI6L2FwcC9nZW5lcmF0ZWQiIFwKICAtLXJlc3RhcnQgdW5sZXNzLXN0b3BwZWQgXAogIC0tY3B1cyAyIFwKICAtLW1lbW9yeSAyZyBcCiAgLS1oZWFsdGgtY21kICJzaCAtYyBcImdyZXAgLXEgY2VsZXJ5IC9wcm9jLzEvY21kbGluZSB8fCBleGl0IDFcIiIgXAogIC0taGVhbHRoLWludGVydmFsIDMwcyBcCiAgLS1oZWFsdGgtdGltZW91dCAxMHMgXAogIC0taGVhbHRoLXJldHJpZXMgMyBcCiAgLS1oZWFsdGgtc3RhcnQtcGVyaW9kIDMwcyBcCiAgJExPR19PUFRTIFwKICAiJExPQ0FMX1dPUktFUiIKCiMgLS0tLSDlkK/liqggV2ViIC0tLS0KIyBMZWdhY3kgYXNzZXRzIOaMgui9veWIsCAvdXNyL3NoYXJlL25naW54L2h0bWwvYXNzZXRzLWxlZ2FjeS9hc3NldHMvCiMgbmdpbngg6YWN572u5LitIGFzc2V0cyBsb2NhdGlvbiDmnIkgZmFsbGJhY2sg6YC76L6RCkxFR0FDWV9WT0xVTUU9IiIKaWYgWyAtZCAiJExFR0FDWV9BU1NFVFNfRElSIiBdICYmIFsgIiQobHMgLUEgIiRMRUdBQ1lfQVNTRVRTX0RJUiIgMj4vZGV2L251bGwpIiBdOyB0aGVuCiAgTEVHQUNZX1ZPTFVNRT0iLXYgJHtMRUdBQ1lfQVNTRVRTX0RJUn06L3Vzci9zaGFyZS9uZ2lueC9odG1sL2Fzc2V0cy1sZWdhY3kvYXNzZXRzOnJvIgogIGVjaG8gIldlYiBjb250YWluZXI6IGxlZ2FjeSBhc3NldHMgbW91bnRlZCAoZmFsbGJhY2spIgplbHNlCiAgZWNobyAiV2ViIGNvbnRhaW5lcjogbm8gbGVnYWN5IGFzc2V0cyB0byBtb3VudCIKZmkKCmVjaG8gIlN0YXJ0aW5nIFdlYiBjb250YWluZXIuLi4iCmRvY2tlciBydW4gLWQgXAogIC0tbmFtZSB4aWFveGlhLXdlYi1wcm9kdWN0aW9uIFwKICAtLW5ldHdvcmsgeGlhb3hpYS1uZXQtcHJvZHVjdGlvbiBcCiAgLXAgMTI3LjAuMC4xOjMwMDI6ODAgXAogIC0tcmVzdGFydCB1bmxlc3Mtc3RvcHBlZCBcCiAgLS1jcHVzIDAuNSBcCiAgLS1tZW1vcnkgNTEybSBcCiAgJExFR0FDWV9WT0xVTUUgXAogIC0taGVhbHRoLWNtZCAid2dldCAtLXNwaWRlciAtcSBodHRwOi8vMTI3LjAuMC4xOjgwIiBcCiAgLS1oZWFsdGgtaW50ZXJ2YWwgMzBzIFwKICAtLWhlYWx0aC10aW1lb3V0IDVzIFwKICAtLWhlYWx0aC1yZXRyaWVzIDMgXAogICRMT0dfT1BUUyBcCiAgIiRMT0NBTF9XRUIiCgojIC0tLS0g562J5b6FIEFQSSDlgaXlurcgLS0tLQplY2hvICJXYWl0aW5nIGZvciBBUEkgdG8gYmVjb21lIGhlYWx0aHkuLi4iCmk9MAp3aGlsZSBbICIkaSIgLWx0IDQwIF07IGRvCiAgaWYgY3VybCAtc2YgLS1tYXgtdGltZSA1IGh0dHA6Ly8xMjcuMC4wLjE6ODAwMS9oZWFsdGggPi9kZXYvbnVsbCAyPiYxOyB0aGVuCiAgICBlY2hvICJBUEkgaXMgaGVhbHRoeSEiCiAgICBicmVhawogIGZpCiAgaT0kKChpICsgMSkpCiAgZWNobyAiICBXYWl0aW5nLi4uICgkaS80MCkiCiAgc2xlZXAgMwpkb25lCgppZiBbICIkaSIgLWdlIDQwIF07IHRoZW4KICBlY2hvICJFUlJPUjogQVBJIGRpZCBub3QgYmVjb21lIGhlYWx0aHkgd2l0aGluIDEyMHMiCiAgZG9ja2VyIGxvZ3MgLS10YWlsIDUwIHhpYW94aWEtYXBpLXByb2R1Y3Rpb24KICBleGl0IDEKZmkKCiMgLS0tLSDnrYnlvoUgV2ViIOWBpeW6tyAtLS0tCmVjaG8gIldhaXRpbmcgZm9yIFdlYiB0byBiZWNvbWUgaGVhbHRoeS4uLiIKaT0wCndoaWxlIFsgIiRpIiAtbHQgMTUgXTsgZG8KICBpZiBjdXJsIC1zZiAtLW1heC10aW1lIDUgaHR0cDovLzEyNy4wLjAuMTozMDAyLyA+L2Rldi9udWxsIDI+JjE7IHRoZW4KICAgIGVjaG8gIldlYiBpcyBoZWFsdGh5ISIKICAgIGJyZWFrCiAgZmkKICBpPSQoKGkgKyAxKSkKICBlY2hvICIgIFdhaXRpbmcuLi4gKCRpLzE1KSIKICBzbGVlcCAyCmRvbmUKCmlmIFsgIiRpIiAtZ2UgMTUgXTsgdGhlbgogIGVjaG8gIkVSUk9SOiBXZWIgZGlkIG5vdCBiZWNvbWUgaGVhbHRoeSB3aXRoaW4gMzBzIgogIGRvY2tlciBsb2dzIC0tdGFpbCAzMCB4aWFveGlhLXdlYi1wcm9kdWN0aW9uCiAgZXhpdCAxCmZpCgojIC0tLS0g5riF55CG5pen6ZWc5YOPIC0tLS0KZWNobyAiQ2xlYW5pbmcgdXAgb2xkIGltYWdlcy4uLiIKZG9ja2VyIGltYWdlIHBydW5lIC1hZiAtLWZpbHRlciAidW50aWw9MTY4aCIgMj4vZGV2L251bGwgfHwgdHJ1ZQpkb2NrZXIgYnVpbGRlciBwcnVuZSAtYWYgLS1maWx0ZXIgInVudGlsPTE2OGgiIDI+L2Rldi9udWxsIHx8IHRydWUKCmVjaG8gIiIKZWNobyAiPT09IFByb2R1Y3Rpb24gZGVwbG95bWVudCBjb21wbGV0ZSA9PT0iCmVjaG8gIkFQSTogICAgICBodHRwOi8vMTI3LjAuMC4xOjgwMDEiCmVjaG8gIldlYjogICAgICBodHRwOi8vMTI3LjAuMC4xOjMwMDIiCmVjaG8gIlZlcnNpb246ICAkSU1BR0VfVEFHIgpkb2NrZXIgcHMgLS1mb3JtYXQgInRhYmxlIHt7Lk5hbWVzfX1cdHt7LlN0YXR1c319XHR7ey5JbWFnZX19IiB8IGdyZXAgcHJvZHVjdGlvbgo="
|
|
|
|
echo "$DEPLOY_B64" | base64 -d | ssh -i "$key_path" "$production_user@$production_host" "IMAGE_TAG='${GITHUB_REF_NAME}' REGISTRY_TOKEN='${REGISTRY_TOKEN}' sh"
|
|
|
|
production-e2e:
|
|
name: Production Browser E2E
|
|
runs-on: saas
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
needs: deploy-production
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
shell: sh
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -eu
|
|
python3 - <<'PY'
|
|
import io, os, tarfile, time, urllib.request, urllib.error
|
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
|
# Retry up to 5 times with backoff for transient 5xx errors
|
|
last_err = None
|
|
for attempt in range(5):
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=120) as response:
|
|
archive = response.read()
|
|
break
|
|
except urllib.error.HTTPError as e:
|
|
last_err = e
|
|
if e.code >= 500 and attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
except Exception as e:
|
|
last_err = e
|
|
if attempt < 4:
|
|
wait = 2 ** attempt
|
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
else:
|
|
raise last_err
|
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
|
for member in tar.getmembers():
|
|
name = member.name
|
|
if name == root_prefix[:-1]:
|
|
continue
|
|
if name.startswith(root_prefix):
|
|
member.name = name[len(root_prefix):]
|
|
if member.name:
|
|
tar.extract(member, '.')
|
|
PY
|
|
|
|
- name: Run production browser E2E
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
docker run --rm \
|
|
-e E2E_BASE_URL=https://saas.xiaoxiajianji.com \
|
|
-e E2E_API_BASE=https://api.xiaoxiajianji.com/api/v1 \
|
|
-e E2E_BROWSER_CHANNEL=chromium \
|
|
-v "$PWD:/workspace" \
|
|
-w /workspace/apps/web \
|
|
mcr.microsoft.com/playwright:v1.45.0-jammy \
|
|
sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts'
|