From b624c43432957456d046c62022f7394c2d973798 Mon Sep 17 00:00:00 2001 From: WorkBuddy Date: Tue, 22 Sep 2026 10:54:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=E4=BE=9D=E6=8D=AE=20Actions=20?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E6=97=A5=E5=BF=97=E4=BF=AE=E5=A4=8D=E4=B8=89?= =?UTF-8?q?=E5=A4=84=E5=A4=B1=E8=B4=A5=EF=BC=88runner=20=E6=97=A0=20pnpm?= =?UTF-8?q?=20/=20=E9=97=A8=E7=A6=81=E7=BC=BA=E4=BE=9D=E8=B5=96=20/=20?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E9=85=8D=E7=BD=AE=E6=9C=AA=E5=85=A5=E5=BA=93?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一轮已把 CI 跑起来并读到真实结果:7 个 job 中 Mini Program 与 Test Shell Guard 通过,Admin Web、Backend、UI Contracts 三个失败,migration 与 e2e 因依赖 backend 被跳过。本轮全部依据 Actions 原始日志定位,而不是本地推测 —— 三者都不是业务代码问题。 1. Admin Web Build:runner 已不再预装 pnpm 日志:Unable to locate executable file: pnpm runner 镜像(ubuntu-24.04 / 2.337.0)的 PATH 里没有 pnpm,而 setup-node 的 cache: 'pnpm' 在 restore 阶段就需要调用 pnpm 可执行文件,于是 job 在第一步失败、 后续步骤全部 skipped。同批的 miniapp job 没配 cache 所以正常通过,正好反向印证。 改为先用 corepack 启用并钉住 pnpm@9.15.9,store 缓存改由 actions/cache 手动配置 (此时 pnpm 才存在于 PATH)。 2. UI Contracts:门禁依赖没装 日志:Cannot find module '.../miniapp/node_modules/typescript/lib/typescript.js' phase04 守卫会用 TypeScript 编译器 API 解析小程序源码,但本 job 此前不装依赖。 本地能跑通只因为工作区早已装过;CI 的每个 job 都在独立 runner 上, miniapp job 安装的依赖不会共享过来。补上 npm install,并修正 workflow 中 「零 npm 依赖、故无需装依赖」的错误注释 —— 与实际不符的声明本身就是缺陷来源。 3. Backend:断言了一个永远不会入库的文件 日志:DeploymentTests.testDockerComposeProdExists 期望 true 实际 false infrastructure/docker-compose.prod.yml 被 .gitignore 列在 「Secrets and certificates」段而排除出版本控制,但这是**误分类**: 该文件不含任何明文密钥,敏感值全部走 ${VAR} 注入、db 密码走 Docker secrets, 文件头注释本身也写着「敏感信息通过 .env 文件注入,不进 Git」。 后果是该测试「本地恒绿(文件在工作区)、CI 恒红(checkout 拿不到)」, 且从本地永远发现不了。现修正 .gitignore 把它纳入版本控制; 真正的密钥仍由 secrets/、certs/、backups/ 等规则挡住。 修复后,此前因 backend 失败而被跳过的 migration 与 e2e 两个 job 也会真正跑起来。 --- .github/workflows/ci.yml | 50 +++++++-- .gitignore | 12 +- infrastructure/docker-compose.prod.yml | 145 +++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 infrastructure/docker-compose.prod.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eda6f3..fddd71b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,16 +42,36 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'pnpm' - cache-dependency-path: admin-web/pnpm-lock.yaml + # 这里不能写 cache: 'pnpm'。 + # 2026-09-22 实测:runner 镜像(ubuntu-24.04 / runner 2.337.0)**已不再预装 + # pnpm**,而 setup-node 的 cache 步骤在 restore 阶段就需要调用 pnpm 可执行文件, + # 于是直接报 "Unable to locate executable file: pnpm",整个 job 在第一步失败、 + # 后续步骤全部 skipped —— 表现为 admin-web job 秒红,且与业务代码无关。 + # (同批的 miniapp job 没配 cache 所以活了下来,正好反向印证了这一点。) + # 因此:先让 pnpm 存在于 PATH,缓存改为手动配置。 - # 必须钉版本:runner 预装的 pnpm 会随 GitHub 镜像升级而漂移,而本仓库的 - # pnpm-workspace.yaml 含有 onlyBuiltDependencies 等版本敏感配置, - # 且 pnpm 9 与 10 对「是否默认放行依赖 build 脚本」的语义相反 - # (esbuild / vue-demi 的 postinstall 若被静默阻断,install 能过、build 会红, - # 且错误信息不指向真正原因)。钉死后 CI 语义不再随 runner 变化。 - - name: 锁定 pnpm 版本(消除 runner 预装版本漂移) - run: npm i -g pnpm@9.15.9 + - name: 启用并锁定 pnpm + # corepack 随 Node 分发,runner 上没有 pnpm 但有 corepack。 + # 钉 9.15.9 的理由:pnpm 9 与 10 对「是否默认放行依赖 build 脚本」语义相反, + # 而 pnpm-workspace.yaml 里的 onlyBuiltDependencies 正是版本敏感项 + # (esbuild / vue-demi 的 postinstall 若被静默阻断,会出现 install 能过、 + # build 变红,且错误信息不指向真正原因)。钉死后不再随 runner 镜像升级而变化。 + run: | + corepack enable pnpm + corepack prepare pnpm@9.15.9 --activate + pnpm --version + + - name: 解析 pnpm store 路径 + id: pnpm-store + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: 缓存 pnpm store + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('admin-web/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- - name: Install Dependencies working-directory: admin-web @@ -162,7 +182,7 @@ jobs: # 第 16/17 项:`this._x()` 无定义(运行期 TypeError,曾致所有请求 Promise 永不 settle) # 与组件 WXSS 违规选择器(标签/ID/属性选择器); # 第 18 项:api.js 请求层**行为**契约(桩 wx.request 真跑,非源码断言)。 - # 守卫是纯 node 内建模块、零 npm 依赖,故无需装依赖;统一执行器负责逐个跑 + 汇总 + 非零退出, + # 统一执行器负责逐个跑 + 汇总 + 非零退出, # 并校验守卫数量下限(防止守卫被删除/改名后仍然报绿)。 steps: - uses: actions/checkout@v4 @@ -172,6 +192,16 @@ jobs: with: node-version: '20' + # 2026-09-22 修正:本 job 此前注释称「零 npm 依赖,故无需装依赖」,这个声明是错的。 + # 至少 phase04 守卫会 import miniapp/node_modules/typescript/lib/typescript.js + # (用 TS 编译器 API 解析小程序源码)。本地能跑通只因为工作区早已装过 miniapp 依赖; + # CI 的每个 job 都在**独立 runner**上,miniapp job 装的东西不会共享过来, + # 于是这里直接 ERR_MODULE_NOT_FOUND。 + # 「依赖声明与实际不符」正是本地绿 / CI 红最典型的一类来源。 + - name: 安装 miniapp 依赖(phase04 守卫需要 typescript 编译器 API) + working-directory: miniapp + run: npm install + - name: 运行 UI 契约门禁 run: node scripts/run-ui-contracts.mjs diff --git a/.gitignore b/.gitignore index b07d802..9574000 100644 --- a/.gitignore +++ b/.gitignore @@ -70,7 +70,17 @@ Thumbs.db .DS_Store # Secrets and certificates -docker-compose.prod.yml +# +# 2026-09-22 修正:`docker-compose.prod.yml` 原先被列在本段(当作密钥)而排除出 +# 版本控制,这是**误分类** —— 该文件不含任何明文密钥:所有敏感值都通过 +# `${VAR:?请设置...}` 注入,db 密码走 Docker secrets(./secrets/db_password.txt), +# 文件头注释本身也写着「敏感信息通过 .env 文件注入,不进 Git」。 +# +# 后果:backend 的 `DeploymentTests.testDockerComposeProdExists` 断言该文件存在, +# 于是「本地恒绿(文件在工作区)、CI 恒红(checkout 拿不到)」—— +# 一次典型的本地绿/CI 红假绿,且从本地永远发现不了。 +# 生产部署配置需要可复现、可评审,应与 staging 版一致纳入版本控制。 +# 真正的密钥仍由下面三条挡住。 infrastructure/secrets/ infrastructure/certs/ infrastructure/backups/ diff --git a/infrastructure/docker-compose.prod.yml b/infrastructure/docker-compose.prod.yml new file mode 100644 index 0000000..b44cc68 --- /dev/null +++ b/infrastructure/docker-compose.prod.yml @@ -0,0 +1,145 @@ +# Production Docker Compose +# 生产环境配置,敏感信息通过 .env 文件注入,不进 Git +services: + postgres: + image: postgres:16-alpine + container_name: danyang-match-postgres + environment: + POSTGRES_DB: ${DB_NAME:-danyang_match} + POSTGRES_USER: ${DB_USER:-danyang} + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + ports: + - "127.0.0.1:5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./backups:/backups + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-danyang} -d ${DB_NAME:-danyang_match}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - danyang-net + secrets: + - db_password + restart: unless-stopped + + redis: + image: redis:7-alpine + container_name: danyang-match-redis + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:?请设置REDIS_PASSWORD} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?请设置REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - danyang-net + restart: unless-stopped + + minio: + image: minio/minio:latest + container_name: danyang-match-minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?请设置MINIO_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?请设置MINIO_SECRET_KEY} + volumes: + - minio_data:/data + networks: + - danyang-net + restart: unless-stopped + + backend: + build: + context: ../backend + dockerfile: Dockerfile + container_name: danyang-match-backend + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + SPRING_PROFILES_ACTIVE: docker,production + SPRING_CONFIG_IMPORT: configtree:/run/secrets/ + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: ${DB_NAME:-danyang_match} + DB_USER: ${DB_USER:-danyang} + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:?请设置REDIS_PASSWORD} + MINIO_ENDPOINT: http://minio:9000 + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请设置MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请设置MINIO_SECRET_KEY} + JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?请设置JWT_ACCESS_SECRET} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?请设置JWT_REFRESH_SECRET} + CHECKIN_TOKEN_SECRET: ${CHECKIN_TOKEN_SECRET:?请设置CHECKIN_TOKEN_SECRET} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:?请设置ENCRYPTION_KEY} + WECHAT_APP_ID: ${WECHAT_APP_ID:?请设置WECHAT_APP_ID} + WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:?请设置WECHAT_APP_SECRET} + VERIFICATION_PROVIDER: ${VERIFICATION_PROVIDER:?请设置VERIFICATION_PROVIDER} + SPRING_SECURITY_FALLBACK_PASSWORD: ${SPRING_SECURITY_FALLBACK_PASSWORD:?请设置SPRING_SECURITY_FALLBACK_PASSWORD} + FEATURES_APPEAL_ENABLED: ${FEATURES_APPEAL_ENABLED:-false} + ports: + - "127.0.0.1:8080:8080" + networks: + - danyang-net + secrets: + - source: db_password + target: spring.datasource.password + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + admin-web: + build: + context: ../admin-web + dockerfile: Dockerfile + container_name: danyang-match-admin + depends_on: + - backend + ports: + # 管理端端口统一 13520(与 admin-web/vite.config.ts 的开发端口一致, + # 由 ui-tests/ui-document-baseline-contracts.mjs 钉住三处一致)。 + # 仍绑定 127.0.0.1:对外访问走 nginx 的 80/443 + /admin/,此处仅供本机直连排查。 + - "127.0.0.1:13520:80" + networks: + - danyang-net + restart: unless-stopped + + nginx: + image: nginx:alpine + container_name: danyang-match-nginx + depends_on: + - backend + - admin-web + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx-production.conf:/etc/nginx/nginx.conf:ro + - ./certs:/etc/nginx/certs:ro + networks: + - danyang-net + restart: unless-stopped + +volumes: + postgres_data: + redis_data: + minio_data: + +networks: + danyang-net: + driver: bridge + +secrets: + db_password: + file: ./secrets/db_password.txt