Skip to content

Latest commit

 

History

History
130 lines (99 loc) · 16.6 KB

CONTRIBUTING.md

File metadata and controls

130 lines (99 loc) · 16.6 KB

参与开发

本文是 CF-Navs 的工程规则,适用于所有代码、文档和发布改动。与本机环境相关的私有配置(SSH 别名、本地验证目标、浏览器路径、代理端口等)不写在这里。

1. 分支模型

分支 角色 规则
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 / 分支删除,除非明确针对某次事故且已确认要覆盖哪些提交。

2. 一次改动一个主题

  • 一个 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,不要用关闭关键字。

3. 证据引用不写裸行号

计划、台账和提交信息里指向源码时:

  • 推荐:文件路径 + 符号名,例如 worker/routes/icon.ts 的 iconRoutes.get('/icon/:id')、shared/settings.ts 的 SETTINGS_KEYS。
  • 需要精确到行时用带 commit SHA 的 GitHub permalink,SHA 固定后行号不会腐烂。
  • 禁止只写 file.ts:123。一次重构就会让它指向无关代码,而读者无法察觉。

4. 验证分级

每个 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 的文件进 jsdom project,走 vmForks 池(同一 worker 复用已加载的 jsdom,每个文件仍在全新 VM 上下文里求值);其余进 node project,走默认 forks。全量从约 140 秒降到约 45~60 秒(双核机器;环境准备 58.6s → 3.7s),测试文件与用例数不变。注意:
    • 只认首行。docblock 写在第二行的 jsdom 文件仍会在 jsdom 里跑,但落进 node project 的 forks 池,失去提速。
    • vmForks 下 instanceof 跨 realm 会失真(node:sqlite 返回的对象、Node 内置模块产出的 Uint8Array 等来自宿主 realm;改成 vmForks 跑全部文件时曾有 4 个纯逻辑用例因此失败)。不需要 DOM 的纯逻辑测试不要加 jsdom docblock;需要 DOM 又要断言这类对象时,改用结构断言(ArrayBuffer.isView、toMatchObject),不要把文件挪回 forks 了事。
    • 只需要一个对象引用、不触碰 DOM API 的测试应留在 node project(例如 searchBoxVisibility 用 {} 代替元素)。
  • 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 会并入 DOM Element(append / remove 等签名冲突):测试里优先用 appendChild、把查询辅助函数的参数标为 Element 而非 ParentNode,确需具体子类型时经 unknown 收窄并注明原因。
  • Lint:npm run lint(ESLint 9 flat config eslint.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 复核该文件未被跟踪;一旦发现被跟踪就拒绝执行并要求轮换密码。

5. 任务状态放在哪里

只有一个地方记录某件事的状态,避免同一事实手写多份。

类型 状态源 说明
云端已有编号的缺陷与功能需求 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 状态只能通过实时查询确认,不得由本地提交或实现推断。

6. 发版流程

版本号用 v<major>.<minor>.<patch>,与 package.json 的 version 保持一致。

  1. 在 develop 上确认目标改动已达到对应验证级别,CHANGELOG.md 的 [Unreleased] 段落内容完整。
  2. 把 [Unreleased] 改成 ## v0.x.y — YYYY-MM-DD,在其上留一个新的空 [Unreleased]。
  3. 同步 package.json 的 version。
  4. 提交 chore(release): v0.x.y。
  5. 在 develop 的该提交上打 tag:git tag -a v0.x.y -m "v0.x.y",推送分支与 tag。
  6. 等待本仓库 develop 推送触发的 Cloudflare 自动构建和部署,核对目标构建后验证生产自定义域而不只是默认域名;不要再执行一次手动部署。未接入自动部署的独立实例才按部署指南使用 npm run deploy,并单独取得该操作授权。
  7. 执行该版本的 L3 清单,结果回写到 CHANGELOG.md 对应版本段或 docs/BACKLOG.md。
  8. L3 通过后,手动关闭该版本闭环的 Issue,并在评论里写明版本 tag。关闭前先 gh auth status 确认活动账号;写 Issue 需要单独授权。这一步只针对云端已有编号的条目;本地发起的条目没有 Issue,它们的闭环凭据是 CHANGELOG.md 对应版本段加该版本的 tag。
  9. (可选,需维护者主动要求)把该版本合并到 main 归档:git checkout main && git merge --no-ff v0.x.y -m "merge: archive v0.x.y into main"。
  10. 同步自托管线并发布镜像: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 会触发自动部署,应在推送前明确这一效果。

7. 提交前的安全边界

  • 不把真实生产域名、演示地址、私有端点、账号标识、Token、凭据写进代码、文档、测试、截图或提交信息。示例统一用占位域名。
  • git add 只用精确路径,禁止 git add .;提交前检查 git diff --cached --name-only、--stat、--check 和完整暂存差异。
  • 不提交本地验证配置、临时浏览器 profile、验证产物和本机专属的 agent 指令文件。
  • 浏览器验证默认使用专用临时 profile,只创建和关闭本次测试自己的标签页;禁止按进程名批量结束浏览器进程,禁止关闭使用者自己的浏览器。
  • 新增依赖优先选活跃维护的知名包;名称可疑或与常见包高度相似时先核对来源。