Skip to content

chore(harness): worktree 不再静默跳门(绝对 hooksPath)+ E2E_PROXY 覆盖 prewarm + renovate vitest 族分组 - #753

Open
0xPabloLI wants to merge 33 commits into
devfrom
chore/harden-worktree-gates
Open

0xPabloLI wants to merge 33 commits into
devfrom
chore/harden-worktree-gates

Conversation

@0xPabloLI

@0xPabloLI 0xPabloLI commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • prepare 改为 npx husky && node scripts/husky-hooks-path.mjs:把共享的 core.hooksPath 从相对值 .husky/_ 改成主 checkout 的绝对路径
  • .husky/pre-commit / .husky/pre-push 开头加 node_modules 守卫,缺依赖时明确报错
  • 新增 scripts/lib/husky-hooks-path.mjs + scripts/husky-hooks-path.mjs + 17 条 node:test;npm run check:hooks-path 自查
  • 新增 e2e/browserProxy.ts:让 E2E_PROXY 也作用于 prewarm 浏览器(修一次真实的中断,见下文)+ 8 条用例含 wiring guard
  • renovate.json 给 vitest 族加分组规则
  • docs/workflows/cross-branch-workflow.md 新增「建 worktree 后的必做第一步:装依赖」

两笔功能 commit + 一笔 harness commit:

  • bac29c2c — hooksPath 绝对化 + node_modules 守卫 + renovate vitest 族分组 + 文档
  • 8d73964f — e2e prewarm 代理(e2e/browserProxy.ts 单一来源 + wiring guard)
  • 第三笔(见本 PR 最新 head)— pre-commit 在 typecheck 前先生成 icon/token manifest

第三条:全新 worktree 里连 commit 都提交不了

三个 *.generated 模块是 gitignore 的生成物。npm test 的第一步会生成它们,但 .husky/pre-commit 把 npm run typecheck 排在 npm test 之前 ⇒ 在一个刚 npm install、还没跑过测试的 linked worktree 里,任何提交都死在:

src/lib/chainIcons.ts(2,37): error TS2307: Cannot find module './chainIconManifest.generated'

读起来完全像代码回归。对照实验(在 /tmp 新 worktree 里做,不是猜):删掉生成物 ⇒ typecheck 报 3 条 TS2307;跑 node scripts/generate-icon-manifests.mjs(与 npm test 用的同一条命令)⇒ 0 条,且生成物仍被 git 忽略、不污染提交。

注:这条改动要等它进了主 checkout 的工作树才真正被执行——绝对 hooksPath 下 git 跑的是主 checkout 的 .husky/* 内容(本 PR 已把这条限制写在正文里)。

要修的是哪个洞(不是"少跑几步测试",是门整条消失)

husky 把 core.hooksPath 写成相对值 .husky/_,而那个目录由 npm install 的 prepare 生成(它内部放了一个内容为 * 的 .gitignore,即自我忽略,所以不进版本库)。git worktree add 出来的目录没有它 ⇒ git 在该 worktree 里按相对路径找不到任何 hook ⇒ pre-commit 与 pre-push 一条都不执行,而 git commit/git push 照样退出 0。

这比漏跑测试严重,因为本仓有四道门只存在于 pre-push:osv-scanner、semgrep、knip、dup:check。CI 里没有对应 job,而 CI 的 security-audit 还是 npm audit --omit=dev,看不见 dev 依赖的通告。⇒ 从干净 worktree 推上去的 PR 可以全绿而这四道门从没跑过。实际事故:#747 就是这么推上去的(内容无害,但等于绕过了本地门)。

机制与判据

场景 改前 改后
新 worktree 未装依赖,git commit 静默成功,门不跑 pre-commit 执行;守卫直接报「node_modules 不存在于 …,先 npm install」
同上 git push 静默成功,osv/semgrep/knip/dup 全没跑 门执行并按根因失败
主 checkout 不变 不变(绝对路径就指向它自己的 .husky/_)
别的仓库/自定义 hooksPath — 分类为 foreign,不改别人的配置
想退回 husky 默认行为 — HUSKY_HOOKS_PATH_ABSOLUTE=0(只认字面量 0,写错不会静默关闭 ⇒ R13 专门守这条)

端到端复现(不是只测函数):新建一个不装依赖的 worktree,尝试提交 ⇒ HEAD 未移动、文件停在 staged,门确实拦下了;同一操作在改前会静默成功。apply 两次输出 set 然后 noop(幂等,R10 守着);npm run check:hooks-path 报 gated。

测试:17 条 node:test 覆盖判定表;node --test "scripts/**/*.test.mjs" 154/154;npm test rc=0(3793 单测)。

一处如实的局限:绝对 hooksPath 下,git 执行的是主 checkout 工作树里的 .husky/* 内容。所以本 PR 合入前,其它 worktree 跑到的是旧版 hook(没有 node_modules 守卫,但四道门照常执行——修的是"完全不跑",守卫只是把失败变得可读)。合入并把 lovable 更新后,守卫才在所有 worktree 生效。

renovate.json 那条为什么一起放这里

vitest 与 @vitest/* 是 peer 锁死的一族:@vitest/coverage-v8@5 的 peer 要求 vitest 精确同版本,只升一半 ⇒ 严格 npm ci 在 peer-dep-check(dev 与 main 都必填)上失败。#731 就是这个病,而 Renovate 默认按包拆 PR,会原样重犯——所以本 PR(#748)只能人工把两包一起升。这里照 node runtime 那条的先例加分组规则,把"必须同批评审"写进配置而不是靠人记得。

附带修掉的第二个既存缺陷:E2E_PROXY 管不到 prewarm 浏览器

推送这台机器今天被 e2e 挡了一次,查下去发现不是环境问题那么轻——是个真缺陷:

playwright.config.ts 把代理只放进 project 的 use,覆盖的是测试用的浏览器;而 e2e/global-setup.ts 的 prewarm 自己 chromium.launch(),从不带代理。于是直连吞吐一差,warm-up 就卡在 portfolio-mode-toggle 的 120s 预算上,整轮在任何一条测试开始之前中止,文档里的逃生阀看起来完全失效。

量出来的判据(不是猜负载):

路径 ttfb total
直连 /markets(三次) 1.4 / 1.6 / 4.9s 82.6s / 100.6s / 150s+ 未传完(HTTP 都是 200)
走本机 7891 2.9s 4.2s

global-setup.ts 原注释里那句「while the /markets API itself had already returned 200 in ~1s」正是被证伪的前提:200 不说明传输时间。

改法:代理判定抽成 e2e/browserProxy.ts 单一来源,playwright.config.ts 与 global-setup.ts 都从这里取;src/test/browserProxy.test.ts 的 R8 是 wiring guard,断言两处都调用 browserProxyArgs() 且不再手写 process.env.E2E_PROXY——两份各写各的正是这次分叉的成因(照 e2eDevServerCommand() 消除命令副本的先例)。

验证:8 条用例绿;typecheck rc=0;e2e 显式 tsc rc=0(e2e/ 不在 lint/typecheck 覆盖内,按 AGENTS.md 单独跑);E2E_PROXY=… npm run test:e2e:pre-push ⇒ 改前 TimeoutError at global-setup.ts:38、0 条测试执行;改后 63 passed / 1 flaky / rc=0。那 1 条 flaky 是既存问题(标题由实盘数据生成,retry 后标题变化 ⇒ Test not found in the test file),与本次改动无关,已在重试后通过。

本次推送带 E2E_PROXY=http://127.0.0.1:7891:直连此刻传不动 413KB 响应。这不是绕门(同一个 knob 早已是 tests 的既有用法,现在只是让 prewarm 也认它),但如果出口长期如此,warm-up 的 120s 预算与 /markets 的体积需要另开票谈。

Test plan

  • CI peer-dep-check / lint / build 转绿
  • 合并并回灌 lovable 后,在一个不装依赖的新 worktree 里试 git commit,应看到 ❌ [pre-commit] BLOCKED: node_modules 不存在于 …
  • npm run check:hooks-path 在主 checkout 与 worktree 里都报 gated
  • 在直连缓慢的网络上 E2E_PROXY=… npm run test:e2e:pre-push 能跑过 warm-up(本次已实测)
  • 下个 Renovate 窗口确认 vitest 族更新是一笔 PR(含 @vitest/*)

0xPabloLI and others added 30 commits October 1, 2026 01:17
生产每次加载有 1 条 CSP console error:Cloudflare 自动注入的 Web Analytics beacon
bootstrap 被 script-src 拦。已实测证伪「加 hash」这条路——内联块里的 r/t 每请求都变
(两次拉取值不同),Chrome 建议的那个 hash 只对当次响应有效。

不是 #693/1320 引入的回归:1320 之前 script-src 就没有 unsafe-inline 也没有 hash,
这段一直被拦;1320 的 spec 只枚举了 consent 块与 gtag.js 两类。现在把第三类写进
spec,避免下个改 CSP 的 session 再当成新发现。
osv-scanner 从 10-04 起阻断所有本地推送:braces 3.0.3 命中 CVE-2026-93687(8.7 High),
而它上游没有修复版(npm 最新就是 3.0.3;OSV ranges 只有 last_affected、没有 fixed 事件),
所以不能像三天前 axios 那次用 overrides 抬版本解决。它是 tailwindcss@3 → chokidar/micromatch
的 dev 传依赖,不在生产产物里。

只豁免这一条、带理由与 ignoreUntil=2027-01-31(实测:未来日期放行、过期后重新报错,
是真执行的时间闸)。到期若仍无修复版,走根治:换掉 tailwind v3 的 watch/glob 依赖链。
AAV-1328。
自动发现的文件名不能带点(.osv-scanner.toml 实测不生效);未知键是整份配置拒绝
加载而非忽略该条,写错等于豁免全废;ignoreUntil 需 RFC3339 且真的会过期(两个方向
都实测过);osv 只在 pre-push、CI 的 security-audit 是 --omit=dev + continue-on-error,
所以存在「CI 全绿但所有人都推不动」的形态。
相对上一版的四处调整,依据都取一手源码:

1. 取消 weekly schedule,回到引擎默认 at any time,改由 prHourlyLimit=2 +
   prConcurrentLimit=5 节流。按小时滚动才是 Renovate 的模型;把一批更新攒到周一
   是 Dependabot 的成批观感,代价是双轨期看不到任何动作、出问题也发现得晚。
   prConcurrentLimit 5 仍对齐 dependabot.yml 的 open-pull-requests-limit。

2. 纳入 devcontainer manager:.devcontainer/devcontainer.json 的
   image(mcr.microsoft.com/devcontainers/typescript-node:22)与
   features(ghcr.io/devcontainers/features/github-cli:1)此前无人看守——
   dependabot.yml 只配了 npm 与 github-actions。该 manager 同时提取 image 与
   features 两处版本(源码 manager readme),正好覆盖我们 Codespaces 的两个
   版本点。typescript-node 与 CI 的 node-version 是跨消费者契约,故一律人工评审,
   不进 automerge 标签。

3. minimumReleaseAge=7 days 只加在 github-actions 的 non-major 规则上,不是全局。
   它复刻 scripts/check-dep-release-age.mjs 的供应链等待期,而那道门的判据是
   package-lock 的 diff,看不见 workflow 里的 action 钉版 ⇒ actions 此前没有任
   何等待期保护,这里是唯一能落地该策略的位置。

4. 同时设 minimumReleaseAgeBehaviour=timestamp-optional,因为等待期依赖
   release timestamp,而源码注明 Docker 源只在 Docker Hub 取 tag_last_pushed;
   我们两个引用都在 mcr.microsoft.com 与 ghcr.io,无时间戳时默认的
   timestamp-required 会把更新判成 pending 永久搁置(util/minimum-release-age.ts
   的 isPending 分支)。devcontainer 规则因此不加等待期,只设人工评审。

automerge 仍保持 false:合并决策继续走现有 automerge.yml 的 PAT 路径,避免开出
第二条合并通道(也保住 lovable→dev MERGE / 其余 SQUASH 的策略)。

生效前提:Renovate 只读默认分支的配置,本文件需再经 lovable→dev→main 一轮。
AAV-1320 在 Linear 已 Done 却还留在「仅列 open」表里,按文件自身规则移出。
dev 已由 #716(删除 Dependabot 配置)与 #717(Renovate 接管 npm + 7 天等待期)
定下"只用 Renovate"的方向;本分支的 renovate.json 与它从同一 v1 分叉,故以 dev
的决策为基底合并,只改两处会让策略静默变形的地方:

1. 规则按 manager 分域。dev 版两条 packageRules 没有 matchManagers,npm 进入
   enabledManagers 后,npm 的非 major 升级会被打上 github_actions 标签,更紧要的是
   会拿到 automerge 标签 ⇒ 运行时依赖的 minor/patch 将被自动合入。原
   dependabot-auto-triage.yml 的门槛是 npm 只放行 direct:development 的 patch/minor,
   运行时依赖一律人工评审。这里用 matchDepTypes 复刻该门槛:devDependencies 非 major
   才进 automerge 标签,dependencies(运行时)任何类型都不给,major 一律人工。

2. 加 minimumReleaseAgeBehaviour=timestamp-optional。等待期依赖 release 时间戳,
   源码注明 Docker 数据源的 tag_last_pushed 只在 Docker Hub 可得;纳入 devcontainer
   后,我们两个引用(mcr.microsoft.com、ghcr.io)都没有时间戳,默认的
   timestamp-required 会把它们判成 pending 永久搁置(util/minimum-release-age.ts 的
   isPending 分支)。

保留 dev 的选择:weekly schedule、prHourlyLimit 0、全局 7 天等待期、npm 接管。
新增 devcontainer manager(.devcontainer/devcontainer.json 的 image 与 features 此前
无人看守;typescript-node tag 与 CI 的 node-version 是跨消费者契约,故一律人工评审)。

另:不在此刻删除 dependabot-auto-triage.yml / dependabot-resolve-peer-conflicts.yml。
dependabot.yml 只在 dev 被删、main 上仍在 ⇒ Dependabot 仍会往 dev 开 PR,这两个
workflow 目前还活着;等 dependabot.yml 离开 main 之后再收。

验证:renovate.json 无冲突标记且全部键经官方 schema 校验通过(含 matchDepTypes);
overrides 在 merge 中存活(brace-expansion 1.1.21/5.0.12、fast-uri 3.1.8);
lock 与合并后 package.json 一致(npm install 无 diff);osv-scanner rc=0;
npm audit --omit=dev 0 漏洞;check:e2e-market-names 29 spec rc=0。
node 版本在本仓有两个家:workflow 的 node-version(走 node 数据源)与 Dev Container
镜像 typescript-node(走 docker 数据源),二者之间没有任何机制保证同步。正在开的
#727 就是这个裂口的形状——它只改 6 个 workflow 到 v24,不碰 .devcontainer,合下去
CI 就跑 Node 24、Codespaces 仍是 22(本仓无 .nvmrc、package.json 无 engines,所以
这两处就是全部真值)。

做法是跨 manager 分组而不是手工拼一个 PR:后者只修这一次,前者让"必须同批"成为
配置约束——matchDatasources [node, docker] + matchPackageNames [node,
/typescript-node/] → groupName "node runtime",标签 manual-review。分组只在
devcontainer manager 生效后才有意义,故依赖同一份配置里的 enabledManagers。

另:pre-push e2e 从 workers=2 降到 1。该路径跑冷启动 dev server,两条用例
(waitForTableReady、waitForLoadState('networkidle'))在别的 agent 门禁共用本机时
会超时——2026-09-29/30 两次推送被无关红挡下,安静复跑均全绿(其中 faq-anchor
本地 9/9、board 里另一 session 独立判为负载争用)。选择降并发而不是把用例加进
grep-invert 排除名单:排除等于让门禁少测东西,而慢一点不减覆盖。CI 侧的 2 分片
不变(跑 built preview,不受影响)。

验证:renovate.json 全部键经官方 schema 校验(含 matchDatasources/matchPackageNames/
groupName),7 条规则;prettier 干净;check:e2e-market-names rc=0。
建票时把 CF 边缘注入的 __CF$cv 块误归为 Web Analytics beacon。真浏览器复现 +
逐 inline script 扫字面量证明它加载 challenge-platform/scripts/jsd,且站内
已无 cloudflareinsights 引用;zone 在 Free 计划,该检测按一手文档不可关。
#716 删 .github/dependabot.yml 时,AAV-1301 的 ignore(@eslint/js >= 10)没有去处:
npm manager 已开、10.0.1 在 npm 上,下一个窗口会重新提出那个两次让 dev 的
peer-dep-check 与 lint 变红的 bump。用 allowedVersions "<10" 复刻原裁定,9.x 照旧流动。

droid-review.md 里 review_depth / security_scan_schedule 两条 workflow 实际并未设置,
删掉;后者改写为 `on: pull_request` 无 schedule 这种文件可证的说法。
reserves-table-market-filter-pin (5) 用"第 3 行"当作"筛选后会改序"的保证。CI 在
2026-10-05 的快照里整页都是同一个 market,筛选什么都没动,于是 pin 断言量到行仍停在
第三位,两次尝试 y 值一致 —— 确定性,不是抖动。旧选择器面对同一份数据也点到同一颗
chip,所以这不是 #735 的改动引入的。

改成扫可见行的 market chip label,取第一个与首行不同的行;整页同 market 时按本仓
discovery+条件 skip 的既有形状跳过并写明原因。纯函数进 e2e/reserveDiscovery.ts,
配 8 行矩阵单测(去掉 reference 条件会红 4 条,确认断言真咬住实现)。
:393 Path B 的等待是 poll tbody 行数 > 0,消息写着"等表格重新排序",实际只证明表格存在。
supply=900 在 CI 那份快照上没让列表移动,于是"按 Clear 必然触发 pin scroll"量到 0,首跑与
retry 同因,反复挡推送门。

改成用本文件已有的 getVisibleReserveOrder + didReorder(:259 早就这么守):候选场景值逐个试
到确实改序才继续,全都不改序就 skip 并写明原因。实测 :393 从 15.9s 失败变 9.7s 通过。
位置断言与"模拟子行仍可见"两次都通过,唯独 window.scrollBy 探针读 0:重新排序把目标行
上方移走之后,仅靠布局它就已经落进锚点带,不需要滚动。那条探针断言的是机制而不是契约,
且机制归 :259 那条用例专门拥有(改了序就滚、没改序就不滚),删掉重复不减少覆盖。

如实记:这是一次断言放宽,依据写在代码注释与看板第十一行段。
…/_ 导致 pre-push 整条门静默不跑 的机制发现)
…osv 门

6.1.4 命中 GHSA-rj75-hqrm-r3gf(5.9,dev-only);6.x 无修复版(最高 6.1.4),修复只在 7.1.6,
而 postcss-nested / tailwindcss 均声明 ^6.1.x ⇒ 正常升级路径不存在,只能像 fast-uri/js-yaml/ws
那样用 overrides 钉。

跨大版本强钉的验证:npm run build(Tailwind 实际编译)rc=0,且产出的 CSS 逐字节一致
(index-DyVJKc4h.css / vendor-blockchain-DR00Q9RG.css 内容哈希同名,拼接 md5 相同),
osv-scanner rc=0。备选是 osv-scanner.toml 的 ignoreUntil 条目(AAV-1328 对 braces 的做法)。
husky 写的 core.hooksPath 是相对值 .husky/_,而该目录只在 npm install 的 prepare
里生成(且自我忽略:.husky/_/.gitignore 内容是 *)。新 worktree 没这个目录 ⇒ git
找不到任何 hook ⇒ pre-commit/pre-push 整条不执行,而 commit/push 仍退出 0。
本仓有四道门只存在于 pre-push(osv-scanner / semgrep / knip / dup:check),CI 里
没有对应 job,所以这条路会产出「PR 全绿但门从没跑过」。#747 就是这样推上去的。

prepare 改为 `npx husky && node scripts/husky-hooks-path.mjs`:把共享 config 里的
hooksPath 指向主 checkout 的绝对路径,任何 worktree 都解析得到;配套在两个 hook
开头加 node_modules 守卫,缺依赖时明确报错。opt-out: HUSKY_HOOKS_PATH_ABSOLUTE=0;
自查: npm run check:hooks-path。附 17 条 node:test 覆盖判定表(不覆盖非本仓所有的
hooksPath、幂等、只认字面量 0 才关闭)。

另在 renovate.json 给 vitest 族加分组规则:vitest 5 的 peer 要求 @vitest/* 同版本,
只升一半会让严格 npm ci 在 peer-dep-check(dev/main 均必填)上失败——#731 即此病,
换成 Renovate 也会重犯,除非同批评审(照 node runtime 那条的先例)。
playwright.config 只把 proxy 放进 project 的 use,覆盖的是"测试用的浏览器";
e2e/global-setup.ts 自己 chromium.launch() 起的那个始终走直连。直连吞吐一差,
warm-up 就卡在 portfolio-mode-toggle 的 120s 预算上,整轮在任何一条测试开始之前
中止——于是文档里的 E2E_PROXY 看起来完全失效。

实测判据(2026-10-07):/markets 三次直连 ttfb 1.4–5.0s 但 total 82s/100s/150s+
都还没传完(HTTP 都 200),而本机 mixed 口 7891 用 4.2s。所以旧注释里那句
"API 已在 ~1s 返回 200"正是被证伪的前提:200 不说明传输时间。

改法是把代理判定抽成 e2e/browserProxy.ts 的单一来源,两个消费者都从这里取,
避免再次各写各的;src/test/browserProxy.test.ts 的 R8 是 wiring guard,直接断言
两处都调用 browserProxyArgs 且不再手写 process.env.E2E_PROXY。

验证:8 条用例绿、typecheck rc=0、e2e 显式 tsc rc=0(e2e 不在 lint/typecheck 覆盖内)、
E2E_PROXY 下 test:e2e:pre-push 63 passed / 1 flaky(既存:标题由实盘数据生成,
retry 后标题变化报 Test not found)/ rc=0;改前同一命令在 warm-up 处 TimeoutError 中止。
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aave-protocol-explorer Ready Ready Preview Oct 7, 2026 4:29pm UTC

@factory-droid

factory-droid Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Droid finished @0xPabloLI's task —— View job


A well-tested harness hardening PR whose core mechanism (absolute hooksPath, fail-loud node_modules guards, single-source e2e proxy) is sound, with one real bug: the self-check's advertised remediation command cannot actually apply because the npm script hardwires --check. No security findings — command execution uses fixed argv arrays, hooksPath is derived only from git's own metadata, and both env-var opt-outs fail closed.

1 inline comment posted

return {
ok: false,
verdict: 'relative-hooks-path',
hint: `core.hooksPath is "${configValue || '(unset)'}" — linked worktrees silently run no hooks; run \`npm run check:hooks-path -- --apply\` or reinstall`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Advertised fix command npm run check:hooks-path -- --apply is a no-op

The relative-hooks-path verdict tells users to run npm run check:hooks-path -- --apply, but the npm script is pinned to node scripts/husky-hooks-path.mjs --check in package.json, so the trailing -- --apply yields --check --apply. In scripts/husky-hooks-path.mjs, checkOnly = argv.includes('--check') is then true, which short-circuits the apply branch (if (!checkOnly && decision.action === 'set')), so the suggested command just re-reports the same failure and exits 1 instead of repairing core.hooksPath. The one remediation path the gate itself recommends can never fix the state it diagnoses; the working invocation is only the bare node scripts/husky-hooks-path.mjs --apply listed in the CLI header, which the hint never mentions.

Suggested change
hint: `core.hooksPath is "${configValue || '(unset)'}" — linked worktrees silently run no hooks; run \`npm run check:hooks-path -- --apply\` or reinstall`,
hint: `core.hooksPath is "${configValue || '(unset)'}" — linked worktrees silently run no hooks; run \`node scripts/husky-hooks-path.mjs --apply\` or reinstall`,

三个 *.generated 模块是 gitignore 的生成物:npm test 的第一步会生成它们,但 pre-commit
把 typecheck 排在 npm test **之前** ⇒ 在一个刚 npm install、还没跑过测试的 linked worktree 里,
任何提交都死在 TS2307 Cannot find module './chainIconManifest.generated',读起来完全像代码回归。

对照实验(在 /tmp 新 worktree 里做的):删掉生成物 → typecheck 报 3 条 TS2307;
跑 node scripts/generate-icon-manifests.mjs → 0 条。命令与 npm test 用的是同一个,
生成物本身仍被忽略,不会污染提交。

注:这条改动要等它进了主 checkout 的工作树才会真正被执行——绝对 hooksPath 下 git 跑的是
主 checkout 的 .husky/* 内容(PR 正文里已记这条限制)。

This branch was successfully deployed

1 active deployment
Preview — e76d1992 Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant