Serverless Framework 贡献者工程手册:Monorepo 架构、测试矩阵与发布流水线全解
本文基于 Serverless Framework 仓库的 AGENTS.md 编写,系统拆解该 CLI 框架 monorepo 的包结构与 Runner 路由架构、本地开发规范、依赖与锁定文件约束、分层测试策略、esbuild 发布打包链路,以及 CI 流水线与版本发布机制。读完后,你将知道如何在本地跑通这个以 Node.js 24 + npm 12(ES Modules)开发、面向 Node.js >= 18 的 CLI,以及一次改动从 lint、测试到 canary/正式发布的完整旅程。
一、Monorepo 结构总览
Serverless Framework 是一个用于将应用部署到 AWS Lambda 及其他托管云服务的命令行工具,由 YAML 配置(serverless.yml)驱动。仓库采用 npm workspaces 组织 monorepo,顶层结构如下:
├── packages/
│ ├── sf-core/ # CLI shell: entry point, command router, runners
│ ├── serverless/ # Traditional framework: AWS provider, plugins, config schema
│ ├── engine/ # Shared AWS client wrappers used across the CLI
│ ├── mcp/ # MCP server for AI IDEs
│ ├── util/ # Shared utilities
│ ├── standards/ # ESLint and Prettier configs
│ ├── framework-dist/ # Bundled distribution package (excluded from npm workspaces)
│ └── sf-core-installer/ # Published to npm as "serverless" (excluded from npm workspaces)
├── binary-installer/ # Go-based binary installer
├── docs/sf/ # User-facing documentation (published to serverless.com)
├── skills/ # Agent Skills shipped inside the CLI (CI-linted)
└── release-scripts/ # Release automation
根目录 package.json 中的 workspaces 配置印证了这一点:packages/* 全部纳入,但明确排除了 packages/framework-dist 与 packages/sf-core-installer(见 workspaces 字段中的 !packages/framework-dist、!packages/sf-core-installer),并额外纳入 release-scripts。两个被排除的包之所以独立,是因为:
packages/sf-core-installer才是 npm 用户实际npm install到的serverless包,它拥有自己独立的overrides、自己发布时的npm-shrinkwrap.json和独立的.npmrc—— 根目录层面的依赖修复永远传不到它那里;packages/framework-dist在 git 中是一个空壳,内容在构建时才生成,其发布产物直接决定终端用户安装到的内容。
架构要点:CLI Shell 与 Runner 路由
从源码结构看,packages/sf-core 是 CLI 外壳:
- 入口 bin/sf-core.js 用 yargs 解析命令行参数(
scriptName('serverless'),强制--param解析为数组),随后调用ServerlessCore.run(); - 核心调度器 src/lib/router.js 维护
const runners = [ComposeRunner, CfnRunner, TraditionalRunner],按配置文件的名称前缀与shouldRun判定把命令分发给对应 Runner,并在执行前后完成状态保存、遥测事件与元数据落盘; - 该包同时承载认证(
src/lib/auth/)、变量解析器(src/lib/resolvers/)、可观测性与 Agent Skills 逻辑(src/lib/agent-skills/)。
而 packages/serverless 是绝大多数改动落地的地方:AWS provider 实现、全部插件(lib/plugins/aws/、lib/plugins/esbuild/ 等),以及 serverless.yml 配置模式。两个关键文件的分工值得注意:
- lib/config-schema.js 定义
serverless.yml的基础 JSON Schema(含provider、functions、build、frameworkVersion等顶层属性),各插件在此基础上扩展自己的配置面; - lib/classes/plugin-manager.js 是原生/内置插件的权威注册表 —— 该文件开头连续 import 了
pluginDeploy、pluginAwsProvider、pluginAwsPackageCompileEventsApiGateway等数十个插件并聚合为internalPlugins数组;注意lib/plugins/index.js并不是完整列表,判断“框架内置了哪些插件”应以 plugin-manager 为准。
二、本地开发环境搭建
开发统一使用 Node.js 24 + npm 12(ES Modules),但发布的 CLI 支持 Node.js >= 18。根 package.json 通过 packageManager: "npm@12.0.1" 与 devEngines(node ^24.15.0)声明了这一基线:
# 安装依赖(npm ci 永不改写 lockfile;优先于 npm install)
npm ci
# 在测试项目上以本地源码方式运行框架
cd /path/to/your/test-project
node /path/to/serverless/packages/sf-core/bin/sf-core.js deploy
直接 node packages/sf-core/bin/sf-core.js <command> 跑本地源码的方式,正是入口文件与 Runner 路由机制的直接消费场景:命令会在 serverless.yml 所在目录被 findRunner 扫描匹配后进入 TraditionalRunner。
三、代码风格与 Lint 流程
格式规则
仓库通过 Prettier 强制执行以下风格(配置见 prettier.config.js 与共享标准 packages/standards/src/prettier.js):
- 不使用分号(Prettier 会移除)
- 字符串使用单引号
- 2 空格缩进
- LF 换行符
- 一律使用 ES Modules(
import/export),不用require() - 优先原生 JavaScript 而非 lodash;异步代码使用 async/await
- 新增的示例、fixture 与代码片段应使用当前厂商推荐的现代写法(ESM
.mjshandler、最新 runtime、AWS SDK v3)——与仓库中旧内容的风格不一致不构成保留遗留写法的理由
Lint 命令
npm run prettier # 检查格式
npm run prettier:fix # 修复格式
npm run lint # 运行 ESLint
npm run lint:fix # 修复 lint 问题
容易踩坑的细节(Gotchas)
这些细节都有对应的仓库证据:
- ESLint 只检查显式路径 glob —— 配置集中在根目录 eslint.config.js。新建包或顶层源码目录后,如果不把路径加进该文件的 glob 列表,它会被静默地完全不 lint。
- 共享 ESLint 配置禁用了
no-unused-vars等规则 —— packages/standards/src/eslint.js 中关闭了未使用变量检查,因此 lint 抓不到未使用的变量或 import,需要靠代码评审自行把关。 - pre-commit 钩子只做了格式化的半自动化 —— .husky/pre-commit 仅一行
npx lint-staged --concurrent true,根 package.json 的lint-staged配置对*.{js,ts,tsx,jsx}执行prettier -w。也就是说提交时只对暂存的 JS/TS 文件跑 Prettier,push 前仍须手动跑 lint。 - ES Modules 规则的一个例外:
packages/sf-core-installer是 CommonJS(因为它是发布到 npm 的安装器壳)。 .env文件刻意不加入 gitignore —— 测试 fixture 依赖它们(.gitignore中有一条被注释掉的**/.env及说明),所以绝不要把真实凭据写进任何.env文件。
四、依赖管理约束
Node 18 运行时兼容是硬约束
packages/serverless/package.json 声明 engines.node: ">=18.0",即发布产物必须继续支持 Node 18,即便开发环境已用 Node 24。为此,放弃 Node 18 支持的主版本升级会被 .github/dependabot.yml 的 ignore 列表拦截 —— 该文件以注释明确列出每条拦截原因(如 ora v9 drops support for Node.js 18、open v11 drops support for Node.js 18、joi v18 drops support for Node.js 18 等)。升级依赖前应先查看该文件;仅开发用的依赖(devDependencies)则不受此约束。
lockfile 与版本解析规则
package-lock.json只能用 npm 12 写:npm <= 11 会在 workspaces 仓库中静默丢弃根级overrides(npm/cli#4834 问题);普通安装一律用npm ci。- 根 .npmrc 仅一行
min-release-age=3:发布不到 3 天的 npm 版本不会被解析,除非显式传--min-release-age=0。这是一道针对上游“刚发布就有问题”版本的缓冲闸。
五、测试矩阵:单元测试、集成测试与无头 CLI 测试
单元测试(本地运行)
npm run test:unit -w @serverlessinc/sf-core # jest 跑 packages/sf-core/tests/unit/
npm run test:unit -w @serverless/framework # jest 跑 packages/serverless/test/unit/
npm test # 同时跑两个 unit 套件
两个易错点:
- 目录命名不一致:sf-core 用
tests/,serverless 用test/,新增测试时容易放错位置; - 必须通过 npm script 调用 Jest,不能裸跑
jest:脚本中显式设置了--experimental-vm-modules(见 packages/sf-core/package.json 的test:unit脚本:NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" ... jest "tests/unit/.*"),这是 ESM 支持的前提。
集成测试(真实 AWS)
集成测试会部署真实的 AWS 栈。它们在 CI 中对非 draft PR 运行,本地运行需要 AWS 凭据及 TESTING.md 中描述的前置资源:
npm test -w @serverlessinc/sf-core # 集成套件(不含 domains 与 mcp)
npm run test:<suite> -w @serverlessinc/sf-core # 指定套件
可用的定向套件(对应 packages/sf-core/package.json 中的 test:* 脚本)包括:simple:nodejs、simple:python、simple:compose、simple:dashboard、simple:resolvers、resolvers、esbuild、sam、sandboxes、state、deployment-bucket、license-key、domains、mcp、compose:dev、compose:subset。优先选择覆盖你所改动区域的定向套件,而不是全量集成套件。
两个被排除在 npm test 之外的套件及原因:
domains:没有任何 CI workflow 跑它,仅在显式调用时执行;mcp:由路径过滤的CI: MCP Serversworkflow(.github/workflows/ci-mcp.yml)负责;其中只有mcp-auth.test.js需要 TESTING.md 中的 Cognito 前置资源——缺失时该套件跳过、其余照常运行。
注意:tests/integration/ 下的任何新目录会自动加入 npm test,因此昂贵的新套件必须像上面两个一样显式排除。
集成测试的约定(conventions):
- 每个套件为
<name>.test.js配一个同级fixture/目录承载被测服务 —— 一个测试文件独占一个 fixture 目录,因为 jest 并行跑测试文件且不设 worker 上限,两个文件共用一个目录会在.serverless/、node_modules/与暂存产物上相互竞争; - 复用 packages/sf-core/tests/utils/ 中的共享工具(
runSfCore.js、testUtils.js,例如fetchWithRetry用于处理最终一致性端点),不要自己手写 CLI 调用; - fixture 不得声明遗留打包插件(
serverless-esbuild、serverless-webpack、serverless-plugin-typescript、serverless-bundle)——它们会抛PLUGIN_TYPESCRIPT_CONFLICT,除非设置了build.esbuild: false; - 新集成测试必须自清理(部署 → 执行 → 拆除,失败时也要拆除)、使用唯一栈名保证并行安全、fixture 与断言中不得出现密钥或账号 ID。
Dev 模式测试需要预先构建被 gitignore 的 shim:
npm run build:devmode:shim -w @serverless/framework
对应 packages/serverless/package.json 中的脚本:esbuild lib/plugins/aws/dev/shim.js --bundle --platform=node --minify --outfile=lib/plugins/aws/dev/shim.min.js(CI 会作为独立步骤执行)。
其他套件
npm test -w @serverless/mcp # mcp 测试(任何 CI workflow 都不跑)
npm test -w @serverless/engine # engine 单元测试
npm run test:python -w @serverlessinc/sf-core # python 插件测试
npm run test:build -w @serverlessinc/sf-core # 打包冒烟 + skills 打包校验(不在 CI 中)
cd binary-installer && go test ./... && make build-prod # Go 安装器
两个覆盖盲区需要注意:
- python CI job 是路径过滤的(仅当 python 插件路径变更时运行,见 ci-python.yml)——失败可能长期潜伏在 main 上,直到某个 PR 触碰这些路径;
packages/util完全没有测试:它的改动只能通过消费方的套件被间接验证。
无头(Headless)方式测试 CLI 行为
永远不要用 pty 驱动 CLI(script、pty.spawn):pty 与真实终端无法区分,会导致 spinner 动画和交互式提示弹出。请使用纯管道(pipe)——交互判定门槛通常是 stdin.isTTY && stdout.isTTY && !CI,例如:
echo "your-input" | node packages/sf-core/bin/sf-core.js some-command
六、发布与打包链路:esbuild 单文件、非 JS 资产与 Go 安装器
esbuild 打包与非 JS 资产注册
发布的 CLI 用 esbuild 打包成单文件(构建配置:packages/sf-core/esbuild.js,入口 ./bin/sf-core.js,产物 ../framework-dist/dist/sf-core.js)。标准 import/export 模块会被自动打包,但非 JS 资产以及一切通过 __dirname 相对路径加载的资源(JSON、.py 文件、模板、被 spawn 的脚本)必须在 packages/sf-core/scripts/prepareDistributionTarballs.js 中显式注册 —— 否则代码在源码下运行正常、发布后却会断。
另外,构建时还会把 skills/ 目录的 Agent Skills 清单读入并 define 成 __SF_SKILLS_MANIFEST__ 常量内嵌进 bundle(源码运行则实时读取 skills/ 目录),版本号 __SF_CORE_VERSION__ 同理,IS_CANARY=true 时以 git 短 SHA 作为版本。
一个明确的反模式警告来自 esbuild.js 源码注释:esbuild 必须保留在 external 列表中。如果把它一并打包,其内部 transformSync 所 spawn 的 Worker 中 __filename 会指向整个 bundle,导致 Worker 重新执行整个 CLI(esbuild 官方文档与上游 guard 都明确警告“esbuild JavaScript API cannot be bundled”,参见上游 issue #13574)。
framework-dist 与 Go 启动器的分发现场
packages/framework-dist 在 git 中是空壳,构建时生成内容。npm 上的 serverless 包(即 sf-core-installer)只负责下载 Go 启动器二进制;启动器按项目解析 frameworkVersion,把由 framework-dist 构建的发布 tarball 下载到 ~/.serverless/releases/<version> 并在其中执行 npm install —— 发布 tarball 的内容直接成为终端用户的安装内容。启动器行为(版本解析、缓存、24 小时更新节流)详见 binary-installer/README.md。
七、Agent Skills(skills/)的版本契约
CLI 内置了 skills/ 目录下的 Agent Skills(当前含 skills/manifest.json 声明的条目,完整契约见 skills/README.md)。其变更规则非常严格:
- 任何对 skill 内容的改动,必须 bump 其
metadata.version并重新生成清单,否则 CI 失败(CI 的 Lint 阶段会执行node packages/sf-core/scripts/lint-skills.js,见 ci-framework.yml):
node packages/sf-core/scripts/lint-skills.js --update
- 提交时必须把
skills/manifest.json一并带上; - 用户安装中的辅助文件永远不会被删除 —— 因此要新增或重命名文件,而不是复用/改用途一个既有文件名。
八、CI 流水线与发布工作流
CI 运行在指向 main 的 PR 上,使用 Node.js 24.x(见 ci-framework.yml 的 node-version: 24.x):
| Workflow | 内容 | 触发条件 |
|---|---|---|
| CI: Framework CLI | Lint(ESLint + Prettier + Agent Skills)、Test: Engine、Test: Framework(unit + integration) | 纯文档改动整体跳过(paths-ignore: docs/**),draft PR 也跳过 |
| CI: Binary Installer | Go 构建与测试(ci-binary-installer.yml) | 仅当 binary-installer/** 变更 |
| CI: Python Requirements | python 插件测试(ci-python.yml) | 路径过滤 |
| CI: MCP Servers | 实时 mcp 套件(ci-mcp.yml) |
路径过滤到 MCP 插件、api-gateway 与 esbuild 接缝、MCP 测试/fixture |
由于 GitHub Actions 没有 job 级路径过滤,python 与 MCP 套件各自独立成 workflow 文件。
发布工作流(release-*.yml,如 release-framework.yml)只在 push 到 main 或手动 dispatch 时运行,从不被 PR CI 验证 —— 修改它们必须格外仔细。其中 release-framework.yml 还带有路径过滤(packages/{sf-core,serverless,engine,mcp}/**):其他位置(如 packages/util)的改动不会单独触发 release 构建。
九、PR 规范与版本发布流程
- PR 一律 squash merge,PR 标题即 commit message。使用约定式格式
type(scope): description—— 祈使语气、句尾无句号、约 72 字符上限。类型:feat、fix、perf、docs、refactor、test、ci、chore。 - semver 映射:任何
feat:触发 minor 发布;仅fix:/chore:才是 patch。完整的语义化解释见 VERSIONING.md —— 值得注意的是,CLI 输出结构的变更与生成的 CloudFormation 的变更都被视为 breaking。 - 非平凡的功能与修复应先有 open issue —— 见 CONTRIBUTING.md。
- 面向用户的变更(行为、配置面、CLI 输出)应在同一个 PR 中更新
docs/sf/文档。 - 根 README.md 在发布时被复制进发布的 npm 包 —— 对它的编辑本身就是面向用户的内容。
- canary 通道:每次 push 到
main且触碰 release 相关包时,自动发布按 git 短 SHA 命名的 canary 构建(用户在serverless.yml中以frameworkVersion: canary选择该通道)。合入 main 的代码数分钟内即出现在 canary 通道上,因此 main 必须时刻保持可发布状态。 - 正式 release:需在
packages/sf-core-installer/package.json与packages/sf-core/package.json两处 bump 版本,PR 标题严格为chore: release x.x.x;合并后 CI 打 tagsf-core@x.y.z(可用这些 tag 对比两次发布间的内容)。npm 只是次级分发渠道,主渠道是 curl 安装器(install.serverless.com)。完整流水线见 RELEASE_PROCESS.md。
十、关键文件速查
| 文件 | 作用 |
|---|---|
| packages/sf-core/bin/sf-core.js | CLI 入口(yargs 解析 + ServerlessCore.run()) |
| packages/sf-core/src/lib/router.js | 命令调度器(Runner 选择、CLI schema 校验、状态/遥测收尾) |
| packages/serverless/lib/config-schema.js | 基础 serverless.yml 模式(各插件在其上扩展) |
| packages/serverless/lib/classes/plugin-manager.js | 原生/内置插件的权威注册表 |
| packages/sf-core/scripts/prepareDistributionTarballs.js | 发布用非打包资产注册表 |
| packages/standards/src/eslint.js / packages/standards/src/prettier.js | 共享 lint 与格式化配置 |
配套的流程文档入口:TESTING.md(集成测试前置资源)、VERSIONING.md(semver 细则)、RELEASE_PROCESS.md(发布流水线)、skills/README.md(Agent Skills 契约)。掌握以上结构、约束与流水线后,无论是改插件、加测试,还是排查“为什么我的改动没有触发发布”,都可以在这个仓库中找到明确依据。
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 StartedRust0627
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