From 14406059e00daaee78dabd7a957064b8fe184394 Mon Sep 17 00:00:00 2001 From: ConnorQi Date: Sun, 12 Jul 2026 00:33:46 +0800 Subject: [PATCH 1/3] Containerize Tencent Web deployment --- .github/workflows/deploy-tencent.yml | 133 ++++++++++++++++-------- README.md | 2 +- README.zh-CN.md | 2 +- apps/web/.dockerignore | 10 ++ apps/web/Dockerfile | 32 ++++++ apps/web/app/api/health/route.ts | 8 ++ apps/web/next.config.ts | 1 + apps/web/proxy.ts | 2 +- deploy/deploy-container.sh | 102 ++++++++++++++++++ deploy/docker-compose.yml | 29 ++++++ docs/README.md | 2 +- docs/README.zh-CN.md | 2 +- docs/tencent-docker-deployment.md | 14 +-- docs/tencent-docker-deployment.zh-CN.md | 14 +-- 14 files changed, 284 insertions(+), 69 deletions(-) create mode 100644 apps/web/.dockerignore create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/app/api/health/route.ts create mode 100644 deploy/deploy-container.sh create mode 100644 deploy/docker-compose.yml diff --git a/.github/workflows/deploy-tencent.yml b/.github/workflows/deploy-tencent.yml index 0414690..d4cd1a0 100644 --- a/.github/workflows/deploy-tencent.yml +++ b/.github/workflows/deploy-tencent.yml @@ -1,9 +1,20 @@ -name: Deploy Tencent +name: Deploy Tencent Docker permissions: contents: read + packages: write on: + pull_request: + paths: + - apps/web/.dockerignore + - apps/web/Dockerfile + - apps/web/next.config.ts + - apps/web/app/api/health/** + - apps/web/package.json + - apps/web/package-lock.json + - deploy/** + - .github/workflows/deploy-tencent.yml push: branches: - main @@ -11,68 +22,98 @@ on: workflow_dispatch: concurrency: - group: deploy-tencent-${{ github.ref }} + group: deploy-tencent-docker-${{ github.ref }} cancel-in-progress: false +env: + IMAGE_NAME: ghcr.io/junchenmeteor/meteortest-web + jobs: + build: + name: Build Web image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: apps/web + file: apps/web/Dockerfile + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Upload deployment manifest + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: meteortest-deployment-${{ github.sha }} + path: deploy + retention-days: 7 + deploy: - name: Deploy Web + name: Deploy Web container + if: github.event_name != 'pull_request' + needs: build runs-on: [self-hosted, linux, x64, tencent, meteortest] environment: tencent steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Download deployment manifest + uses: actions/download-artifact@v4 + with: + name: meteortest-deployment-${{ github.sha }} + path: deployment - name: Resolve target id: target shell: bash run: | if [ "${GITHUB_REF_NAME}" = "release" ]; then - echo "app_dir=/srv/meteortest-release" >> "$GITHUB_OUTPUT" - echo "pm2_name=meteortest-release" >> "$GITHUB_OUTPUT" + echo "environment=production" >> "$GITHUB_OUTPUT" echo "port=3200" >> "$GITHUB_OUTPUT" + echo "shadow_port=3210" >> "$GITHUB_OUTPUT" + echo "pm2_name=meteortest-release" >> "$GITHUB_OUTPUT" + echo "public_url=https://meteortest.jcmeteor.com/" >> "$GITHUB_OUTPUT" else - echo "app_dir=/srv/meteortest" >> "$GITHUB_OUTPUT" - echo "pm2_name=meteortest-web" >> "$GITHUB_OUTPUT" + echo "environment=preview" >> "$GITHUB_OUTPUT" echo "port=3201" >> "$GITHUB_OUTPUT" + echo "shadow_port=3211" >> "$GITHUB_OUTPUT" + echo "pm2_name=meteortest-web" >> "$GITHUB_OUTPUT" + echo "public_url=https://mt-pre.jcmeteor.com/" >> "$GITHUB_OUTPUT" fi + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Deploy - shell: bash + env: + APP_NAME: meteortest + ENVIRONMENT: ${{ steps.target.outputs.environment }} + HOST_PORT: ${{ steps.target.outputs.port }} + IMAGE_URI: ${{ env.IMAGE_NAME }}:${{ github.sha }} + PM2_NAME: ${{ steps.target.outputs.pm2_name }} + PUBLIC_URL: ${{ steps.target.outputs.public_url }} + RUNTIME_ENV_FILE: /etc/meteortest/meteortest-web.env + SHADOW_PORT: ${{ steps.target.outputs.shadow_port }} run: | - retry() { - local attempts="$1" - local delay="$2" - shift 2 - - local attempt=1 - until "$@"; do - if [ "$attempt" -ge "$attempts" ]; then - return 1 - fi - - echo "Command failed. Retrying in ${delay}s (${attempt}/${attempts})..." - sleep "$delay" - attempt=$((attempt + 1)) - done - } - - mkdir -p '${{ steps.target.outputs.app_dir }}' - rsync -a --delete \ - --exclude '.git' \ - --exclude 'apps/web/.next' \ - --exclude 'apps/web/node_modules' \ - ./ '${{ steps.target.outputs.app_dir }}/' - - cd '${{ steps.target.outputs.app_dir }}/apps/web' - retry 3 15 npm ci - set -a - . /etc/meteortest/meteortest-web.env - set +a - retry 2 15 npm run build - - pm2 delete '${{ steps.target.outputs.pm2_name }}' || true - unset RUNNER_TRACKING_ID - pm2 start npm --name '${{ steps.target.outputs.pm2_name }}' -- run start -- --hostname 127.0.0.1 --port '${{ steps.target.outputs.port }}' - pm2 save - retry 12 5 curl -fsS 'http://127.0.0.1:${{ steps.target.outputs.port }}/' >/dev/null + chmod +x deployment/deploy-container.sh + deployment/deploy-container.sh diff --git a/README.md b/README.md index 82bbb01..f92684d 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ main -> mt-pre.jcmeteor.com release -> meteortest.jcmeteor.com ``` -The detailed Docker/TCR target architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. +The detailed Docker/GHCR architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. Production publishing is automated by the GitHub Actions `Release Manager` workflow: diff --git a/README.zh-CN.md b/README.zh-CN.md index 81a1918..ef0cda3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -251,7 +251,7 @@ main -> mt-pre.jcmeteor.com release -> meteortest.jcmeteor.com ``` -Docker/TCR 目标架构、runner、分支、环境变量、端口映射、PM2 迁移和回滚流程详见 `docs/tencent-docker-deployment.zh-CN.md`。 +Docker/GHCR 架构、runner、分支、环境变量、端口映射、PM2 迁移和回滚流程详见 `docs/tencent-docker-deployment.zh-CN.md`。 生产发布通过 GitHub Actions 的 `Release Manager` workflow 自动化: diff --git a/apps/web/.dockerignore b/apps/web/.dockerignore new file mode 100644 index 0000000..e9dc615 --- /dev/null +++ b/apps/web/.dockerignore @@ -0,0 +1,10 @@ +.git +.next +node_modules +.env +.env.* +!.env.example +*.log +coverage +playwright-report +test-results diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..1813eb2 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,32 @@ +FROM node:24-bookworm-slim AS dependencies + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM dependencies AS builder + +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +FROM node:24-bookworm-slim AS runtime + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + HOSTNAME=0.0.0.0 \ + PORT=3000 + +WORKDIR /app + +RUN groupadd --system --gid 1001 nodejs \ + && useradd --system --uid 1001 --gid nodejs nextjs + +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public + +USER nextjs +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts new file mode 100644 index 0000000..76f9209 --- /dev/null +++ b/apps/web/app/api/health/route.ts @@ -0,0 +1,8 @@ +/** + * Process-local health endpoint used by Docker and deployment checks. + */ +import { NextResponse } from 'next/server' + +export function GET() { + return NextResponse.json({ service: 'meteortest-web', status: 'ok' }) +} diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 6b931f9..820268b 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { devIndicators: false, + output: "standalone", }; if (process.env.VERCEL === "1") { diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index 30dc6e0..8d76c53 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { createServerClient } from '@supabase/ssr' -const publicPaths = new Set(['/login', '/api/auth/logout', '/favicon.ico']) +const publicPaths = new Set(['/login', '/api/auth/logout', '/api/health', '/favicon.ico']) const authOptionalPaths = new Set(['/api/agent/status']) async function getSessionResponse(request: NextRequest) { diff --git a/deploy/deploy-container.sh b/deploy/deploy-container.sh new file mode 100644 index 0000000..fb9445d --- /dev/null +++ b/deploy/deploy-container.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +required=(APP_NAME ENVIRONMENT IMAGE_URI HOST_PORT SHADOW_PORT RUNTIME_ENV_FILE PM2_NAME PUBLIC_URL) +for name in "${required[@]}"; do + if [ -z "${!name:-}" ]; then + echo "Missing required environment variable: ${name}" >&2 + exit 1 + fi +done + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +deploy_dir="/srv/containers/${APP_NAME}/${ENVIRONMENT}" +compose_file="${deploy_dir}/docker-compose.yml" +deployment_env="${deploy_dir}/deployment.env" +project_name="${APP_NAME}-${ENVIRONMENT}" + +mkdir -p "${deploy_dir}" +install -m 0644 "${script_dir}/docker-compose.yml" "${compose_file}" + +write_deployment_env() { + local file="$1" + local image="$2" + local port="$3" + { + printf 'IMAGE_URI=%s\n' "${image}" + printf 'HOST_PORT=%s\n' "${port}" + printf 'RUNTIME_ENV_FILE=%s\n' "${RUNTIME_ENV_FILE}" + } > "${file}" + chmod 0600 "${file}" +} + +compose() { + local project="$1" + local env_file="$2" + shift 2 + docker compose --project-name "${project}" --env-file "${env_file}" --file "${compose_file}" "$@" +} + +wait_for_health() { + local port="$1" + local public_url="${2:-}" + local attempt + for attempt in $(seq 1 24); do + if curl -fsS --max-time 5 "http://127.0.0.1:${port}/api/health" >/dev/null; then + if [ -z "${public_url}" ] || curl -fsS --max-time 10 "${public_url}" >/dev/null; then + return 0 + fi + fi + sleep 5 + done + return 1 +} + +current_container="$(compose "${project_name}" "${deployment_env}" ps -q web 2>/dev/null || true)" +previous_image="" +if [ -n "${current_container}" ]; then + previous_image="$(docker inspect --format '{{.Config.Image}}' "${current_container}")" +fi + +docker pull "${IMAGE_URI}" + +if [ -z "${current_container}" ] && pm2 describe "${PM2_NAME}" >/dev/null 2>&1; then + shadow_env="$(mktemp)" + shadow_project="${project_name}-shadow" + write_deployment_env "${shadow_env}" "${IMAGE_URI}" "${SHADOW_PORT}" + trap 'compose "${shadow_project}" "${shadow_env}" down --remove-orphans >/dev/null 2>&1 || true; rm -f "${shadow_env}"' EXIT + + compose "${shadow_project}" "${shadow_env}" up -d --wait --wait-timeout 180 + wait_for_health "${SHADOW_PORT}" + + pm2 stop "${PM2_NAME}" + write_deployment_env "${deployment_env}" "${IMAGE_URI}" "${HOST_PORT}" + if ! compose "${project_name}" "${deployment_env}" up -d --wait --wait-timeout 180 \ + || ! wait_for_health "${HOST_PORT}" "${PUBLIC_URL}"; then + compose "${project_name}" "${deployment_env}" down --remove-orphans || true + pm2 restart "${PM2_NAME}" --update-env + wait_for_health "${HOST_PORT}" "${PUBLIC_URL}" + exit 1 + fi + + compose "${shadow_project}" "${shadow_env}" down --remove-orphans + rm -f "${shadow_env}" + trap - EXIT + pm2 save +else + write_deployment_env "${deployment_env}" "${IMAGE_URI}" "${HOST_PORT}" + if ! compose "${project_name}" "${deployment_env}" up -d --wait --wait-timeout 180 \ + || ! wait_for_health "${HOST_PORT}" "${PUBLIC_URL}"; then + if [ -n "${previous_image}" ]; then + echo "Deployment failed; restoring ${previous_image}" >&2 + write_deployment_env "${deployment_env}" "${previous_image}" "${HOST_PORT}" + docker pull "${previous_image}" || true + compose "${project_name}" "${deployment_env}" up -d --wait --wait-timeout 180 + wait_for_health "${HOST_PORT}" "${PUBLIC_URL}" + fi + exit 1 + fi +fi + +printf '%s\n' "${IMAGE_URI}" > "${deploy_dir}/current-image" +printf 'Deployed %s to %s (%s)\n' "${IMAGE_URI}" "${project_name}" "${PUBLIC_URL}" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..c963897 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,29 @@ +services: + web: + image: ${IMAGE_URI:?IMAGE_URI is required} + init: true + restart: unless-stopped + env_file: + - ${RUNTIME_ENV_FILE:?RUNTIME_ENV_FILE is required} + environment: + HOSTNAME: 0.0.0.0 + NODE_ENV: production + PORT: 3000 + ports: + - 127.0.0.1:${HOST_PORT:?HOST_PORT is required}:3000 + healthcheck: + test: + - CMD + - node + - -e + - fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1)) + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + mem_limit: 768m + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" diff --git a/docs/README.md b/docs/README.md index a86f9fc..711d366 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ This is the single entry point for MeteorTest documentation. Start here for new | Supabase runbook | `supabase-account-data-runbook.md` / `supabase-account-data-runbook.zh-CN.md` | Auth/RLS, preferences, AI history, display refs SQL execution and verification. | | Local Agent operations | `local-agent-operations.md` / `local-agent-operations.zh-CN.md` | Agent daemon, check interval, heartbeat, logs, OpenClaw checks. | | Public preview deployment | `vercel-public-preview.md` / `vercel-public-preview.zh-CN.md` | Vercel public preview deployment and safety checks. | -| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker/TCR target architecture, main/release mapping, PM2 migration, and rollback. | +| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker/GHCR architecture, main/release mapping, PM2 migration, and rollback. | | Release automation | `release-manager.md` | GitHub Actions release workflow, release PR automation, and recovery commands. | | Private Agent loop | `private-agent-preview-loop.md` / `private-agent-preview-loop.zh-CN.md` | Validation flow for public Web plus private Agent execution. | | Data exposure boundary | `internal-id-exposure-hardening.md` / `internal-id-exposure-hardening.zh-CN.md` | Internal UUIDs, public refs, DTO/View Model rules. | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index 7d54f5d..cdf926c 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -23,7 +23,7 @@ | Supabase 手册 | `supabase-account-data-runbook.zh-CN.md` / `supabase-account-data-runbook.md` | Auth/RLS、账号偏好、AI 历史、display refs 的 SQL 执行与验证。 | | Local Agent 运维 | `local-agent-operations.zh-CN.md` / `local-agent-operations.md` | Agent 常驻、检查频率、心跳、日志、OpenClaw 巡检。 | | 公网预览部署 | `vercel-public-preview.zh-CN.md` / `vercel-public-preview.md` | Vercel 公网预览部署和安全检查。 | -| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker/TCR 目标架构、main/release 映射、PM2 迁移和回滚。 | +| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker/GHCR 架构、main/release 映射、PM2 迁移和回滚。 | | 发布自动化 | `release-manager.md` | GitHub Actions 发布入口、release PR 自动化和中断恢复命令。 | | 私有 Agent 闭环 | `private-agent-preview-loop.zh-CN.md` / `private-agent-preview-loop.md` | 私有 Agent 连接公网 Web 后端的验证流程。 | | 数据暴露边界 | `internal-id-exposure-hardening.zh-CN.md` / `internal-id-exposure-hardening.md` | 内部 UUID、公开引用、DTO/View Model 规则。 | diff --git a/docs/tencent-docker-deployment.md b/docs/tencent-docker-deployment.md index 0c6ccf7..6cea349 100644 --- a/docs/tencent-docker-deployment.md +++ b/docs/tencent-docker-deployment.md @@ -17,7 +17,7 @@ Nginx MUST continue binding public ports 80/443. Containers MUST publish only to 1. A GitHub-hosted runner checks out the requested commit. 2. CI installs dependencies in `apps/web`, then runs lint and the production build. Repository-wide validation continues to cover the Python Agent separately. -3. CI builds a multi-stage Next.js standalone image and pushes it to Tencent Container Registry (TCR). +3. CI builds a multi-stage Next.js standalone image and pushes it to GitHub Container Registry (GHCR). 4. The image is tagged with an immutable commit SHA. Branch and release tags MAY be aliases, but deployment MUST resolve to the SHA tag. 5. The MeteorTest Tencent runner pulls the image and updates only the matching Compose project. 6. The runner waits for container health and verifies the public domain. @@ -27,7 +27,7 @@ The server MUST NOT copy source, install npm dependencies, or build Next.js afte ## Image contract -- Proposed image: `//meteortest-web`. +- Image: `ghcr.io/junchenmeteor/meteortest-web:`. - Build context: `apps/web` unless implementation validation identifies a repository-root dependency. - Next.js MUST use `output: 'standalone'`. - The runtime stage MUST contain only the standalone server, static assets, and required public files. @@ -46,11 +46,7 @@ Docker migration covers only `apps/web`. The private Python Local Agent MUST rem ## Configuration and secrets -Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. GitHub stores only TCR access and non-secret deployment metadata. - -- variables: TCR registry, namespace, image repository; -- secrets: TCR username/token or equivalent short-lived credential; -- Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image. +Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. GitHub Actions uses the repository-scoped `GITHUB_TOKEN` for GHCR access, so no long-lived registry password is required. Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image. ## Compose requirements @@ -83,7 +79,7 @@ Do not run `pm2 kill`. Keep PM2 definitions and source directories until both en 1. resolve the immutable image SHA; 2. record the currently running SHA; -3. authenticate to TCR and pull the image; +3. authenticate to GHCR with the workflow token and pull the image; 4. update the matching Compose project; 5. wait for container health; 6. verify the localhost port and public domain; @@ -96,7 +92,7 @@ For a normal rollback, deploy the previous image SHA and repeat health checks. D ## Acceptance checklist - CI builds the same commit that is tagged and deployed. -- No application or Agent secret exists in image history, build logs, or TCR metadata. +- No application or Agent secret exists in image history, build logs, or GHCR metadata. - Preview and production deploy and roll back independently. - Containers bind only to localhost. - Nginx passes `nginx -t` before reload. diff --git a/docs/tencent-docker-deployment.zh-CN.md b/docs/tencent-docker-deployment.zh-CN.md index e59ed73..315020a 100644 --- a/docs/tencent-docker-deployment.zh-CN.md +++ b/docs/tencent-docker-deployment.zh-CN.md @@ -17,7 +17,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. 1. GitHub 托管 Runner 检出目标提交。 2. CI 在 `apps/web` 安装依赖并执行 lint 和生产构建;仓库级验证继续单独覆盖 Python Agent。 -3. CI 构建多阶段 Next.js standalone 镜像并推送到腾讯云容器镜像服务(TCR)。 +3. CI 构建多阶段 Next.js standalone 镜像并推送到 GitHub Container Registry(GHCR)。 4. 镜像使用不可变 commit SHA 标签;分支和版本标签 MAY 作为别名,但部署 MUST 最终解析到 SHA 标签。 5. MeteorTest 专属腾讯 Runner 拉取镜像,只更新对应 Compose 项目。 6. Runner 等待容器健康并验证公网域名。 @@ -27,7 +27,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 镜像契约 -- 建议镜像名:`//meteortest-web`。 +- 镜像名:`ghcr.io/junchenmeteor/meteortest-web:`。 - 构建上下文:默认使用 `apps/web`;如果实施验证发现仓库根依赖,再调整到根目录。 - Next.js MUST 使用 `output: 'standalone'`。 - 运行阶段 MUST 只包含 standalone server、静态资源和必要 public 文件。 @@ -46,11 +46,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 配置与密钥 -真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub 只保存 TCR 访问凭据和非敏感部署元数据。 - -- Variables:TCR registry、namespace、image repository; -- Secrets:TCR 用户名/Token 或等价短期凭据; -- Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像。 +真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub Actions 使用仓库范围的 `GITHUB_TOKEN` 访问 GHCR,不需要长期镜像仓库密码。Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像。 ## Compose 要求 @@ -83,7 +79,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. 1. 解析新的不可变镜像 SHA; 2. 记录当前运行 SHA; -3. 登录 TCR 并拉取镜像; +3. 使用 workflow token 登录 GHCR 并拉取镜像; 4. 更新对应 Compose 项目; 5. 等待容器健康; 6. 验证 localhost 端口和公网域名; @@ -96,7 +92,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 验收清单 - CI 构建、标记和部署的是同一个 commit。 -- 镜像历史、构建日志和 TCR 元数据中不存在应用或 Agent 密钥。 +- 镜像历史、构建日志和 GHCR 元数据中不存在应用或 Agent 密钥。 - 预览与生产可独立部署、独立回滚。 - 容器只绑定 localhost。 - reload 前 Nginx 配置通过 `nginx -t`。 From 6bc87cda0760031d03ceb2544f58ecc99fcdbda7 Mon Sep 17 00:00:00 2001 From: ConnorQi Date: Sun, 12 Jul 2026 00:50:02 +0800 Subject: [PATCH 2/3] Deliver Docker image as workflow artifact --- .github/workflows/deploy-tencent.yml | 27 +++++++++---------------- README.md | 2 +- README.zh-CN.md | 2 +- deploy/deploy-container.sh | 6 ++++-- docs/README.md | 2 +- docs/README.zh-CN.md | 2 +- docs/tencent-docker-deployment.md | 14 ++++++------- docs/tencent-docker-deployment.zh-CN.md | 14 ++++++------- 8 files changed, 31 insertions(+), 38 deletions(-) diff --git a/.github/workflows/deploy-tencent.yml b/.github/workflows/deploy-tencent.yml index d4cd1a0..7540bb9 100644 --- a/.github/workflows/deploy-tencent.yml +++ b/.github/workflows/deploy-tencent.yml @@ -2,7 +2,6 @@ name: Deploy Tencent Docker permissions: contents: read - packages: write on: pull_request: @@ -26,7 +25,7 @@ concurrency: cancel-in-progress: false env: - IMAGE_NAME: ghcr.io/junchenmeteor/meteortest-web + IMAGE_NAME: meteortest-web jobs: build: @@ -39,25 +38,21 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image uses: docker/build-push-action@v6 with: context: apps/web file: apps/web/Dockerfile platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} + outputs: type=docker,dest=${{ runner.temp }}/meteortest-image.tar tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max + - name: Compress image artifact + if: github.event_name != 'pull_request' + run: gzip -1 < "${RUNNER_TEMP}/meteortest-image.tar" > deploy/meteortest-image.tar.gz + - name: Upload deployment manifest if: github.event_name != 'pull_request' uses: actions/upload-artifact@v4 @@ -97,19 +92,15 @@ jobs: echo "public_url=https://mt-pre.jcmeteor.com/" >> "$GITHUB_OUTPUT" fi - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Load image artifact + run: gzip -dc deployment/meteortest-image.tar.gz | docker load - name: Deploy env: APP_NAME: meteortest ENVIRONMENT: ${{ steps.target.outputs.environment }} HOST_PORT: ${{ steps.target.outputs.port }} - IMAGE_URI: ${{ env.IMAGE_NAME }}:${{ github.sha }} + IMAGE_URI: meteortest-web:${{ github.sha }} PM2_NAME: ${{ steps.target.outputs.pm2_name }} PUBLIC_URL: ${{ steps.target.outputs.public_url }} RUNTIME_ENV_FILE: /etc/meteortest/meteortest-web.env diff --git a/README.md b/README.md index f92684d..4240aca 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ main -> mt-pre.jcmeteor.com release -> meteortest.jcmeteor.com ``` -The detailed Docker/GHCR architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. +The detailed Docker/Actions Artifact architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. Production publishing is automated by the GitHub Actions `Release Manager` workflow: diff --git a/README.zh-CN.md b/README.zh-CN.md index ef0cda3..c818100 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -251,7 +251,7 @@ main -> mt-pre.jcmeteor.com release -> meteortest.jcmeteor.com ``` -Docker/GHCR 架构、runner、分支、环境变量、端口映射、PM2 迁移和回滚流程详见 `docs/tencent-docker-deployment.zh-CN.md`。 +Docker/Actions Artifact 架构、runner、分支、环境变量、端口映射、PM2 迁移和回滚流程详见 `docs/tencent-docker-deployment.zh-CN.md`。 生产发布通过 GitHub Actions 的 `Release Manager` workflow 自动化: diff --git a/deploy/deploy-container.sh b/deploy/deploy-container.sh index fb9445d..defb212 100644 --- a/deploy/deploy-container.sh +++ b/deploy/deploy-container.sh @@ -58,7 +58,10 @@ if [ -n "${current_container}" ]; then previous_image="$(docker inspect --format '{{.Config.Image}}' "${current_container}")" fi -docker pull "${IMAGE_URI}" +if ! docker image inspect "${IMAGE_URI}" >/dev/null 2>&1; then + echo "Image is not loaded: ${IMAGE_URI}" >&2 + exit 1 +fi if [ -z "${current_container}" ] && pm2 describe "${PM2_NAME}" >/dev/null 2>&1; then shadow_env="$(mktemp)" @@ -90,7 +93,6 @@ else if [ -n "${previous_image}" ]; then echo "Deployment failed; restoring ${previous_image}" >&2 write_deployment_env "${deployment_env}" "${previous_image}" "${HOST_PORT}" - docker pull "${previous_image}" || true compose "${project_name}" "${deployment_env}" up -d --wait --wait-timeout 180 wait_for_health "${HOST_PORT}" "${PUBLIC_URL}" fi diff --git a/docs/README.md b/docs/README.md index 711d366..f72fab6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ This is the single entry point for MeteorTest documentation. Start here for new | Supabase runbook | `supabase-account-data-runbook.md` / `supabase-account-data-runbook.zh-CN.md` | Auth/RLS, preferences, AI history, display refs SQL execution and verification. | | Local Agent operations | `local-agent-operations.md` / `local-agent-operations.zh-CN.md` | Agent daemon, check interval, heartbeat, logs, OpenClaw checks. | | Public preview deployment | `vercel-public-preview.md` / `vercel-public-preview.zh-CN.md` | Vercel public preview deployment and safety checks. | -| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker/GHCR architecture, main/release mapping, PM2 migration, and rollback. | +| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker/Actions Artifact architecture, main/release mapping, PM2 migration, and rollback. | | Release automation | `release-manager.md` | GitHub Actions release workflow, release PR automation, and recovery commands. | | Private Agent loop | `private-agent-preview-loop.md` / `private-agent-preview-loop.zh-CN.md` | Validation flow for public Web plus private Agent execution. | | Data exposure boundary | `internal-id-exposure-hardening.md` / `internal-id-exposure-hardening.zh-CN.md` | Internal UUIDs, public refs, DTO/View Model rules. | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index cdf926c..d75b9e5 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -23,7 +23,7 @@ | Supabase 手册 | `supabase-account-data-runbook.zh-CN.md` / `supabase-account-data-runbook.md` | Auth/RLS、账号偏好、AI 历史、display refs 的 SQL 执行与验证。 | | Local Agent 运维 | `local-agent-operations.zh-CN.md` / `local-agent-operations.md` | Agent 常驻、检查频率、心跳、日志、OpenClaw 巡检。 | | 公网预览部署 | `vercel-public-preview.zh-CN.md` / `vercel-public-preview.md` | Vercel 公网预览部署和安全检查。 | -| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker/GHCR 架构、main/release 映射、PM2 迁移和回滚。 | +| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker/Actions Artifact 架构、main/release 映射、PM2 迁移和回滚。 | | 发布自动化 | `release-manager.md` | GitHub Actions 发布入口、release PR 自动化和中断恢复命令。 | | 私有 Agent 闭环 | `private-agent-preview-loop.zh-CN.md` / `private-agent-preview-loop.md` | 私有 Agent 连接公网 Web 后端的验证流程。 | | 数据暴露边界 | `internal-id-exposure-hardening.zh-CN.md` / `internal-id-exposure-hardening.md` | 内部 UUID、公开引用、DTO/View Model 规则。 | diff --git a/docs/tencent-docker-deployment.md b/docs/tencent-docker-deployment.md index 6cea349..964c425 100644 --- a/docs/tencent-docker-deployment.md +++ b/docs/tencent-docker-deployment.md @@ -17,9 +17,9 @@ Nginx MUST continue binding public ports 80/443. Containers MUST publish only to 1. A GitHub-hosted runner checks out the requested commit. 2. CI installs dependencies in `apps/web`, then runs lint and the production build. Repository-wide validation continues to cover the Python Agent separately. -3. CI builds a multi-stage Next.js standalone image and pushes it to GitHub Container Registry (GHCR). +3. CI exports the multi-stage Next.js standalone image as a compressed GitHub Actions artifact. 4. The image is tagged with an immutable commit SHA. Branch and release tags MAY be aliases, but deployment MUST resolve to the SHA tag. -5. The MeteorTest Tencent runner pulls the image and updates only the matching Compose project. +5. The MeteorTest Tencent runner downloads the artifact, loads the immutable image into Docker, and updates only the matching Compose project. 6. The runner waits for container health and verifies the public domain. 7. A failed health check MUST restore the previous image SHA. @@ -27,7 +27,7 @@ The server MUST NOT copy source, install npm dependencies, or build Next.js afte ## Image contract -- Image: `ghcr.io/junchenmeteor/meteortest-web:`. +- Image tag: `meteortest-web:`. - Build context: `apps/web` unless implementation validation identifies a repository-root dependency. - Next.js MUST use `output: 'standalone'`. - The runtime stage MUST contain only the standalone server, static assets, and required public files. @@ -46,7 +46,7 @@ Docker migration covers only `apps/web`. The private Python Local Agent MUST rem ## Configuration and secrets -Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. GitHub Actions uses the repository-scoped `GITHUB_TOKEN` for GHCR access, so no long-lived registry password is required. Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image. +Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. GitHub Actions stores the compressed image for seven days and transfers it through the workflow artifact service, so no container-registry password is required. Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image artifact. ## Compose requirements @@ -79,8 +79,8 @@ Do not run `pm2 kill`. Keep PM2 definitions and source directories until both en 1. resolve the immutable image SHA; 2. record the currently running SHA; -3. authenticate to GHCR with the workflow token and pull the image; -4. update the matching Compose project; +3. download and load the image artifact; +4. update the matching Compose project from the local immutable image; 5. wait for container health; 6. verify the localhost port and public domain; 7. retain the previous SHA for rollback. @@ -92,7 +92,7 @@ For a normal rollback, deploy the previous image SHA and repeat health checks. D ## Acceptance checklist - CI builds the same commit that is tagged and deployed. -- No application or Agent secret exists in image history, build logs, or GHCR metadata. +- No application or Agent secret exists in image history, build logs, or artifact metadata. - Preview and production deploy and roll back independently. - Containers bind only to localhost. - Nginx passes `nginx -t` before reload. diff --git a/docs/tencent-docker-deployment.zh-CN.md b/docs/tencent-docker-deployment.zh-CN.md index 315020a..ea8b227 100644 --- a/docs/tencent-docker-deployment.zh-CN.md +++ b/docs/tencent-docker-deployment.zh-CN.md @@ -17,9 +17,9 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. 1. GitHub 托管 Runner 检出目标提交。 2. CI 在 `apps/web` 安装依赖并执行 lint 和生产构建;仓库级验证继续单独覆盖 Python Agent。 -3. CI 构建多阶段 Next.js standalone 镜像并推送到 GitHub Container Registry(GHCR)。 +3. CI 将多阶段 Next.js standalone 镜像导出为压缩的 GitHub Actions Artifact。 4. 镜像使用不可变 commit SHA 标签;分支和版本标签 MAY 作为别名,但部署 MUST 最终解析到 SHA 标签。 -5. MeteorTest 专属腾讯 Runner 拉取镜像,只更新对应 Compose 项目。 +5. MeteorTest 专属腾讯 Runner 下载 Artifact、将不可变镜像加载到 Docker,并只更新对应 Compose 项目。 6. Runner 等待容器健康并验证公网域名。 7. 健康检查失败时 MUST 恢复上一镜像 SHA。 @@ -27,7 +27,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 镜像契约 -- 镜像名:`ghcr.io/junchenmeteor/meteortest-web:`。 +- 镜像标签:`meteortest-web:`。 - 构建上下文:默认使用 `apps/web`;如果实施验证发现仓库根依赖,再调整到根目录。 - Next.js MUST 使用 `output: 'standalone'`。 - 运行阶段 MUST 只包含 standalone server、静态资源和必要 public 文件。 @@ -46,7 +46,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 配置与密钥 -真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub Actions 使用仓库范围的 `GITHUB_TOKEN` 访问 GHCR,不需要长期镜像仓库密码。Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像。 +真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub Actions 保留压缩镜像七天,并通过 workflow Artifact 服务传输,不需要镜像仓库密码。Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像 Artifact。 ## Compose 要求 @@ -79,8 +79,8 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. 1. 解析新的不可变镜像 SHA; 2. 记录当前运行 SHA; -3. 使用 workflow token 登录 GHCR 并拉取镜像; -4. 更新对应 Compose 项目; +3. 下载并加载镜像 Artifact; +4. 使用本地不可变镜像更新对应 Compose 项目; 5. 等待容器健康; 6. 验证 localhost 端口和公网域名; 7. 保留上一 SHA 用于回滚。 @@ -92,7 +92,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 验收清单 - CI 构建、标记和部署的是同一个 commit。 -- 镜像历史、构建日志和 GHCR 元数据中不存在应用或 Agent 密钥。 +- 镜像历史、构建日志和 Artifact 元数据中不存在应用或 Agent 密钥。 - 预览与生产可独立部署、独立回滚。 - 容器只绑定 localhost。 - reload 前 Nginx 配置通过 `nginx -t`。 From 724ebdf601135ed4f85faa88c321bf0537deb27d Mon Sep 17 00:00:00 2001 From: ConnorQi Date: Sun, 12 Jul 2026 01:11:44 +0800 Subject: [PATCH 3/3] fix(deploy): upload image artifacts directly --- .github/workflows/deploy-tencent.yml | 30 +++++++++++++++++++++++-- README.md | 2 +- deploy/receive-image-upload.sh | 27 ++++++++++++++++++++++ docs/README.md | 2 +- docs/README.zh-CN.md | 2 +- docs/tencent-docker-deployment.md | 6 ++--- docs/tencent-docker-deployment.zh-CN.md | 4 ++-- 7 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 deploy/receive-image-upload.sh diff --git a/.github/workflows/deploy-tencent.yml b/.github/workflows/deploy-tencent.yml index 7540bb9..c6c905a 100644 --- a/.github/workflows/deploy-tencent.yml +++ b/.github/workflows/deploy-tencent.yml @@ -31,6 +31,7 @@ jobs: build: name: Build Web image runs-on: ubuntu-latest + environment: tencent steps: - name: Checkout uses: actions/checkout@v4 @@ -53,12 +54,33 @@ jobs: if: github.event_name != 'pull_request' run: gzip -1 < "${RUNNER_TEMP}/meteortest-image.tar" > deploy/meteortest-image.tar.gz + - name: Upload image artifact to Tencent + if: github.event_name != 'pull_request' + env: + DEPLOY_KEY: ${{ secrets.TENCENT_ARTIFACT_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.TENCENT_ARTIFACT_KNOWN_HOSTS }} + DEPLOY_HOST: ${{ vars.TENCENT_ARTIFACT_HOST }} + DEPLOY_USER: ${{ vars.TENCENT_ARTIFACT_USER }} + run: | + key_file="${RUNNER_TEMP}/artifact-upload-key" + known_hosts_file="${RUNNER_TEMP}/artifact-known-hosts" + printf '%s\n' "${DEPLOY_KEY}" > "${key_file}" + printf '%s\n' "${DEPLOY_KNOWN_HOSTS}" > "${known_hosts_file}" + chmod 0600 "${key_file}" "${known_hosts_file}" + ssh -i "${key_file}" -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \ + -o UserKnownHostsFile="${known_hosts_file}" \ + "${DEPLOY_USER}@${DEPLOY_HOST}" "${GITHUB_SHA}" \ + < deploy/meteortest-image.tar.gz + rm -f deploy/meteortest-image.tar.gz "${key_file}" "${known_hosts_file}" + - name: Upload deployment manifest if: github.event_name != 'pull_request' uses: actions/upload-artifact@v4 with: name: meteortest-deployment-${{ github.sha }} - path: deploy + path: | + deploy/docker-compose.yml + deploy/deploy-container.sh retention-days: 7 deploy: @@ -93,7 +115,7 @@ jobs: fi - name: Load image artifact - run: gzip -dc deployment/meteortest-image.tar.gz | docker load + run: gzip -dc "/srv/deploy-inbox/meteortest/${GITHUB_SHA}.tar.gz" | docker load - name: Deploy env: @@ -108,3 +130,7 @@ jobs: run: | chmod +x deployment/deploy-container.sh deployment/deploy-container.sh + + - name: Remove uploaded image artifact + if: success() + run: rm -f "/srv/deploy-inbox/meteortest/${GITHUB_SHA}.tar.gz" diff --git a/README.md b/README.md index 4240aca..fe084ca 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ main -> mt-pre.jcmeteor.com release -> meteortest.jcmeteor.com ``` -The detailed Docker/Actions Artifact architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. +The detailed Docker image artifact architecture, runner, branch, environment, port mapping, PM2 migration, and rollback flow lives in `docs/tencent-docker-deployment.md`. Production publishing is automated by the GitHub Actions `Release Manager` workflow: diff --git a/deploy/receive-image-upload.sh b/deploy/receive-image-upload.sh new file mode 100644 index 0000000..c6f03c6 --- /dev/null +++ b/deploy/receive-image-upload.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_name="${1:-}" +image_sha="${SSH_ORIGINAL_COMMAND:-}" + +if [[ ! "${app_name}" =~ ^(meteorvoice|meteortest)$ ]]; then + echo "Unsupported application" >&2 + exit 1 +fi + +if [[ ! "${image_sha}" =~ ^[0-9a-f]{40}$ ]]; then + echo "Invalid image SHA" >&2 + exit 1 +fi + +target_dir="/srv/deploy-inbox/${app_name}" +target_file="${target_dir}/${image_sha}.tar.gz" +temporary_file="${target_file}.uploading" + +mkdir -p "${target_dir}" +umask 0027 +ulimit -f 262144 +timeout 900 tee "${temporary_file}" >/dev/null +test -s "${temporary_file}" +mv "${temporary_file}" "${target_file}" +printf 'Stored %s\n' "${target_file}" diff --git a/docs/README.md b/docs/README.md index f72fab6..cf2cfd5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ This is the single entry point for MeteorTest documentation. Start here for new | Supabase runbook | `supabase-account-data-runbook.md` / `supabase-account-data-runbook.zh-CN.md` | Auth/RLS, preferences, AI history, display refs SQL execution and verification. | | Local Agent operations | `local-agent-operations.md` / `local-agent-operations.zh-CN.md` | Agent daemon, check interval, heartbeat, logs, OpenClaw checks. | | Public preview deployment | `vercel-public-preview.md` / `vercel-public-preview.zh-CN.md` | Vercel public preview deployment and safety checks. | -| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker/Actions Artifact architecture, main/release mapping, PM2 migration, and rollback. | +| Tencent Docker deployment | `tencent-docker-deployment.md` / `tencent-docker-deployment.zh-CN.md` | Docker image artifact architecture, main/release mapping, PM2 migration, and rollback. | | Release automation | `release-manager.md` | GitHub Actions release workflow, release PR automation, and recovery commands. | | Private Agent loop | `private-agent-preview-loop.md` / `private-agent-preview-loop.zh-CN.md` | Validation flow for public Web plus private Agent execution. | | Data exposure boundary | `internal-id-exposure-hardening.md` / `internal-id-exposure-hardening.zh-CN.md` | Internal UUIDs, public refs, DTO/View Model rules. | diff --git a/docs/README.zh-CN.md b/docs/README.zh-CN.md index d75b9e5..f11ba54 100644 --- a/docs/README.zh-CN.md +++ b/docs/README.zh-CN.md @@ -23,7 +23,7 @@ | Supabase 手册 | `supabase-account-data-runbook.zh-CN.md` / `supabase-account-data-runbook.md` | Auth/RLS、账号偏好、AI 历史、display refs 的 SQL 执行与验证。 | | Local Agent 运维 | `local-agent-operations.zh-CN.md` / `local-agent-operations.md` | Agent 常驻、检查频率、心跳、日志、OpenClaw 巡检。 | | 公网预览部署 | `vercel-public-preview.zh-CN.md` / `vercel-public-preview.md` | Vercel 公网预览部署和安全检查。 | -| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker/Actions Artifact 架构、main/release 映射、PM2 迁移和回滚。 | +| 腾讯云 Docker 部署 | `tencent-docker-deployment.zh-CN.md` / `tencent-docker-deployment.md` | Docker 镜像制品架构、main/release 映射、PM2 迁移和回滚。 | | 发布自动化 | `release-manager.md` | GitHub Actions 发布入口、release PR 自动化和中断恢复命令。 | | 私有 Agent 闭环 | `private-agent-preview-loop.zh-CN.md` / `private-agent-preview-loop.md` | 私有 Agent 连接公网 Web 后端的验证流程。 | | 数据暴露边界 | `internal-id-exposure-hardening.zh-CN.md` / `internal-id-exposure-hardening.md` | 内部 UUID、公开引用、DTO/View Model 规则。 | diff --git a/docs/tencent-docker-deployment.md b/docs/tencent-docker-deployment.md index 964c425..9fc44c3 100644 --- a/docs/tencent-docker-deployment.md +++ b/docs/tencent-docker-deployment.md @@ -17,9 +17,9 @@ Nginx MUST continue binding public ports 80/443. Containers MUST publish only to 1. A GitHub-hosted runner checks out the requested commit. 2. CI installs dependencies in `apps/web`, then runs lint and the production build. Repository-wide validation continues to cover the Python Agent separately. -3. CI exports the multi-stage Next.js standalone image as a compressed GitHub Actions artifact. +3. CI exports the multi-stage Next.js standalone image as a compressed Docker image artifact. 4. The image is tagged with an immutable commit SHA. Branch and release tags MAY be aliases, but deployment MUST resolve to the SHA tag. -5. The MeteorTest Tencent runner downloads the artifact, loads the immutable image into Docker, and updates only the matching Compose project. +5. The MeteorTest Tencent runner loads the uploaded artifact into Docker and updates only the matching Compose project. 6. The runner waits for container health and verifies the public domain. 7. A failed health check MUST restore the previous image SHA. @@ -46,7 +46,7 @@ Docker migration covers only `apps/web`. The private Python Local Agent MUST rem ## Configuration and secrets -Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. GitHub Actions stores the compressed image for seven days and transfers it through the workflow artifact service, so no container-registry password is required. Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image artifact. +Real Web credentials remain in `/etc/meteortest/meteortest-web.env`. Compose injects the file at container startup. The GitHub-hosted runner sends the compressed image directly to a Tencent inbox through an SSH key restricted to that upload command. The self-hosted runner only loads and deploys it. No container-registry password or interactive upload shell is required. Supabase service-role, AI provider, Agent, and project execution secrets MUST NOT enter the image artifact. ## Compose requirements diff --git a/docs/tencent-docker-deployment.zh-CN.md b/docs/tencent-docker-deployment.zh-CN.md index ea8b227..07c744b 100644 --- a/docs/tencent-docker-deployment.zh-CN.md +++ b/docs/tencent-docker-deployment.zh-CN.md @@ -17,7 +17,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. 1. GitHub 托管 Runner 检出目标提交。 2. CI 在 `apps/web` 安装依赖并执行 lint 和生产构建;仓库级验证继续单独覆盖 Python Agent。 -3. CI 将多阶段 Next.js standalone 镜像导出为压缩的 GitHub Actions Artifact。 +3. CI 将多阶段 Next.js standalone 镜像导出为压缩的 Docker 镜像制品。 4. 镜像使用不可变 commit SHA 标签;分支和版本标签 MAY 作为别名,但部署 MUST 最终解析到 SHA 标签。 5. MeteorTest 专属腾讯 Runner 下载 Artifact、将不可变镜像加载到 Docker,并只更新对应 Compose 项目。 6. Runner 等待容器健康并验证公网域名。 @@ -46,7 +46,7 @@ Nginx MUST 继续监听公网 80/443 端口。容器 MUST 只发布到 `127.0.0. ## 配置与密钥 -真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub Actions 保留压缩镜像七天,并通过 workflow Artifact 服务传输,不需要镜像仓库密码。Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像 Artifact。 +真实 Web 凭据继续保存在 `/etc/meteortest/meteortest-web.env`,由 Compose 在容器启动时注入。GitHub 托管 Runner 使用只允许写入指定目录的 SSH 密钥,将压缩镜像直传到腾讯云制品收件箱;自托管 Runner 只负责加载和部署。该通道不需要镜像仓库密码,上传账号也不能获得交互式 Shell。Supabase service-role、AI provider、Agent 和项目执行密钥 MUST NOT 进入镜像制品。 ## Compose 要求