服务工程、交付与可观测性
保持依赖和任务图真实对应,连接检查、产物、环境、发布恢复以及人和 Agent 的开发反馈。
从工作区到可交付产物
pnpm workspace 连接应用和共享包,内部包通过声明的接口被消费。依赖版本与锁文件保存安装条件,构建脚本授权按实际包需求控制;packageManager 字段与 CI 安装工具相匹配。归档 pnpm 特定版本的选项不作为所有版本通用配置。
Turborepo 编排任务及缓存。dependsOn: ['^build'] 描述依赖包构建,不自动规定同包 lint、typecheck、test 必须先于 build。若需要先检查后构建,显式建立同包依赖或在 CI 顺序执行。输入、输出和环境变量决定缓存有效性,遗漏输入会复用不适用结果。
应用、库、生成契约和客户端各有产物。CI 确认存在、可解析和符合接口,发布再确认实际被交付的版本。格式、类型和构建不能替代运行环境与业务行为。
任务图可以显式连接同包检查。下面假设各包在 package.json 中定义了对应脚本;叶子包没有某项脚本时,需要另核对该包的实际检查覆盖。
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"lint": {},
"typecheck": { "dependsOn": ["^build"] },
"test": { "dependsOn": ["^build"], "outputs": [] },
"build": {
"dependsOn": ["^build", "lint", "typecheck", "test"],
"outputs": ["dist/**"],
"env": ["PUBLIC_ORIGIN"]
},
"dev": { "cache": false, "persistent": true }
}
}依赖包先 build 供类型或测试消费,同包 build 再等待自己的检查。构建产物为 dist 是例子的包约定;Next、静态导出或其他工具应声明真实输出和应排除的缓存,不直接复制。需要读环境的检查任务也在各自 env 声明。动态秘密或可变远程数据影响构建时,还需决定是否缓存和如何使输入可见。Turborepo 配置
本地反馈与公开流水线
Git hooks 提供提交时反馈,CI 提供目标提交检查。Conventional Commits 表达变更信息,Changesets 保存包版本意图;纯应用与发布 npm 包的版本目标不同,不要求每次正文或应用配置改变都建立包发布。
源码、锁文件、配置和环境共同进入构建基线。包安装的生命周期脚本、拉取请求来源、缓存写权限和 secrets 各需最小权限。CI 并发取消适合替代旧检查,正在产生外部效果的发布不能随意取消。
CI 可先冻结安装条件,再执行任务图。下列 GitHub Actions 片段在一次 job 内运行;Node 和 pnpm 版本由仓库文件给出,项目应维护与框架一致的支持范围。
name: code-check
on: [pull_request]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec turbo run build这里的 action 主版本用于说明接口,正式采用时核对支持与更新政策,必要时 pin 到审查过的提交;pnpm/action-setup 从 packageManager 读取版本。ubuntu-latest 会随平台演进,环境变化可能影响原生依赖与测试。此流水线未在本文启动,其 build 覆盖取决于前述任务图和各包脚本。
测试替身和数据怎样接线
纯规则用 Node/Vitest 等 runner,React 组件可用 Testing Library 与适合目标的 DOM 宿主;完整浏览器才能观察布局、焦点和真实媒体。网络替身如 MSW 接在运输边界,模块替身接在导入边界。handler 注册、未处理请求政策、每例 reset 和最后 close 都由测试生命周期管理。
Vitest 的最小配置可以把这些关系明确下来;下例只选择 Node 测试环境,组件项目按需要使用单独环境配置,coverage 阈值由真实目标决定:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
clearMocks: true,
restoreMocks: true,
},
});vi.mock 受模块装载和提升规则影响,静态导入可能早于用例里的赋值;需要不同装载状态时按 runner 接口安排动态 import 和 reset。不要靠在测试名称中写“集成”扩大替换后的证据范围。Vitest Mocking
Faker 提供字段数据,工厂提供关联与默认结构。可复核性同时依赖种子、调用次序、版本和相对日期参照,单独固定种子不能固定随“今天”变化的日期:
import { faker } from '@faker-js/faker';
export function sampleRecords() {
faker.seed(1234);
return Array.from({ length: 3 }, (_, index) => ({
id: `record-${index + 1}`,
title: faker.lorem.words(3),
createdAt: faker.date.recent({
refDate: '2026-01-01T00:00:00.000Z', days: 10,
}).toISOString(),
}));
}该示例固定身份及日期参照,随机标题只填充与断言无关的数据;失败时保存具体输入或版本及种子。共享单例并发使用仍会受交错影响,独立测试可创建独立 Faker 实例。Fishery 等工厂可以明确关联,schema 生成器只生成结构时仍需业务有效规则。Faker 可复现条件
Testcontainers 的真实依赖验证先启容器并等待就绪,取得库返回连接信息,用生产初始化路径和迁移建模式,再装入隔离数据,最后关闭池并停止容器。globalSetup 与测试 worker 不共享普通变量,通过 runner 的 provide/inject 或明确文件接口传递连接配置;写文件后尚未加载就不会自动生效。固定测试数据库身份与目标检查,避免清理脚本触到其他环境。本文没有启动容器或运行上述示例。
配置与部署
启动校验把环境字符串变成受控配置,必要目标缺失时拒绝接收流量。多阶段容器构建分开构建工具与运行产物,运行用户、写入目录和端口按真实环境声明。容器监听本地回环与监听所有接口具有不同可达性,开放后仍需网络授权边界。
静态导出只部署文件,动态请求、Server Actions、常驻 worker 等需要相应执行环境;平台适配器和运行时版本决定可用能力。长期连接与大文件还受执行时间、内存和请求限制。
发布保存可恢复产物与实际版本,部署后查询流量版本及关键结果。回滚代码没有自动回滚数据库、外部付款或新存储格式。迁移要有兼容顺序和数据恢复边界。
遥测怎样接回一次工作
结构化日志保存事件、主体范围、结果与关联 ID,避免秘密和过量原始内容。pino、winston 和框架 Logger 的传输与格式能力不同,按 sink、成本和资产选择,不沿用无可比负载的性能排行。
指标说明数量、单位、时间窗及分母,延迟分位与平均数提供不同信息。追踪通过 span 与上下文连接跨服务操作,采样会限制覆盖;OpenTelemetry 的 API、SDK、自动插桩和 exporter 各有工作,接入成功不能证明所有异步路径保留上下文。OpenTelemetry JS
Sentry 等错误系统需要按初始化顺序、过滤、sourcemap 和脱敏配置接线。业务拒绝、依赖失败与内部异常分别决定是否上报;告警对应明确行动和影响,零流量的零错误不构成健康证明。
日志可在一次服务请求中生成 child logger,使关联信息跟随局部调用,而不要求每层重拼字符串。下例只展示受控字段,实际 HTTP 中间件还需校验外部关联 ID 或生成服务器 ID。
import pino from 'pino';
import { randomUUID } from 'node:crypto';
const log = pino({ redact: ['req.headers.authorization', 'password', 'token'] });
export async function runOperation<T>(operation: (requestLog: pino.Logger) => Promise<T>) {
const requestLog = log.child({ requestId: randomUUID() });
const started = performance.now();
try {
const result = await operation(requestLog);
requestLog.info({ elapsedMs: performance.now() - started }, 'operation completed');
return result;
} catch (error) {
requestLog.error({ elapsedMs: performance.now() - started }, 'operation failed');
throw error;
}
}调用者把同一 requestLog 交给业务层,子日志再加对象身份。redact 只匹配声明路径,秘密埋在其他嵌套字段、URL 或 Error 中仍需控制,不能把任意原始请求完整输出。连接到日志采集端时核对批处理、背压、停机刷新和保留。此例没有建立新遥测服务,追踪上下文还需单独接 OpenTelemetry SDK 与 exporter。Pino child logger
用 Agent 开发服务
规格描述目标行为、约束、禁止变动范围和完成判断;计划说明拟改变的对象及验证;diff 提供实际变化;检查与环境观察提供结果。模型输出进入同一软件责任链,主责任者核对接口、证据和整合。
上下文按任务取得并更新,陈旧摘要、工具输出和外部文档保持来源身份。压缩与子任务可以减轻单窗口材料负担,也可能丢掉约束,整合时重新接到原始事实。固定“可审行数”、角色工时比例和指令条数没有在本页成为统一门槛。
工具权限在模型外强制,代码编辑、实际运行和外部发布分别有边界。概率性代码生成可以通过确定的程序规则检查;AI 功能质量还需要其任务评测。测试通过范围、真实依赖、生产观察和用户效果分开报告,不把产码量或自我感受当成完成交付。
最后更新于