本文是 CF-Navs 的工程规则,适用于所有代码、文档和发布改动。与本机环境相关的私有配置(SSH 别名、本地验证目标、浏览器路径、代理端口等)不写在这里。
| 分支 | 角色 | 规则 |
|---|---|---|
develop |
集成分支、默认工作分支,也是发版与部署来源 | 所有开发在这里落地;提交前必须通过 L0 验证;版本 tag 打在这里的对应提交上 |
main |
已审阅归档快照 | 只接受来自 develop 的合并,禁止直接提交、禁止在 main 上改文件;只在维护者明确要求时才合并,不作为部署来源 |
docker |
自托管(Node + Docker)独立版本线 | 只接受来自 develop 发版 tag 的合并与 docker 专属提交;永不合并回 develop/main;推送触发 CI 发布 GHCR 镜像 |
- 开始任何改动前确认当前分支是
develop。 - 部署来自
develop,因此main不代表线上代码;线上状态以实际部署的构建、对应提交及行为验证为准,版本 tag 只标识正式发行版本。 - 合并
develop→main是一个独立的归档动作,需要维护者主动要求;不要为了发版而自动合并。 - 因为
main是默认分支而部署走develop,推送到develop的关闭关键字不会关闭 Issue。Issue 需要在部署验证通过后手动关闭,并在评论里引用对应的版本 tag。 main上出现过只存在于 main 的内容(如 Issue 模板),这类单向缺失会让develop不再是main的超集,后续合并必然冲突。发现 main 独有内容时,先回流到develop,再继续开发。docker的应用层问题一律回develop修复,随发版同步进入docker;docker只承载 Node/Docker 运行时与自托管部署相关改动。同步锚点是版本 tag(见第 6 节第 10 步),不追踪未发版的develop尖端。展开说明见docs/reference/BRANCHING_AND_RELEASE.md。- 禁止
git push --force/--force-with-lease/reset --hard/ 分支删除,除非明确针对某次事故且已确认要覆盖哪些提交。
-
一个 commit 只闭环一个编号(一个 Issue,或本地 backlog 的一条)。文档同步、测试、变更记录属于该改动的一部分,放在同一个 commit;不同编号不要合并进同一个 commit。
-
把多轮工作打包进一个 commit 会让回滚、追溯和复核都失效,也无法回答"这个修复上线了吗"。
-
做完一个编号就立刻提交,不要攒着。 这是硬要求,不是建议:
docs/reference/GITHUB_ISSUES_REQUIREMENTS.md、docs/plans/PROBLEM_HANDLING_TASK_LIST.md、docs/BACKLOG.md、CHANGELOG.md这几份文件会被多个编号同时改,一旦累积成一堆再想按编号拆开,就只能做行级/章节级的索引手术——零上下文补丁在这些文件里会定位错行(---、空行这类锚点重复出现),把表格行插到表格外,甚至重复插入同一行。攒着改的代价远高于多提交几次。 -
一轮里做多个编号时,顺序是「改第一个编号 → 跑该级别验证 → 提交 → 再改第二个」。中途需要临时保存进度时用未提交的工作区,不要把第二个编号的改动叠到第一个之上。
-
提交信息格式(沿用仓库既有风格):
<type>: <英文单行主题,不超过 70 字符> <中文正文:为什么改、核对推翻了什么假设、关键实现取舍、已知降级> Verified: L0,L1 refs #<编号> -
type取fix/feat/docs/chore/refactor/test/perf。 -
Issue 关联:Bug 用
fixes #N,功能用closes #N,仅引用用refs #N。关闭关键字只有在提交进入默认分支main后才生效;由于部署走develop、main只在维护者要求时才合并,日常提交请一律用refs #N,把实际关闭留到部署验证之后手动执行,不要据此宣称已关闭。 -
一个改动同时牵涉多个 Issue 且没有单一主责 Issue 时,用
refs,不要用关闭关键字。
计划、台账和提交信息里指向源码时:
- 推荐:文件路径 + 符号名,例如
worker/routes/icon.ts的iconRoutes.get('/icon/:id')、shared/settings.ts的SETTINGS_KEYS。 - 需要精确到行时用带 commit SHA 的 GitHub permalink,SHA 固定后行号不会腐烂。
- 禁止只写
file.ts:123。一次重构就会让它指向无关代码,而读者无法察觉。
每个 commit 在信息尾部声明达到的最高级别(Verified: L0 / Verified: L0,L1 …)。未达到的级别不是"可以忽略",而是自动进入发版前清单。
开发工具要求 Node.js 22.12+(22.x)或 24+(见 package.json 的 engines.node);CI 固定使用 Node.js 24 LTS。Vite 7 / Svelte 插件要求 22.12+,Vitest 4 不支持 Node.js 23;不要将 CI 降回 Node.js 20,当前 Wrangler / Miniflare 也已要求 Node.js 22。L0 通过不能代替 L1 启动验证。
| 级别 | 内容 | 命令 | 前置条件 |
|---|---|---|---|
| L0 静态 | lint、类型、单元测试、构建、空白字符 | npm run lint、npm run type-check、npm test、npm run build、git diff --check |
无。CI 已覆盖前四项 |
| L1 API 端到端 | 鉴权、CRUD、排序、设置、导入导出、登出失效 | npm run smoke |
需先 npm run build(ASSETS 绑定要 ./dist)。脚本自己起隔离实例、造临时 D1 与一次性凭据并拆除,CI 已覆盖 |
| L2 浏览器 | 渲染、交互、后台流程 | npm run regression:chrome;组件测试随 npm test 一起跑 |
回归套件需可达目标地址与管理员凭据;组件测试无前置 |
| L3 真机 / 部署后 | 首访/二访、Service Worker 与预缓存、离线、匿名边界、弹窗尺寸、登出撤销、三档截图 | npm run accept:prod + npm run perf:audit |
需已部署实例。脚本只读,不产生任何服务端写入,可无条件对生产跑 |
| L4 真机独有 | iOS Safari 输入放大、100dvh 与虚拟键盘、剪贴板 transient activation、连续两次部署才出现的新版本提示 |
人工 | 自动化拿不到有效证据的那部分,逐项见 部署后验收 §5 |
按改动范围决定必须达到哪一级:
| 改动范围 | 最低要求 |
|---|---|
| 纯文档 | L0(git diff --check + 相关链接可达) |
| 纯函数 / 类型 / 共享定义 | L0 |
worker/routes/、worker/lib/db/、schema.sql |
L0 + L1 |
| Svelte 组件、样式、交互 | L0 + L2 |
| 缓存策略、Service Worker、图标链路、性能相关 | L0 + L2,推送后跑 L3,并把 L4 项登记到发版清单 |
补充纪律:
- 要测行为先抽纯函数再单测。组件的 DOM 行为(焦点、ARIA、键盘、禁用联动)用
@testing-library/svelte+ jsdom 写组件测试,文件首行加// @vitest-environment jsdom,不改全局测试环境。 - 计算样式、
dvh与安全区、虚拟键盘、剪贴板用户手势、iOS 输入放大 jsdom 证明不了。计算样式与视口数值现在由 L3 的 CDP 脚本覆盖;真机独有的那几项进 L4。不要用源码文本断言假装覆盖了它们。 - 源码文本断言(
readFileSync+toContain)只用于"接线是否存在",不用于证明行为。 - 测试分两个 Vitest project(
vite.config.ts):首行是// @vitest-environment jsdom的文件进jsdomproject,走vmForks池(同一 worker 复用已加载的 jsdom,每个文件仍在全新 VM 上下文里求值);其余进nodeproject,走默认forks。全量从约 140 秒降到约 45~60 秒(双核机器;环境准备 58.6s → 3.7s),测试文件与用例数不变。注意:- 只认首行。docblock 写在第二行的 jsdom 文件仍会在 jsdom 里跑,但落进
nodeproject 的forks池,失去提速。 vmForks下instanceof跨 realm 会失真(node:sqlite返回的对象、Node 内置模块产出的Uint8Array等来自宿主 realm;改成vmForks跑全部文件时曾有 4 个纯逻辑用例因此失败)。不需要 DOM 的纯逻辑测试不要加 jsdom docblock;需要 DOM 又要断言这类对象时,改用结构断言(ArrayBuffer.isView、toMatchObject),不要把文件挪回forks了事。- 只需要一个对象引用、不触碰 DOM API 的测试应留在
nodeproject(例如searchBoxVisibility用{}代替元素)。
- 只认首行。docblock 写在第二行的 jsdom 文件仍会在 jsdom 里跑,但落进
tests/已纳入类型检查:npm run type-check末尾串联npm run type-check:tests(tsc --noEmit -p tsconfig.tests.json),CI 经type-check一并执行。tsconfig.tests.json继承主配置的严格设置,额外加载@types/node(与 CI 的 Node 24 对齐);测试导入的scripts/lib/*.mjs只参与类型推断(checkJs: false),scripts/本身仍不在检查范围,若要纳入需另开主题逐文件补 JSDoc。测试与@cloudflare/workers-types同处一个程序,后者 HTMLRewriter 的Element会并入 DOMElement(append/remove等签名冲突):测试里优先用appendChild、把查询辅助函数的参数标为Element而非ParentNode,确需具体子类型时经unknown收窄并注明原因。- Lint:
npm run lint(ESLint 9 flat configeslint.config.js,--max-warnings 0),覆盖worker/、src/(含.svelte)、shared/、tests/、scripts/(Node 全局)与public/sw.js;浏览器扩展与_archive/不在范围内。规则只取@eslint/js/typescript-eslint(非 type-aware,类型由 type-check 负责)/eslint-plugin-svelte的 recommended,关闭项及原因写在配置注释里;不含任何格式化规则,仓库不使用 Prettier,缩进、行尾等基础约定见.editorconfig,不要为风格做整文件重排。未使用的变量 / 形参 / catch 变量与 tsconfig 同一约定:刻意保留的用_前缀。确需豁免时用// eslint-disable-next-line <规则> -- <原因>逐行注明,不要整文件关闭。 - 组件测试依赖
vite.config.ts在 test 模式下的resolve.conditions: ['browser']。Svelte 5 的exports["."]在browser条件下提供src/index-client.js,默认落到src/index-server.js,其中onMount是空实现;选错入口会让组件注册的window/document监听器静默不存在。不要删掉这项配置。 - 断言「观察不到某个行为」时,先确认不是运行时解析或缺失的浏览器 API(
scrollIntoView、matchMedia、scrollTo在 jsdom 里都需要自己补),再下「jsdom 做不到」的结论。把环境问题写成不可迁移结论会留下错误的判断记录。 - 新写的组件测试要做一次反向对照:改坏被测行为,确认对应用例精确失败。若两道防线互为兜底(去掉任一条都不红),在文件里注明这是 defense-in-depth,不要因此删掉断言。
- 部署由推送触发:
develop收到提交后 Cloudflare 自动构建更新站点,没有手动 deploy 步骤。因此「推上去」等于「改线上」,L0 未过的改动不要推。 - 判断线上是否已是新版本,优先验证改动本身的可观察结果(例如加了一条 CSS 预留就去线上量那个元素的计算样式)——它同时证明「部署了」和「部署对了」。构建产物哈希只能说明有新部署:Cloudflare 在自己的环境重新构建,产物哈希与本机
npm run build的结果不必相同,拿本地dist的哈希去等线上出现同一个值会一直等不到。/api/data/version不能用于此,它返回的是数据版本号。 - 生产上的验证按副作用分层:只读探针(
accept:prod)随时可跑;改设置再还原属 Tier 1,需逐次授权;replace导入与管理员密码轮换属 Tier 2,只在本地实例做。regression:chrome的密码轮换已默认关闭,需REGRESSION_ALLOW_PASSWORD_ROTATION=1显式开启。 - 验证脚本的目标域名与凭据只放在被 Git 忽略的
verify.local.json,或同名环境变量。脚本每次运行都用git ls-files复核该文件未被跟踪;一旦发现被跟踪就拒绝执行并要求轮换密码。
只有一个地方记录某件事的状态,避免同一事实手写多份。
| 类型 | 状态源 | 说明 |
|---|---|---|
| 云端已有编号的缺陷与功能需求 | GitHub Issue | open/closed 就是状态,label 就是分类,milestone 就是发版批次。一个 Issue 只聊一件事 |
| 本地发起的问题、改进与新增功能 | docs/BACKLOG.md |
不为它们新开 Issue。 本地开发中发现的缺陷、自己提出的改进、内部技术债、文档债、验证欠账、待裁定事项,全部记在这里,一行一条,含阻塞原因 |
| 安全问题 | 不开公开 Issue | 见 SECURITY.md;修复落地后再在变更记录中公开描述 |
- GitHub Issue 只承接云端已有的报告,不作为本地工作项的容器。本地条目即使后来变成用户可观察的行为改动,也不因此新开 Issue——成果进
CHANGELOG.md,随版本 tag 发布。只有当某个本地条目确实对应一个已存在的 Issue 时,才在 backlog 的「详情」列回指那个编号,并在该 Issue 的生命周期里闭环。反之也不要把内部技术债搬到公开 Issue。 docs/plans/下的文档是决策记录:说明当初为什么这么改、哪些约束必须继续遵守。它们不维护状态,写完就不再改勾选和进度表。状态一律看docs/BACKLOG.md或 Issue。- 云端 Issue 状态只能通过实时查询确认,不得由本地提交或实现推断。
版本号用 v<major>.<minor>.<patch>,与 package.json 的 version 保持一致。
- 在
develop上确认目标改动已达到对应验证级别,CHANGELOG.md的[Unreleased]段落内容完整。 - 把
[Unreleased]改成## v0.x.y — YYYY-MM-DD,在其上留一个新的空[Unreleased]。 - 同步
package.json的version。 - 提交
chore(release): v0.x.y。 - 在
develop的该提交上打 tag:git tag -a v0.x.y -m "v0.x.y",推送分支与 tag。 - 等待本仓库
develop推送触发的 Cloudflare 自动构建和部署,核对目标构建后验证生产自定义域而不只是默认域名;不要再执行一次手动部署。未接入自动部署的独立实例才按部署指南使用npm run deploy,并单独取得该操作授权。 - 执行该版本的 L3 清单,结果回写到
CHANGELOG.md对应版本段或docs/BACKLOG.md。 - L3 通过后,手动关闭该版本闭环的 Issue,并在评论里写明版本 tag。关闭前先
gh auth status确认活动账号;写 Issue 需要单独授权。这一步只针对云端已有编号的条目;本地发起的条目没有 Issue,它们的闭环凭据是CHANGELOG.md对应版本段加该版本的 tag。 - (可选,需维护者主动要求)把该版本合并到
main归档:git checkout main && git merge --no-ff v0.x.y -m "merge: archive v0.x.y into main"。 - 同步自托管线并发布镜像:
git checkout docker && git merge v0.x.y,处理接缝文件冲突并通过 docker 分支验证后推送docker(触发 CI 发布 GHCR 镜像),再打git tag -a docker-v0.x.y -m "docker-v0.x.y"并推送,产出同号版本镜像。docker永不反向合并。
版本 tag 与 git tag --contains <sha> 只能证明发行版本包含某提交,不能证明部署成功;修复是否上线仍须核对实际目标构建和可观察行为。打 tag、推送、手动部署、关闭 Issue、合并 main 分别按授权范围执行,不因一次「发版」指令自动扩大范围;本仓库推送到 develop 会触发自动部署,应在推送前明确这一效果。
- 不把真实生产域名、演示地址、私有端点、账号标识、Token、凭据写进代码、文档、测试、截图或提交信息。示例统一用占位域名。
git add只用精确路径,禁止git add .;提交前检查git diff --cached --name-only、--stat、--check和完整暂存差异。- 不提交本地验证配置、临时浏览器 profile、验证产物和本机专属的 agent 指令文件。
- 浏览器验证默认使用专用临时 profile,只创建和关闭本次测试自己的标签页;禁止按进程名批量结束浏览器进程,禁止关闭使用者自己的浏览器。
- 新增依赖优先选活跃维护的知名包;名称可疑或与常见包高度相似时先核对来源。