fix: parse staging image manifests safely
Deploy staging to test server / deploy (push) Successful in 2m36s
Deploy staging to test server / deploy (push) Successful in 2m36s
Handle ACR single-platform manifest arrays in Gitea staging migration and deployment so immutable image resolution can complete.
This commit is contained in:
@@ -63,8 +63,8 @@ jobs:
|
||||
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}"
|
||||
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", ""))')"
|
||||
api_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:api-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); d=d[0] if isinstance(d,list) and len(d)==1 else d; print(d.get("Descriptor", d).get("digest", "") if isinstance(d,dict) else "")')"
|
||||
web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); d=d[0] if isinstance(d,list) and len(d)==1 else d; print(d.get("Descriptor", d).get("digest", "") if isinstance(d,dict) else "")')"
|
||||
[[ "$api_digest" =~ ^sha256:[0-9a-f]{64}$ && "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1
|
||||
printf 'git_sha=%s\napi_digest=%s\nweb_digest=%s\n' "$DEPLOY_SHA" "$api_digest" "$web_digest" > "${RUNNER_TEMP}/manifest.env"
|
||||
node frontend/scripts/staging-image-manifest.mjs "${RUNNER_TEMP}/manifest.env" "$DEPLOY_SHA" "$IMAGE_REPOSITORY" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
[[ "$(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}"
|
||||
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="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); d=d[0] if isinstance(d,list) and len(d)==1 else d; print(d.get("Descriptor", d).get("digest", "") if isinstance(d,dict) else "")')"
|
||||
[[ "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1
|
||||
echo "web_image=${IMAGE_REPOSITORY}@${web_digest}" >> "$GITHUB_OUTPUT"
|
||||
echo "deploy_sha=$DEPLOY_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
+1
-1
@@ -1622,7 +1622,7 @@
|
||||
- 触发条件:进入同域 `/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权限门禁拒绝。同日还确认 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`。
|
||||
- 验证:`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 项回归全部通过。首次独立 staging 迁移 run 1300 在镜像校验阶段暴露 `docker manifest inspect --verbose` 对 ACR 返回单元素数组,而解析器只接受对象,触发 `AttributeError: 'list' object has no attribute 'get'`;Gitea staging 迁移与部署已兼容单平台数组并增加聚焦契约测试。staging 最终验证仍等待迁移应用和已登录 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
|
||||
- 复发自:BUG-093
|
||||
|
||||
@@ -18,6 +18,14 @@ const migrationWorkflow = new URL(
|
||||
"../../.github/workflows/migrate-staging-database.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const giteaDeployWorkflow = new URL(
|
||||
"../../.gitea/workflows/deploy-staging.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const giteaMigrationWorkflow = new URL(
|
||||
"../../.gitea/workflows/migrate-staging-database.yml",
|
||||
import.meta.url,
|
||||
);
|
||||
const rolloutWorkflow = new URL(
|
||||
"../../.github/workflows/configure-staging-rectification-rollout.yml",
|
||||
import.meta.url,
|
||||
@@ -204,6 +212,13 @@ test("staging owns its deployment control plane independently from main", () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("Gitea staging digest resolution accepts single-platform manifest arrays", () => {
|
||||
for (const workflow of [read(giteaDeployWorkflow), read(giteaMigrationWorkflow)]) {
|
||||
assert.match(workflow, /d=d\[0\] if isinstance\(d,list\) and len\(d\)==1 else d/);
|
||||
assert.match(workflow, /if isinstance\(d,dict\) else ""/);
|
||||
}
|
||||
});
|
||||
|
||||
test("staging mutations retain every pending deployment and migration", () => {
|
||||
for (const workflow of [read(deployWorkflow), read(migrationWorkflow)]) {
|
||||
assert.match(
|
||||
|
||||
Reference in New Issue
Block a user