AIRI 单仓库的 pnpm CI/CD 实践:冻结锁文件、Store 缓存与 Monorepo 增量构建
本文基于 AIRI 仓库内置的 pnpm 技能文档 best-practices-ci.md,系统讲解 pnpm 在 CI/CD 环境中的最佳实践:GitHub Actions / GitLab CI / Docker 三类流水线的完整配置、关键安装标志位(--frozen-lockfile、--prefer-offline、--ignore-scripts)、Corepack 版本锁定,以及 Monorepo 下的增量构建策略。读完本文,你能将同一套 pnpm CI 方案迁移到自己仓库,并理解 AIRI 这类大型 Monorepo 是如何在真实流水线中落地这些实践的。
pnpm 的 CI 自动行为:先搞懂这三件事
在写任何 YAML 之前,先理解 pnpm 检测到 CI 环境(CI 环境变量等)后的三个自动行为,它们决定了后续所有配置的逻辑起点(引自文档开篇):
- 自动进入 frozen-lockfile 模式:CI 中 pnpm 默认不允许更新
pnpm-lock.yaml,锁文件与实际依赖不一致时安装直接失败。 - v11 起对不兼容锁文件直接报错:由更高主版本的 pnpm 生成的锁文件,低版本 pnpm 在 CI 中不再尝试重写,而是直接失败——这要求 CI 使用的 pnpm 版本必须与生成锁文件时的版本保持同一大版本。
- 全局虚拟存储(global virtual store)在 CI 中被自动禁用:CI 环境没有可复用的预热缓存,不要指望跨构建复用的全局虚拟存储来加速 CI。
AIRI 仓库本身就是这条规则的实践者:根 package.json 中通过 "packageManager": "pnpm@11.24.0" 精确锁定 pnpm 11.x,而仓库根的 pnpm-lock.yaml 声明 lockfileVersion: '9.0'——锁文件版本与 pnpm 大版本严格对应。CI 工作流 .github/workflows/ci.yml 的每个 job 都通过 pnpm/setup@v2 行动显式指定 runtime,保证与本地 .tool-versions 中声明的 nodejs 26.7.0 版本一致,从工具链层面杜绝“CI 与锁文件生成环境不同步”的问题。
GitHub Actions
基本配置
文档给出的最简 CI 模板如下,覆盖了“安装 pnpm → 配置 Node 并开启缓存 → 冻结锁文件安装 → 测试 → 构建”的完整链路:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile # or: pnpm ci
- run: pnpm test
- run: pnpm build
这里要区分两个等价但语义不同的命令:
pnpm install --frozen-lockfile:安装依赖,若锁文件需要更新则失败;pnpm ci(别名clean-install、install-clean):等价于pnpm clean+pnpm install --frozen-lockfile,即先清理node_modules再按锁文件全新安装,适合追求“完全可复现”的 CI 构建。
AIRI 的 ci.yml 采用了更新一代的 pnpm/setup@v2 action(合并了 pnpm 安装与 Node runtime 配置),并用 cache: true 开启内置缓存、install: false 把安装步骤留给显式的 pnpm install --frozen-lockfile,与模板中的模式一一对应。
Store 缓存:大型仓库的关键优化
对依赖庞大的项目,应额外缓存 pnpm 的内容寻址 store(全局包实体存放地)。文档推荐的写法是先用 pnpm store path 解析出 store 目录,再交给 actions/cache:
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- run: pnpm install --frozen-lockfile
缓存 key 以 pnpm-lock.yaml 的哈希为核心:锁文件不变时精确命中;变更时通过 restore-keys 前缀回退到旧缓存部分复用。
信任边界提醒(引自文档原文): 只在可信任务之间缓存/恢复 pnpm store 与缓存目录。一个不可信任务可写入的 store,绝不能被可信任务复用——store 属于 pnpm 的信任域,缓存污染等价于供应链攻击面。
在 AIRI 中,缓存实际上由两层组成:一是 pnpm store(pnpm/setup@v2 的 cache: true 自动处理),二是 Turborepo 的构建缓存。ci.yml 中专门有一步用 actions/cache@v5 缓存 .turbo 目录,key 为 ${{ runner.os }}-turbo-${{ github.sha }} 并按前缀回退。两者配合时,CI 的耗时大头(装依赖 + 重复构建)都被压缩。
矩阵测试(Matrix Testing)
文档给出的跨 OS × 跨 Node 版本矩阵模板:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm test
AIRI 的 build-test job 是矩阵思路的另一种应用维度——按应用切分而非按 OS 切分:ci.yml 的 strategy.matrix.include 中列出了 stage-web、stage-tamagotchi、stage-tamagotchi-godot、ui-transitions、ui-loading-screens 五个条目,每个条目绑定一条构建命令(如 pnpm -F @proj-airi/stage-web run build),让不同产物的构建并行且互不阻塞。这种“一个矩阵单元 = 一个可交付产物”的划分方式,与本文后面的 Monorepo 增量策略一脉相承。
GitLab CI
文档提供的 GitLab 完整配置(node 镜像 + corepack 激活 + store 目录缓存 + 三阶段流水线):
image: node:20
stages:
- install
- test
- build
variables:
PNPM_HOME: /root/.local/share/pnpm
PATH: $PNPM_HOME:$PATH
before_script:
- corepack enable
- corepack prepare pnpm@latest --activate
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .pnpm-store
install:
stage: install
script:
- pnpm config set store-dir .pnpm-store
- pnpm install --frozen-lockfile
test:
stage: test
script:
- pnpm test
build:
stage: build
script:
- pnpm build
两个值得注意的实现细节:
- 通过
pnpm config set store-dir .pnpm-store把 store 移到工作区内的.pnpm-store目录,使其可被 GitLab 的cache.paths捕获(GitLab 缓存的是路径而非环境目录); corepack prepare pnpm@latest --activate是模板写法,实际使用时建议替换为精确版本(如pnpm@11.24.0),与“CI pnpm 版本须与锁文件生成方一致”的原则一致。
Docker
PATH 变更(v11,引自文档原文): 自 pnpm v11 起,全局 pnpm 二进制位于
$PNPM_HOME/bin。在 Docker 中应设置ENV PATH="$PNPM_HOME/bin:$PATH"(而非旧式的$PNPM_HOME)。另外官方镜像ghcr.io/pnpm/pnpm:<version>(Debian slim、仅含 pnpm)可用于自选 Node 的场景,通过pnpm runtime set node <ver> -g或devEngines.runtime指定 Node。
多阶段构建
文档给出的通用多阶段模板——先按“清单文件优先”的顺序 COPY 以最大化层缓存,构建阶段与运行阶段分离:
# Build stage
FROM node:24-slim AS builder
# Enable corepack for pnpm
RUN corepack enable
WORKDIR /app
# Copy package files first for layer caching
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/*/package.json ./packages/
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm build
# Production stage
FROM node:20-slim AS runner
RUN corepack enable
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
# Production install
RUN pnpm install --frozen-lockfile --prod
CMD ["node", "dist/index.js"]
模板中“先 COPY 清单文件、后 COPY 源码”的顺序是层缓存技巧的核心:只要 package.json / pnpm-lock.yaml / pnpm-workspace.yaml 不变,pnpm install --frozen-lockfile 这一层就命中缓存。
AIRI 仓库 apps/stage-web/Dockerfile 提供了另一个真实变体:它没有走“清单优先”的 COPY 拆分,而是用 BuildKit 的 cache mount 把 pnpm store 挂到构建缓存层中,跨构建复用了依赖实体:
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
pnpm install --frozen-lockfile
两种写法(清单文件层缓存 vs BuildKit cache mount)目标相同——让 store 层面的下载/解压只发生一次——可按 CI 平台能力选用。stage-web 的构建阶段还会串起三个产物:pnpm -F @proj-airi/stage-web run build、pnpm -F @proj-airi/docs run build:base、pnpm -F @proj-airi/stage-ui run story:build,最后由 nginx:stable-alpine 作为 production stage 托管静态资源(Dockerfile)。
Monorepo 优化
针对 Monorepo 的镜像构建,文档强调保持目录结构地 COPY 各子包的 package.json,再用 --filter 只构建目标包:
FROM node:20-slim AS builder
RUN corepack enable
WORKDIR /app
# Copy workspace config
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
# Copy all package.json files maintaining structure
COPY packages/core/package.json ./packages/core/
COPY packages/api/package.json ./packages/api/
# Install all dependencies
RUN pnpm install --frozen-lockfile
# Copy source
COPY . .
# Build specific package
RUN pnpm --filter @myorg/api build
注意 pnpm install 仍需在 workspace 根执行一次(单锁文件模型决定 Monorepo 不能按子包单独安装),--filter 只用于收敛构建/运行范围。AIRI 的所有脚本都遵循这一模式,例如根 package.json 中 "dev": "pnpm -r -F @proj-airi/stage-web dev"、"build:packages": "turbo run build -F=\"./packages/*\"。
关键 CI 标志位详解
--frozen-lockfile:CI 的默认纪律
永远在 CI 中使用。 当 pnpm-lock.yaml 与 package.json 声明不一致、需要更新时直接失败,把“锁文件过期”这类问题拦在合并之前,而不是让 CI 默默改写锁文件:
pnpm install --frozen-lockfile
AIRI 的 CI 中每条 job(lint / build-test / unit-test / typecheck)都显式执行 pnpm install --frozen-lockfile(ci.yml),没有依赖 pnpm 的 CI 自动检测,属于双保险。
--prefer-offline
优先使用本地 store 中已有的包实体,仅在缺失时访问网络——与 store 缓存配合后,命中缓存的构建几乎不产生外网请求:
pnpm install --frozen-lockfile --prefer-offline
--ignore-scripts
跳过所有生命周期脚本以加速安装,但需谨慎使用(文档原话:use cautiously):部分依赖(native 模块、需要 postinstall 拉取二进制的包)依赖脚本才能工作。
pnpm install --frozen-lockfile --ignore-scripts
在 AIRI 这类含大量 native/编译型依赖的仓库中,更精细的控制粒度是 pnpm-workspace.yaml 中的 allowBuilds 映射(v11 取代了旧版的 onlyBuiltDependencies/neverBuiltDependencies):pnpm-workspace.yaml 逐包声明了哪些依赖允许执行构建脚本(如 esbuild: true、electron: true、canvas: true),哪些显式禁用(better-sqlite3: false),未列出的包默认视为未审核而被阻塞。这比一刀切的 --ignore-scripts 既更快(只跑必要的脚本)也更安全(未审核包一律不跑)。
另外注意根包自身的 postinstall 不受 allowBuilds 管辖:AIRI 根 package.json 声明了 "postinstall": "pnpm exec simple-git-hooks && pnpm run build:packages",因此 CI 中 pnpm install 结束时实际上已经触发过一轮包构建——这也是为什么工作流在 install 之后仍会显式再跑 pnpm run build:packages 来保证构建状态可控。
Corepack 集成:把 pnpm 版本写进仓库
文档推荐的版本锁定方式是把 packageManager 写进 package.json,任何环境只需 corepack enable 即可获得正确版本:
// package.json
{
"packageManager": "pnpm@10.0.0"
}
# GitHub Actions
- run: corepack enable
- run: pnpm install --frozen-lockfile
AIRI 的根 package.json 正是这样写的:"packageManager": "pnpm@11.24.0",精确到补丁版本。文档还补充了三个进阶选项:
- 范围式锁定:使用
devEngines.packageManager(可写版本范围),解析出的具体版本会记录在锁文件中; - 外部版本管理豁免:若 pnpm 版本由 asdf / mise / Volta 等外部工具管理,可在
pnpm-workspace.yaml中设置pmOnFail: ignore跳过 pin 检查; - 一次性切换:
pnpm with current <cmd>可在不改变全局状态的前提下用当前锁定版本执行单条命令。
Monorepo CI 策略:只构建变化的部分
AIRI 的 workspace 范围由 pnpm-workspace.yaml 的 packages 字段声明(packages/**、apps/**、plugins/**、server/**、engines/** 等九个通配,并排除 !**/dist/**),全仓库共享一个锁文件。依赖图上的构建编排则交给 Turborepo:turbo.json 中 build 任务声明了 dependsOn: ["^build"](先构建上游依赖包)与 outputs: ["dist/**"](构建产物参与缓存)。
策略一:基于过滤的增量构建
用 ...[origin/main] 过滤语法选出相对基准分支有变化的包,再交给 turbo 按依赖图传播:
- name: Build changed packages
run: |
pnpm --filter "...[origin/main]" build
这与 AIRI 根 package.json 中 turbo run build -F="./packages/*" -F="./apps/*" -F="./server/**" 的全量入口互补——PR 场景走增量过滤,发布/夜间构建走全量。
策略二:按包拆分的并行 job
文档给出了“先检测变化、再矩阵展开”的完整 GitHub Actions 方案:
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.changes.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: changes
run: |
echo "packages=$(pnpm --filter '...[origin/main]' list --json | jq -c '[.[].name]')" >> $GITHUB_OUTPUT
test:
needs: detect-changes
if: needs.detect-changes.outputs.packages != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install --frozen-lockfile
- run: pnpm --filter ${{ matrix.package }} test
关键细节:fetch-depth: 0 是必需的(过滤基线需要完整 git 历史);detect-changes 的输出经 fromJson 展开为矩阵维度;if 条件保证无变化时测试 job 整体跳过。AIRI 的 check-provenance job(ci.yml)则展示了同一仓库中另一类“检测先行”job——用 danielroe/provenance-action 校验锁文件变更的溯源完整性,与文档中“store 缓存的信任域”提醒同属供应链安全范畴。
最佳实践总结
文档的六条核心结论,以及它们在 AIRI 仓库中的对应落点:
- CI 使用
pnpm ci或--frozen-lockfile→ ci.yml 每个 job 显式执行pnpm install --frozen-lockfile; - 缓存 pnpm store(仅限可信 job 之间) →
pnpm/setup@v2的cache: true+.turbo缓存双层叠加; - CI 的 pnpm 大版本必须与锁文件生成方一致 → 根 package.json 锁定
pnpm@11.24.0,pnpm-lock.yaml 为lockfileVersion: '9.0',本地 .tool-versions 固定 Node 26.7.0 与 CI runtime 对齐; - 在 package.json 中锁定
packageManager(或devEngines.packageManager) → 已按pnpm@11.24.0精确锁定; - Monorepo 用
--filter只构建变化的部分 → turbo.json 的dependsOn/outputs声明 + 根脚本中大量pnpm -F用法; - Docker 多阶段构建,PATH 指向
$PNPM_HOME/bin→ apps/stage-web/Dockerfile 的 build/production 双阶段 + BuildKit store 缓存挂载。
配套的仓库证据还包括:pnpm-workspace.yaml 中的 overrides(如 axios: npm:feaxios@^0.0.23)、patches/ 目录下的 patchedDependencies、minimumReleaseAge: 4320 的发布年龄限制,这些配置共同构成了“frozen-lockfile 之下锁文件仍然可信”的完整链条——CI 实践不是孤立的 YAML 技巧,而是与仓库级依赖治理策略共同生效的一套体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00