From 85ae00f391cf3af969d997cd997e245539bec147 Mon Sep 17 00:00:00 2001 From: linmeng <819991304@qq.com> Date: Thu, 30 Jul 2026 10:17:50 +0800 Subject: [PATCH] fix: isolate staging deployment control Make staging migration, deployment, and maintenance workflows use the staging branch exclusively so test delivery no longer depends on or modifies production main. --- .gitea/workflows/deploy-staging.yml | 7 +++---- .gitea/workflows/migrate-staging-database.yml | 7 +++---- .../configure-staging-rectification-rollout.yml | 4 ++-- .github/workflows/deploy-staging.yml | 13 ++++++------- .github/workflows/migrate-staging-database.yml | 11 ++++------- .github/workflows/reset-staging-account.yml | 4 ++-- docs/BUG_HISTORY.md | 4 ++-- frontend/tests/staging-backend-workflows.test.ts | 7 ++++--- 8 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 697714bf..b221ca15 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -32,14 +32,14 @@ jobs: STAGING_URL: ${{ vars.STAGING_URL }} STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} steps: - - name: Checkout trusted main controller + - name: Checkout staging controller run: | set -euo pipefail git init . git remote remove origin 2>/dev/null || true git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin main staging - git checkout --detach --force origin/main + git fetch --no-tags origin staging + git checkout --detach --force origin/staging - name: Verify runner toolchain run: | set -euo pipefail @@ -62,7 +62,6 @@ jobs: [[ "$staging_sha" =~ ^[0-9a-f]{40}$ ]] || exit 1 if [[ "$ALLOW_ROLLBACK" != true ]]; then [[ "$DEPLOY_SHA" == "$staging_sha" ]] || { echo "forward redeploy must use staging head" >&2; exit 1; }; fi git cat-file -e "${DEPLOY_SHA}^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" origin/main || { echo "revision is not in reviewed main history" >&2; exit 1; } printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin api_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:api-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" diff --git a/.gitea/workflows/migrate-staging-database.yml b/.gitea/workflows/migrate-staging-database.yml index 97f78453..ae81f864 100644 --- a/.gitea/workflows/migrate-staging-database.yml +++ b/.gitea/workflows/migrate-staging-database.yml @@ -26,14 +26,14 @@ jobs: DEPLOY_PATH: ${{ vars.STAGING_PATH }} STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} steps: - - name: Checkout trusted main controller + - name: Checkout staging controller run: | set -euo pipefail git init . git remote remove origin 2>/dev/null || true git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin main staging - git checkout --detach --force origin/main + git fetch --no-tags origin staging + git checkout --detach --force origin/staging - name: Verify runner toolchain run: | set -euo pipefail @@ -52,7 +52,6 @@ jobs: [[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1 [[ "$(git ls-remote origin refs/heads/staging | awk '{print $1}')" == "$DEPLOY_SHA" ]] || { echo "migration requires current staging head" >&2; exit 1; } git cat-file -e "${DEPLOY_SHA}^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" origin/main || { echo "revision is not in reviewed main history" >&2; exit 1; } printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" [[ "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1 diff --git a/.github/workflows/configure-staging-rectification-rollout.yml b/.github/workflows/configure-staging-rectification-rollout.yml index a18fb072..d17b64f6 100644 --- a/.github/workflows/configure-staging-rectification-rollout.yml +++ b/.github/workflows/configure-staging-rectification-rollout.yml @@ -47,10 +47,10 @@ jobs: SYNTHETIC_SMOKE_USER_IDS: ${{ inputs.synthetic_smoke_user_ids }} steps: - - name: Checkout trusted controller + - name: Checkout staging controller uses: actions/checkout@v4 with: - ref: main + ref: staging persist-credentials: false - name: Validate rollout request and staging target diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 25842b41..08c5096b 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -111,10 +111,10 @@ jobs: echo "allow_rollback=$allow_rollback" } >>"$GITHUB_OUTPUT" - - name: Checkout trusted main controller + - name: Checkout staging controller uses: actions/checkout@v4 with: - ref: main + ref: staging fetch-depth: 0 persist-credentials: false @@ -135,16 +135,15 @@ jobs: node frontend/scripts/staging-image-manifest.mjs \ artifacts/staging-image/manifest.env "$DEPLOY_SHA" >>"$GITHUB_OUTPUT" - - name: Verify reviewed revision and staging target + - name: Verify staging revision and target env: DEPLOY_SHA: ${{ steps.revision.outputs.sha }} run: | set -euo pipefail git cat-file -e "$DEPLOY_SHA^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || { - echo "staging revision is not in the reviewed main history" >&2 - exit 1 - } + if [ "${{ steps.revision.outputs.allow_rollback }}" = "false" ]; then + test "$DEPLOY_SHA" = "$(git rev-parse HEAD)" + fi test "$DEPLOY_HOST" = "118.26.111.127" test "$DEPLOY_PORT" = "22" test "$DEPLOY_USER" = "deploy" diff --git a/.github/workflows/migrate-staging-database.yml b/.github/workflows/migrate-staging-database.yml index 0072cbff..c817d255 100644 --- a/.github/workflows/migrate-staging-database.yml +++ b/.github/workflows/migrate-staging-database.yml @@ -74,10 +74,10 @@ jobs: echo "gate_run_attempt=$gate_run_attempt" } >>"$GITHUB_OUTPUT" - - name: Checkout trusted main controller + - name: Checkout staging controller uses: actions/checkout@v4 with: - ref: main + ref: staging fetch-depth: 0 persist-credentials: false @@ -98,16 +98,13 @@ jobs: node frontend/scripts/staging-image-manifest.mjs \ artifacts/staging-image/manifest.env "$DEPLOY_SHA" >>"$GITHUB_OUTPUT" - - name: Verify reviewed revision and staging target + - name: Verify staging revision and target env: DEPLOY_SHA: ${{ steps.revision.outputs.sha }} run: | set -euo pipefail git cat-file -e "$DEPLOY_SHA^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" HEAD || { - echo "staging revision is not in the reviewed main history" >&2 - exit 1 - } + test "$DEPLOY_SHA" = "$(git rev-parse HEAD)" test "$DEPLOY_HOST" = "118.26.111.127" test "$DEPLOY_PORT" = "22" test "$DEPLOY_USER" = "deploy" diff --git a/.github/workflows/reset-staging-account.yml b/.github/workflows/reset-staging-account.yml index bfc4af47..b26b8248 100644 --- a/.github/workflows/reset-staging-account.yml +++ b/.github/workflows/reset-staging-account.yml @@ -41,10 +41,10 @@ jobs: RESET_CONFIRMATION: ${{ inputs.confirmation }} steps: - - name: Checkout trusted controller + - name: Checkout staging controller uses: actions/checkout@v4 with: - ref: main + ref: staging persist-credentials: false - name: Validate account reset request and staging target diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b801eb3f..b5b52b05 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1620,8 +1620,8 @@ - 影响面:后台 Refine 侧栏、`/admin/payments`、`/admin/packages`、易支付配置与对话页充值入口。 - 用户现象:支付记录与支付配置占用两个导航项,页面仍使用主站 `standalone-page/admin-header/admin-section` 样式;套餐新增表单常驻页面,后台默认退出入口还会触发登出,管理员难以直接返回对话;对话页支付入口缺少安全默认关闭和服务端创建订单硬门禁。2026-07-29 复发时,Z-Pay 配置不能折叠且占据长页面,后台受全局 `html/body overflow:hidden` 限制无法纵向滚动,套餐 API 与易支付配置 API 仍调用 self-hosted adapter 不支持的 Supabase builder/RPC。2026-07-30 部署 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 后,`GET /api/admin/payments` 与套餐管理仍返回 500。 - 触发条件:进入同域 `/admin` 后管理支付记录或套餐,或点击 Refine 侧栏底部默认 Logout;复发条件为进入支付管理、展开长配置或调用套餐 CRUD / 易支付配置读写。2026-07-30 的数据库权限复发在 `admin_runtime` 通过 `ADMIN_DATABASE_URL` 查询支付表时稳定触发。 -- 根因:首轮支付后台实现依赖 Supabase 专用关联 select、分页、计数和 Admin Auth 查询;self-hosted staging 的本地 PostgreSQL adapter 不支持这些 builder 能力,支付记录因此统一降级为“支付记录服务暂时不可用”。同页套餐设计也不符合最新后台信息架构,易支付配置响应漏投影 `chat_enabled`,chat 创建订单又依赖服务端提交网关后猜测跳转地址,不兼容标准易支付收银台表单页。复发遗漏源于上轮只把支付记录切换到 PostgreSQL,套餐与配置契约测试没有锁定 self-hosted 数据链,且未覆盖聊天全局滚动边界下的后台专用滚动容器。2026-07-30 的直接根因是 `20260727020000_epay_packages_orders.sql` 只向 Supabase 的 `service_role` / `authenticated` 授权,未向 self-hosted 后台实际使用的 `admin_runtime` 授予 `payment_packages`、`payment_orders` 权限,也未添加对应 RLS 策略;因此数据库健康且新 SHA 已部署,后台 SQL 仍被 PostgreSQL 权限门禁拒绝。 -- 修复:支付记录改为通过 `queryAdminRows` 执行参数化 SQL,联表 `public.payment_orders`、`public.payment_packages` 和 `identity.users`,以窗口计数保留分页合同并用独立聚合 SQL输出统计;不再使用 Supabase builder 或 Admin Auth。后台在支付管理之后新增独立“套餐管理”资源和页面,套餐新增、编辑、停用、错误重试及原字段保持完整,支付页只保留概览、Z-Pay(易支付)渠道配置和支付记录。配置读取补回 `chat_enabled` 与 `chatEnabled`。创建订单完成登录、开关、配置、SSRF、套餐和订单校验后,直接返回带 `sign/sign_type` 的标准 `submit.php` 收银台 URL,不服务端请求网关、不返回商户密钥;对话页用浏览器打开该 URL,套餐加载异常显示安全错误,正常 `enabled=false` 仍静默隐藏。复发修复将 Z-Pay 配置改为默认收起的 Ant Design `Collapse`,展开后才显示表单和操作;为 AdminApp 增加 `admin-app-shell` 的 `100dvh` 独立纵向滚动边界而不改聊天全局规则;套餐 CRUD 全部改用 `queryAdminRows` 参数化 SQL、UUID 校验、`returning` 与 404;易支付读取仅在 PostgreSQL `42P01` 时回退环境变量,保存直接参数化调用 `public.admin_save_epay_settings` 并使用函数返回行,保留原子审计和脱敏响应。2026-07-30 新增前向迁移 `20260730010000_admin_payment_permissions.sql`,向 `admin_runtime` 最小授予套餐读写、订单只读、易支付配置读取及保存函数执行权限,并为启用 RLS 的支付表补齐角色策略;不授予订单写入或删除权限。 +- 根因:首轮支付后台实现依赖 Supabase 专用关联 select、分页、计数和 Admin Auth 查询;self-hosted staging 的本地 PostgreSQL adapter 不支持这些 builder 能力,支付记录因此统一降级为“支付记录服务暂时不可用”。同页套餐设计也不符合最新后台信息架构,易支付配置响应漏投影 `chat_enabled`,chat 创建订单又依赖服务端提交网关后猜测跳转地址,不兼容标准易支付收银台表单页。复发遗漏源于上轮只把支付记录切换到 PostgreSQL,套餐与配置契约测试没有锁定 self-hosted 数据链,且未覆盖聊天全局滚动边界下的后台专用滚动容器。2026-07-30 的直接根因是 `20260727020000_epay_packages_orders.sql` 只向 Supabase 的 `service_role` / `authenticated` 授权,未向 self-hosted 后台实际使用的 `admin_runtime` 授予 `payment_packages`、`payment_orders` 权限,也未添加对应 RLS 策略;因此数据库健康且新 SHA 已部署,后台 SQL 仍被 PostgreSQL权限门禁拒绝。同日还确认 staging 迁移与部署工作流错误地要求目标 SHA 属于 `main` 历史,使完全独立的测试分支被生产分支阻塞;该控制面耦合导致为恢复 staging 而误合并生产 main。 +- 修复:支付记录改为通过 `queryAdminRows` 执行参数化 SQL,联表 `public.payment_orders`、`public.payment_packages` 和 `identity.users`,以窗口计数保留分页合同并用独立聚合 SQL输出统计;不再使用 Supabase builder 或 Admin Auth。后台在支付管理之后新增独立“套餐管理”资源和页面,套餐新增、编辑、停用、错误重试及原字段保持完整,支付页只保留概览、Z-Pay(易支付)渠道配置和支付记录。配置读取补回 `chat_enabled` 与 `chatEnabled`。创建订单完成登录、开关、配置、SSRF、套餐和订单校验后,直接返回带 `sign/sign_type` 的标准 `submit.php` 收银台 URL,不服务端请求网关、不返回商户密钥;对话页用浏览器打开该 URL,套餐加载异常显示安全错误,正常 `enabled=false` 仍静默隐藏。复发修复将 Z-Pay 配置改为默认收起的 Ant Design `Collapse`,展开后才显示表单和操作;为 AdminApp 增加 `admin-app-shell` 的 `100dvh` 独立纵向滚动边界而不改聊天全局规则;套餐 CRUD 全部改用 `queryAdminRows` 参数化 SQL、UUID 校验、`returning` 与 404;易支付读取仅在 PostgreSQL `42P01` 时回退环境变量,保存直接参数化调用 `public.admin_save_epay_settings` 并使用函数返回行,保留原子审计和脱敏响应。2026-07-30 新增前向迁移 `20260730010000_admin_payment_permissions.sql`,向 `admin_runtime` 最小授予套餐读写、订单只读、易支付配置读取及保存函数执行权限,并为启用 RLS 的支付表补齐角色策略;不授予订单写入或删除权限。Gitea 与 GitHub 的 staging 迁移、部署和测试环境运维工作流统一 checkout `staging`,删除 staging SHA 属于 `main` 历史的要求;生产工作流保持不变。误合入 main 的 PR #1 已由 PR #2 的 revert 恢复,恢复后 main 内容树与合并前提交 `43581ac0f75e7f157032503475e878bd53ad161d` 完全一致。 - 验证:`frontend/tests/admin-contracts.test.ts` 锁定支付、套餐资源顺序;`frontend/tests/admin-payments-contract.test.ts` 锁定本地参数化 SQL、`identity.users` 联表、套餐 SQL CRUD/UUID/404、独立套餐页面、默认折叠和后台专用滚动容器;`frontend/tests/epay-settings.test.ts` 锁定 `chatEnabled` 回显、`queryAdminRows` 读取、参数化 `admin_save_epay_settings`、不依赖 Supabase builder/RPC、默认折叠和不泄露 key。2026-07-29 运行三份契约测试共 27 项全部通过;ESLint、TypeScript 与 `git diff --check` 结果记录在本次交付报告。2026-07-30 线上健康响应证明部署 SHA 为 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 且本地业务库、身份库均健康;静态权限审计确认支付迁移缺少 `admin_runtime` grant/RLS。新增权限迁移契约后,支付、套餐、配置三组 21 项回归全部通过;生产态最终验证仍等待迁移应用和已登录 smoke,因此状态保持 `investigating`。 - 防复发:self-hosted staging 后台查询不得依赖 LocalPostgresDataClient 未实现的 Supabase builder、RPC 或 Admin Auth 能力;支付与套餐必须保持独立资源顺序。套餐与易支付配置契约必须显式拒绝 Supabase builder/RPC 并锁定参数化 SQL、404、原子函数写入和安全错误响应;支付配置必须默认折叠,后台必须拥有独立滚动容器且不得放宽聊天的全局 `overflow:hidden`。易支付配置读写测试必须同时覆盖数据库列和公开字段;创建订单只生成经公网 SSRF 校验的签名收银台 URL,商户密钥只能参与服务端签名,不得进入 URL、响应、日志或审计。对话支付默认关闭,UI 与创建订单 API 必须共享服务端开关;可用性测试不得提交伪订单或返回 URL、PID、密钥、headers/body。 - 相关记录:BUG-087、BUG-092 diff --git a/frontend/tests/staging-backend-workflows.test.ts b/frontend/tests/staging-backend-workflows.test.ts index 3c12f68e..28804da6 100644 --- a/frontend/tests/staging-backend-workflows.test.ts +++ b/frontend/tests/staging-backend-workflows.test.ts @@ -193,11 +193,12 @@ test("deploy and migration consume the exact successful gate artifact", () => { } }); -test("main owns the deployment control plane and target revisions are data only", () => { +test("staging owns its deployment control plane independently from main", () => { for (const workflow of [read(deployWorkflow), read(migrationWorkflow)]) { - assert.match(workflow, /name: Checkout trusted main controller[\s\S]*ref: main/); + assert.match(workflow, /name: Checkout staging controller[\s\S]*ref: staging/); assert.match(workflow, /fetch-depth: 0/); - assert.match(workflow, /git merge-base --is-ancestor "\$DEPLOY_SHA" HEAD/); + assert.doesNotMatch(workflow, /ref: main/); + assert.doesNotMatch(workflow, /reviewed main history/); assert.match(workflow, /--include='\/deploy\/' --include='\/deploy\/\*\*\*' --exclude='\*'/); assert.doesNotMatch(workflow, /ref: \$\{\{ steps\.revision\.outputs\.sha \}\}/); }