From 04c4bd3eadd8f4dc25963a27eecca47d3738f0bb Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 9 Aug 2026 13:18:47 +0800 Subject: [PATCH] ci: isolate staging dependency install --- .gitea/workflows/backend-quality-gate.yml | 23 ++++++++++++++---- docs/BUG_HISTORY.md | 16 +++++++++++++ .../tests/staging-backend-workflows.test.ts | 24 +++++++++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/backend-quality-gate.yml b/.gitea/workflows/backend-quality-gate.yml index 6dcaf261..b263f793 100644 --- a/.gitea/workflows/backend-quality-gate.yml +++ b/.gitea/workflows/backend-quality-gate.yml @@ -32,6 +32,8 @@ jobs: timeout-minutes: 45 env: GITEA_SHA: ${{ gitea.sha }} + NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c + NODE_TOOL_IMAGE: node:22-bookworm-slim steps: - name: Checkout exact Gitea revision run: | @@ -96,9 +98,6 @@ jobs: docker compose --help | grep -q -- '--project-name' - name: Prepare pinned Node tooling - env: - NODE_TOOL_SOURCE_IMAGE: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c - NODE_TOOL_IMAGE: node:22-bookworm-slim run: | set -euo pipefail if ! docker image inspect "$NODE_TOOL_SOURCE_IMAGE" >/dev/null 2>&1; then @@ -167,7 +166,23 @@ jobs: "pandas==2.3.3" \ "timezonefinder==8.2.5" \ -r requirements.txt -r requirements-dev.txt - npm ci --prefix frontend + workdir="$(pwd -P)" + docker run --rm \ + --cpus=1.5 \ + --memory=2g \ + --memory-swap=2g \ + --pids-limit=256 \ + --user "$(id -u):$(id -g)" \ + --volume "$workdir:$workdir" \ + --workdir "$workdir" \ + --env HOME=/tmp \ + --env "NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY" \ + "$NODE_TOOL_SOURCE_IMAGE" \ + npm ci --prefix frontend \ + --no-audit \ + --no-fund \ + --progress=false \ + --maxsockets=4 - name: Validate backend, package, frontend, and database contracts run: | diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 9484e8fd..8cf65c93 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2536,3 +2536,19 @@ - 相关记录:BUG-146、BUG-147 - 复发自:无 - 修复版本:2026-08-08 staging 变更 + +## BUG-149 | staging quality gate 宿主 npm ci 触发控制面持续 502 并被基础设施终止 + +- 状态:resolved(local candidate,远端 gate/deploy 待本提交) +- 首次发现:2026-08-09 +- 最近更新:2026-08-09 +- 影响面:Gitea staging quality gate 的依赖安装阶段、`manman-linux` runner 与 Gitea/runner 控制面可用性;测试、lint、build、exact-SHA checkout/publish/deploy 合同未改变。 +- 用户现象:quality gate 在宿主机直接执行 `npm ci` 期间,Gitea 与 runner 控制面持续返回 502;步骤日志没有 npm 安装错误,任务最终由基础设施终止,无法进入后续远端 gate/deploy 收口。 +- 触发条件:self-hosted runner 在宿主环境执行无独立资源边界的 frontend `npm ci`,同时由共享控制面维持 job 调度、状态与日志链路。 +- 根因:依赖安装直接占用宿主资源且缺少 CPU、内存、swap、PID 与并发 socket 上限,故障证据落在 runner/控制面资源争用和基础设施终止边界,而不是 npm 包解析或安装错误。 +- 修复:仅将 `npm ci` 放入 digest-pinned Node 容器,并限制为 CPU 1.5、memory 2g、no swap、pids 256、`npm_config_maxsockets=4`;测试、lint、build 与 exact-SHA 行为保持原样,不扩大容器化范围,也不改变发布和部署信任边界。 +- 验证:本地 deployment contracts 45/45、workflow contracts 29/29;独立 review 无 P0/P1。远端 staging gate/deploy 待本提交后验证,本记录不提前声称远端收口。 +- 防复发:宿主依赖安装必须维持 digest-pinned Node runtime 和显式资源上限;合同测试需锁定只有 `npm ci` 在受限容器内执行,并持续确认测试、lint、build、exact-SHA checkout/publish/deploy 语义未漂移。 +- 相关记录:BUG-129、BUG-136、BUG-142 +- 复发自:无 +- 修复版本:待本次提交 / gate / deploy diff --git a/frontend/tests/staging-backend-workflows.test.ts b/frontend/tests/staging-backend-workflows.test.ts index 9d69afa1..26227ca1 100644 --- a/frontend/tests/staging-backend-workflows.test.ts +++ b/frontend/tests/staging-backend-workflows.test.ts @@ -171,8 +171,8 @@ test("Gitea quality gate validates before publishing an immutable ACR manifest", assert.equal((workflow.match(/timeout 180 docker pull "\$NODE_TOOL_SOURCE_IMAGE"/g) ?? []).length, 2); assert.equal((workflow.match(/docker tag "\$NODE_TOOL_SOURCE_IMAGE" "\$NODE_TOOL_IMAGE"/g) ?? []).length, 2); assert.equal((workflow.match(/cat > "\$tool_dir\/node" <<'EOF'/g) ?? []).length, 1); - assert.equal((workflow.match(/--volume "\$workdir:\$workdir"/g) ?? []).length, 2); - assert.equal((workflow.match(/--workdir "\$workdir"/g) ?? []).length, 2); + assert.equal((workflow.match(/--volume "\$workdir:\$workdir"/g) ?? []).length, 3); + assert.equal((workflow.match(/--workdir "\$workdir"/g) ?? []).length, 3); assert.equal((workflow.match(/ln -s node "\$tool_dir\/npm"/g) ?? []).length, 1); assert.equal((workflow.match(/>> "\$GITHUB_PATH"/g) ?? []).length, 2); assert.match(workflow, /node:22-bookworm-slim "\$\{0##\*\/\}" "\$@"/); @@ -182,6 +182,26 @@ test("Gitea quality gate validates before publishing an immutable ACR manifest", assert.match(workflow, /docker cp "\$container_id:\/usr\/local\/lib\/node_modules\/npm" "\$tool_dir\/npm-package"/); assert.match(workflow, /node --version \| grep -Eq '\^v22\\\.'/); assert.match(workflow, /npm --version/); + assert.match( + workflow, + /validate:[\s\S]*?env:\n\s+GITEA_SHA: \$\{\{ gitea\.sha \}\}\n\s+NODE_TOOL_SOURCE_IMAGE: swr\.cn-north-4\.myhuaweicloud\.com\/ddn-k8s\/docker\.io\/library\/node:22-bookworm-slim@sha256:ef343465b6a14bbdf2ab52f6e100ec0659a792464fcf72c462370d88b3df909c\n\s+NODE_TOOL_IMAGE: node:22-bookworm-slim[\s\S]*?- name: Prepare pinned Node tooling/, + ); + const installStep = workflow.match( + /- name: Install dependencies[\s\S]*?(?=\n\s+- name: Validate backend, package, frontend, and database contracts)/, + )?.[0] ?? ""; + assert.match(installStep, /workdir="\$\(pwd -P\)"/); + assert.match(installStep, /docker run --rm/); + assert.match(installStep, /--cpus=1\.5/); + assert.match(installStep, /--memory=2g/); + assert.match(installStep, /--memory-swap=2g/); + assert.match(installStep, /--pids-limit=256/); + assert.match(installStep, /--user "\$\(id -u\):\$\(id -g\)"/); + assert.match(installStep, /--volume "\$workdir:\$workdir"/); + assert.match(installStep, /--workdir "\$workdir"/); + assert.match(installStep, /--env HOME=\/tmp/); + assert.match(installStep, /--env "NPM_CONFIG_REGISTRY=\$NPM_CONFIG_REGISTRY"/); + assert.match(installStep, /"\$NODE_TOOL_SOURCE_IMAGE" \\\n\s+npm ci --prefix frontend \\\n\s+--no-audit \\\n\s+--no-fund \\\n\s+--progress=false \\\n\s+--maxsockets=4/); + assert.doesNotMatch(installStep, /--ignore-scripts|--omit(?:=|\s+)optional|--force/); assert.match(workflow, /docker compose version --short/); assert.match(workflow, /docker compose --help \| grep -q -- '--project-name'/); assert.match(