---
date: 2026-05-19
type: error
tags: [ci, gitea-actions, setup-node, cache, host-runner, performance, dedicated-runner]
---

# `actions/setup-node@v4` 的 `cache: 'npm'` 在 host-persistent runner 上纯冗余，post-step tar 3.4 GB 占 10 min/job

## 现象

`quality-gates::build-check` 每个 PR 跑 14.5 分钟，但 step 加总只有 4 分钟。**多出来的 10 分钟全在 `Post Setup Node.js` 这步**——`actions/setup-node@v4` 在 job 收尾 tar `~/.npm` 3.4 GB（zstdmt 8 min CPU bound）+ 上传 ~1 GB 给 act_runner 本地 cache server（2 min）。`contract-check` 同症状（实测 409s，~6 min 在 post cache save）。

PR #422（改 backend 2 文件，2026-05-18 跑）实测：

| Step | 时长 |
|---|---|
| 全部业务 step 加总 | **249s ≈ 4 min** |
| Job 总时长 | **867s ≈ 14.5 min** |
| "幽灵"差值 | **post-step `Post Setup Node.js`：10:10:17 → 10:20:08，10 min** |

## 根因

`actions/setup-node@v4` 自带 `cache: 'npm'` 是给 **GitHub-hosted runner（ephemeral VM）** 设计的——每次 job 全新 VM，`~/.npm` 不存在，必须从 cache server 下载再还原；job 结束 VM 销毁，所以 save 是有意义的。

但本项目 dedicated-runner-1（`43.166.205.48`）/ -2（`43.166.182.155`）是 **host 模式**（`act_runner config` labels 含 `:host`），跨 job 不开容器，`~/.npm` 在 ubuntu 用户家目录里**天然持续**。SSH 实测：

```
$ du -sh ~/.npm/_cacache
28G  /home/ubuntu/.npm/_cacache

$ sudo du -sh /var/lib/act_runner/cache
21G  /var/lib/act_runner/cache       # act_runner 本地 cache server 又冗余存了一份
$ sudo find /var/lib/act_runner/cache -type f | wc -l
22                                    # 22 个 cache entry，平均 ~1 GB
```

问题链：

1. `actions/setup-node@v4` 不知道自己跑在 host-persistent runner 上
2. 默认 cache restore：从 `http://localhost:.../cache/...` 下载 ~1 GB tar → 解压回 `~/.npm`（覆盖原本就在的内容）
3. `npm install` 命中 `~/.npm`（本来就命中，cache 是否存在不影响）
4. 默认 cache save：tar `~/.npm` 3.4 GB → 上传给本机 act_runner cache server
5. **整套 restore + save 都是纯冗余**——没有 cache 也一样命中

setup-node 自带 cache 的行为还跟 `actions/cache@v4` 不同：
- `actions/cache@v4`：cache key 命中 → 不 save → 0s（Next.js build cache 那个就是这样）
- **`setup-node@v4` 内置 cache：每次都 save**（即使 lockfile 没变、cache 没新增），它假设 `npm install` 会修改 `~/.npm`

## 解法

`.gitea/workflows/quality-gates.yml` 两处 setup-node 删 `cache: 'npm'` + `cache-dependency-path:` 共 4 行：

```yaml
- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 22
    # cache: 'npm'                       ← 删
    # cache-dependency-path: |           ← 删
    #   backend/package-lock.json        ← 删
    #   frontend/package-lock.json       ← 删
```

`npm install` 仍然命中 `~/.npm`，install 时间不变。

**保留** `actions/cache@v4` 配的 Next.js build cache（`frontend/.next/cache` 只 ~200 MB，命中后不 save，ROI 正向）。

## 工程化保险

1. **`/var/lib/act_runner/cache` 一次性清** —— 两台 dedicated-runner host 各 `sudo rm -rf /var/lib/act_runner/cache/*` 释放 **42 GB**。act_runner 重启不需要，目录会自动重建。act_runner 自己仍管 actions/cache@v4 的入口，所以下次 cache hit 还是会重建必要 entry，只是少了 setup-node 那份冗余。
2. **元规则沉淀** —— 应用业界通用 action 前先校核"它的设计假设是否匹配本项目环境"。`setup-node` cache 是 GitHub-hosted ephemeral 假设，跟 host-persistent runner 错配；类似的还有 `cache@v4` 在 self-hosted 上也常常 ROI 负向（cache server 跟 runner 在同一台机器，I/O 没省）。延伸 `CLAUDE.md` §11（外部 API 调用优先评估 CLI 化）的元规则精神——"外部依赖应用前先评估是否匹配本项目环境"。后续若再遇同类反例，考虑在 `docs/standards/` 立一条独立元规则"通用 CI action / 第三方依赖应用前评估其设计假设"。
3. **未来再加 setup-node 步骤时默认不开 `cache:`** —— 等真有需要再开（极少；多数情况 `~/.npm` 已够用）。

## ROI

| 维度 | 改前 | 改后 |
|---|---|---|
| `build-check` 时长 | 14.5 min | **~5 min** |
| `contract-check` 时长 | 7 min | **~1.5 min** |
| 每 PR 节省 | — | **~10-15 min** |
| Runner 磁盘冗余 | 21 GB × 2 host | **0 + 释放 42 GB** |
| Runner cache server 流量 | ~2 GB/PR（本机 IPC） | 0 |

## 教训

1. **行业默认 action 配置不等于"对本项目最优"** —— `setup-node@v4` 90% 用户在 GitHub-hosted 上，默认配置围绕那个场景调优；self-hosted host 模式跑同一份配置，行为可能从"正向加速"翻成"纯冗余 10 分钟"。
2. **job 总时长 vs step 加总差很多就值得查 post-step** —— 14.5 min 全长 / 4 min step 加总 = 10 min "幽灵"必然有 post 阶段在干事。Gitea Actions UI 默认折叠 post-step，要主动展开看。
3. **持续验证 ROI** —— 加 cache 是为了快，但 cache 本身有维护成本（tar/upload/save）。每加一个 cache 必须实测前后时长，确认确实快了；自动相信"加 cache = 更快"是反例。

## 关联

- Gitea issue: #456
- 调研背景：PR #446 期间用户问"backend-integration 慢怎么解决"，调研发现真瓶颈不在 backend-integration（106s）而在 build-check 的 setup-node cache save
- 元规则：[[CLAUDE-md-section-11-evaluate-before-applying-generic-tools]]
- 同类思路：dedicated-runner host 模式定义见 `docs/ops/02-gitea-config.md` §3
