fix(ci): 依据 Actions 真实日志修复三处失败(runner 无 pnpm / 门禁缺依赖 / 生产配置未入库)

上一轮已把 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 也会真正跑起来。
This commit is contained in:
WorkBuddy
2026-09-22 10:54:24 +08:00
parent c6b48d5d91
commit b624c43432
3 changed files with 196 additions and 11 deletions
+40 -10
View File
@@ -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
+11 -1
View File
@@ -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/
+145
View File
@@ -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