diff --git a/deploy/railway-api.Dockerfile b/deploy/railway-api.Dockerfile index 48f8561f..5ac9d225 100644 --- a/deploy/railway-api.Dockerfile +++ b/deploy/railway-api.Dockerfile @@ -1,4 +1,4 @@ -FROM m.daocloud.io/docker.io/library/python:3.12-slim +FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index ce8f4b2c..59a5287d 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -5003,6 +5003,22 @@ - 复发自:BUG-307(当时把分盘句和宫位表从正文挪到界面后,入口合同锁死了那句提示词) - 修复版本:379f0e6f +## BUG-333 | staging 发布 API 镜像时 DaoCloud TLS 握手超时 + +- 状态:resolved +- 首次发现:2026-08-20 +- 最近更新:2026-08-20 +- 影响面:Gitea `backend-quality-gate` 的 `publish` job、`deploy/railway-api.Dockerfile`、`xiaoxin` runner 拉官方基础镜像 +- 用户现象:`validate` 通过后 `Build and publish exact-SHA ACR images` 在 API Dockerfile 第一行失败:`Head "https://m.daocloud.io/v2/docker.io/library/python/manifests/3.12-slim"` TLS handshake timeout。 +- 触发条件:向 `staging` 推送后 publish 构建 `deploy/railway-api.Dockerfile`。 +- 根因:API 基础镜像仍走 DaoCloud 代理。同一 runner 已验证可达华为 SWR / ECR / ACR;Web 镜像早已改用 `swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine`,API 未跟上。 +- 修复:API `FROM` 改为与 Web 相同的华为 SWR `docker.io/library` 路径,指向 `python:3.12-slim`。不改运行时端口、pip/apt 镜像或 ACR 目标仓库。 +- 验证:`tests/test_railway_deployment.py`、`frontend/tests/staging-backend-workflows.test.ts`。远端 publish 由本提交后的 Gitea quality gate 验收。 +- 防复发:API/Web 官方基础镜像必须走华为 SWR `ddn-k8s/docker.io/library`,合同禁止 `daocloud`。不要为了重试再加一套 `for attempt in 1 2 3`,workflow 里该循环次数已被合同锁死为 5。 +- 相关记录:BUG-149、BUG-150、BUG-332 +- 复发自:无 +- 修复版本:待提交 + ## BUG-329 | 生时纠正 Agent 回答在结算后一次性出现,推理中无法停止 - 状态:resolved diff --git a/frontend/tests/staging-backend-workflows.test.ts b/frontend/tests/staging-backend-workflows.test.ts index bffefb30..28702463 100644 --- a/frontend/tests/staging-backend-workflows.test.ts +++ b/frontend/tests/staging-backend-workflows.test.ts @@ -105,6 +105,7 @@ const railwayWebDockerfile = new URL( const nextConfig = new URL("../next.config.ts", import.meta.url); const instrumentation = new URL("../src/instrumentation.ts", import.meta.url); const frontendPackage = new URL("../package.json", import.meta.url); +const apiDockerfile = new URL("../../deploy/railway-api.Dockerfile", import.meta.url); function read(url: URL): string { return readFileSync(url, "utf8"); @@ -228,6 +229,15 @@ test("quality gate validates relevant changes once and publishes a digest manife assert.doesNotMatch(workflow, /(?:^|:)latest$/m); }); +test("staging images pull official bases from Huawei SWR instead of DaoCloud", () => { + const api = read(apiDockerfile); + const web = read(railwayWebDockerfile); + assert.match(api, /^FROM swr\.cn-north-4\.myhuaweicloud\.com\/ddn-k8s\/docker\.io\/library\/python:3\.12-slim$/m); + assert.match(web, /^FROM swr\.cn-north-4\.myhuaweicloud\.com\/ddn-k8s\/docker\.io\/library\/node:22-alpine/m); + assert.doesNotMatch(api, /daocloud/i); + assert.doesNotMatch(web, /daocloud/i); +}); + test("Gitea quality gate validates before publishing an immutable ACR manifest", () => { const workflow = read(giteaQualityWorkflow); @@ -237,6 +247,10 @@ test("Gitea quality gate validates before publishing an immutable ACR manifest", assert.equal((workflow.match(/runs-on: xiaoxin/g) ?? []).length, 2); assert.match(workflow, /validate:[\s\S]*?timeout-minutes: 45[\s\S]*?publish:[\s\S]*?timeout-minutes: 60/); assert.match(workflow, /GITEA_SHA: \$\{\{ gitea\.sha \}\}/); + assert.match( + workflow, + /docker build -f deploy\/railway-api\.Dockerfile -t "\$IMAGE_REPOSITORY:api-\$GITEA_SHA"/, + ); assert.match( workflow, /docker build --build-arg NEXT_DEPLOYMENT_ID="\$GITEA_SHA" -f deploy\/railway-web\.Dockerfile/, diff --git a/tests/test_railway_deployment.py b/tests/test_railway_deployment.py index 3f8d0b93..8454dd88 100644 --- a/tests/test_railway_deployment.py +++ b/tests/test_railway_deployment.py @@ -9,15 +9,18 @@ def test_railway_services_use_the_product_frontend_and_dynamic_ports() -> None: api = (ROOT / "deploy" / "railway-api.Dockerfile").read_text(encoding="utf-8") assert "COPY frontend/package.json frontend/package-lock.json" in web - assert "--hostname 0.0.0.0" in web and "${PORT:-3000}" in web + assert "HOSTNAME=0.0.0.0" in web + assert 'CMD ["node", "server.js"]' in web assert "next-env.d.ts" not in web - assert "FROM m.daocloud.io/docker.io/library/python:3.12-slim" in api + assert "FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim" in api + assert "daocloud" not in api.lower() assert "COPY SKILL.md mcp_server.py" in api assert "--host 0.0.0.0" in api and "${PORT:-5200}" in api assert "http.server" not in api assert "FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine" in web + assert "daocloud" not in web.lower() def test_web_image_copies_postcss_config_before_building_frontend() -> None: