# 发布指引 本文件说明如何把一个版本发布出去:发布前要核什么、命令怎么写、发布后怎么确认它真的落地了。 流程本身定义在 [skills/dsh-dev-workflow/SKILL.md](../skills/dsh-dev-workflow/SKILL.md) 的 「Releases (SemVer + Keep a Changelog)」与「Withdrawing a Published Release」两节; 本文件是它在这个仓库里的可执行版本,包含本机的实测结果与踩过的坑。 ## 一次性准备 | 前置条件 | 检查方式 | | --- | --- | | 已登录 npm,且账号对 `@melosic` scope 有发布权 | `npm whoami --registry=https://registry.npmjs.org` | | `gh` 已登录(用于创建 GitHub Release) | `gh auth status` | | 工作区干净、`main` 已同步 | `git status --short`、`git log --oneline -1` | **`npm whoami` 报 `ENEEDAUTH` 时不要用 `--no-verify` 绕过去**——没有登录就是没有登录, 发布命令会以 `E404` 或 `403` 失败。先 `npm login`。 **镜像源不能发布。** 本机 `~/.npmrc` 的 `registry` 指向 `registry.npmmirror.com`(只读镜像), 它是同步源、不是发布端点。所有发布相关命令都必须显式带上 `--registry=https://registry.npmjs.org`,否则会拿到与真实 npm 不一致的结果: ```bash npm whoami --registry=https://registry.npmjs.org npm view @melosic/dsh-dev-workflow --registry=https://registry.npmjs.org npm audit --registry=https://registry.npmjs.org npm publish --registry=https://registry.npmjs.org ``` `npm audit` 在本仓库还有一层特别:仓库用 pnpm,没有 `package-lock.json`, 所以裸 `npm audit` 报 `ENOLOCK`,而 `pnpm audit` 走默认 registry 报 `ERR_PNPM_AUDIT_ENDPOINT_NOT_EXISTS`。两者都要显式指定官方 registry: ```bash pnpm audit --registry=https://registry.npmjs.org ``` ## 发布前检查清单 以下每一条都在本仓库实测过。顺序即执行顺序。 ### 1. 元数据 - `name` 为 `@melosic/dsh-dev-workflow`(scoped;`dsh-dev-workflow` 在 npm 上已被他人占用)。 - `version` 与即将发布的版本号一致。 - `type` 为 `module`(ESM-only 是 DSH 插件的硬要求)。 - `main` / `types` / `exports` 指向 `lib/`;`exports` 含 `.`、`./cordis.patch.yml`、 `./locale/*.json`、`./package.json`。 - `dsh.bundle.patch` 为 `./cordis.patch.yml`。 - `cordis.patch.yml` **同时**被 `dsh.bundle.patch` 声明并列入 `files`—— 由 `pnpm ci:checks` 的第二项守卫强制。 #### bundle manifest 完整性检查 DSH 装的不是一个 npm 包,是一个 bundle:`package.json` 的 `dsh.bundle.patch` 指向一份 patch, 宿主把它插进 profile 的 Cordis 入口列表。**这四处任何一处断了,症状都发生在用户机器上, 而不在本仓库的 CI 里。** 发布前逐条核: | 要核的东西 | 怎么核 | 断了的症状 | | --- | --- | --- | | `dsh.bundle.patch` 声明了 | `node -e "console.log(require('./package.json').dsh.bundle.patch)"` | 装上了也不会被挂载;用户看到「装成功但没反应」 | | 声明的文件真实存在 | 上一条的输出能在仓库根找到 | 同上,且 `pnpm pack` 不会报错 | | 该文件在 `files` 白名单里 | `pnpm ci:checks` 第二项守卫 | 本地一切正常,tarball 里没有它——只在用户机器上炸 | | patch 内容与入口 id | patch 里 `id: dsh-dev-workflow`、`name: '@melosic/dsh-dev-workflow'`;id 不能与 DSH 已有 loader 条目冲突 | 与别人的条目打架,或加载到错的包 | `pnpm ci:checks` 只强制其中两条(声明 + 随包发布)。**剩下两条没有自动守卫**, 所以要靠 `pnpm pack --dry-run`(看 `cordis.patch.yml` 在不在列表里)与一次真实安装来确认: ```bash pnpm pack --dry-run # 产物里有 cordis.patch.yml dsh plugin --profile add @melosic/dsh-dev-workflow # 装完能挂载、命令能出现 ``` 最可靠的验收是**装到一个空 profile 里**跑一次:`/dev-workflow status` 能出五行、 两个工具出现在工具列表里,就说明 manifest 的整条链路是通的。这一步不能靠「CI 绿了」代替。 ### 2. 打包产物 ```bash pnpm pack --dry-run ``` 本仓库的实测输出(v0.1.0)为 tarball `melosic-dsh-dev-workflow-0.1.0.tgz`,其中至少有: ``` CHANGELOG.md cordis.patch.yml client.js LICENSE README.md README.zh.md package.json lib/**(.js + .d.ts + .js.map) locale/en.json locale/zh.json skills/dsh-dev-workflow/SKILL.md skills/dsh-dev-workflow/SKILL.zh.md ``` 完整清单以 `pnpm pack --dry-run` 的实际输出为准——上面这份会随 `files` 变化。 四点值得盯: - **`cordis.patch.yml` 必须在里面**。它不在,包装上了也不会被挂载——而这只会发生在用户机器上。 - **`client.js` 必须在里面**。它是设置面板的浏览器侧代码,由 `dsh.client` 声明而非 `files` 白名单兜底;漏掉它面板会空白,同样只在用户机器上暴露。 - **`docs/` 不在里面**,这是刻意的:`files` 是白名单,深度文档随仓库发布,不随包发布。 - **`tests/` 不在里面**,同理。 - 目录名是单数 **`locale/`**,不是 `locales/`。DSH 宿主按 `` `${specifier}/locale/en.json` `` 读字典,复数目录会静默读不到。 ### 3. 门禁 ```bash pnpm typecheck && pnpm lint && pnpm format:check && pnpm test && pnpm build && pnpm ci:checks ``` `ci:checks` 输出 `locale keys aligned (112 keys); bundle patch declared and shipped; skill headings aligned (28 sections); published entry points present (8 checked); mirrored pairs consistent (3 groups); CHANGELOG records 0.3.0; no numbered development phases cited.`——这就是全部七项结构守卫。(括号里的数字都是当前值, 脚本按实际内容算出,不硬编码;换句话说这些数字会随内容变,不要拿这里出现的具体数字去断言 CI 是否通过。) 这七项不靠人记得跑:`pnpm publish` / `npm publish` 会先触发 `prepack` (`pnpm build && pnpm ci:checks`),所以**打包这一步必然在编译之后、且必然过一遍结构守卫**—— `lib/` 是 gitignore 的构建产物,`files` 里第一项就是它,少了它发出去的包装上去直接不能用。 ### 4. 审计 ```bash pnpm audit --registry=https://registry.npmjs.org ``` 本项目 `dependencies` 为空,**运行期零依赖**,所以审计命中的必然是开发期路径。 必须区分两类命中: | 级别 | 包 | 路径 | 处置 | | --- | --- | --- | --- | | moderate | `fflate` | `@deepseek-ai/dsh > @deepseek-ai/dsh-skill-office > @deepseek-ai/libreoffice-kit > fflate` | 经 peerDependency 传递的开发期路径,**不在本包发布产物中**;由宿主侧升级,本包不干预 | | high | `braces` | `lint-staged > micromatch > braces` | devDependency;**上游暂无修复版本**(`patched <0.0.0`),无法通过升级消除。记录在案,不阻塞发布 | **不为了让审计变绿而删掉 lint-staged。** 用一个真实的开发体验损失,换一条不可能被利用的 告警消失,方向是反的。要判断的是「这条路径能不能到达用户」,答案是不能。 ### 5. 规范一致性 以下各项由 `pnpm ci:checks` 与 CI **机械保证**,本文不复述其判定细节: - `scripts/ci-checks.mjs` 的七项守卫:两个 locale 字典 key 一致;`cordis.patch.yml` 已声明 且在 `files` 里;SKILL 双语标题数量与顺序一致;发布入口文件存在于 `files` 白名单内 (配合 `prepack`);成对镜像文件同改(`docs.mirrors` 的任一组只改了一半就失败); `package.json` 的版本在 `CHANGELOG.md` 里有对应标题;任何随仓库发布的 `.md` 都不按编号 引用开发分期。 - 代码中无硬编码用户可见文本:`git grep -P '[\x{4e00}-\x{9fff}]' -- 'src/**/*.ts'` 应无输出; 英文文案也一律走 `t()`,只在 `locale/*.json` 里出现。 - `git log` 的**新提交**标题行全部符合 Conventional Commits:`.husky/commit-msg` 管提交那一刻, CI 再对**本次 PR 新增的范围**跑一次 `commitlint`(`--from --to `),所以绕过 本地钩子或从网页合并进来的提交同样过不去。存量违规是 `c247d5d` 与 `fd8004f` 两条历史提交的 **正文**行(`body-max-line-length`,上游默认 error);改写已发布的历史不可接受,所以范围是 豁免的,规则不是——新提交仍受同一门禁约束。 - `mode` 默认 `'on'`;四类守卫的每项默认值都不是 `allow`——由 `tests/settings-schema.spec.ts` 对 schema 逐叶子断言,不靠人眼核对。 - `CHANGELOG.md` 的 `[Unreleased]` 段包含本次发布的全部变更(人工核对,无守卫)。 `inject` 声明完整(`export const inject = ['tools', 'skills']`;`commands` 与 `subprocess` 通过 `ctx.get(...)` 可选读取,故不进 `inject`,理由见 [docs/ARCHITECTURE.md](ARCHITECTURE.md)) 由 `tests/register.spec.ts` 断言,不在 `ci:checks` 里。 ### 6. 名称可用性 ```bash npm view dsh-dev-workflow version --registry=https://registry.npmjs.org # 他人占用 npm view @melosic/dsh-dev-workflow version --registry=https://registry.npmjs.org # E404 = 可用 ``` 首次发布前的 `E404` 是**好**消息。已发布之后这里会返回版本号,那就不再是可用性检查, 而是「上一个版本是什么」的查询。 ## DSH peerDependencies 的分段枚举规则 这一节是 `.dev-docs/PROGRESS.md` 「待决策 / 遗留问题」第 4 条要求的落地。 **写错这里,后果不是安装失败,而是插件被静默禁用。** DSH 从 `0.1.7-rc.1` 起在加载前做兼容性校验(`dsh-app-boot` 的 `preflight()`)。规则如下: 1. **只读 `peerDependencies`。** `dependencies` 完全不参与兼容判断 (`if (!Object.hasOwn(fields, "peerDependencies")) return void 0;`)。 把 DSH 域内的包写进 `dependencies` 而不是 `peerDependencies`,等于没声明兼容范围。 2. **只校验 `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-*` 前缀的 peer。** `@deepseek-ai/cordis`、`react` 之类的 peer 会被跳过——它们仍是正常的 npm peer, 只是不参与 DSH 的兼容判断。 3. **`0.x` 下不要用 `^` 或 `~`。** 判定是 `semver.satisfies(runtimeVersion, requirement, { includePrerelease: true })`, 而 `0.x` 的次版本位可以携带破坏性变更,`^0.2.0` 的语义在这里会误导人。 官方包对 DSH 域内 peer 一律写精确版本(`0.2.0-rc.2`)。 4. **按实测基线写下界,暂不写上界(本项目的当前选择)。** 本项目采用的写法是: ```json "@deepseek-ai/dsh": ">=0.2.0-rc.2", "@deepseek-ai/dsh-skill": ">=0.2.0-rc.2", "@deepseek-ai/dsh-tools": ">=0.2.0-rc.2" ``` 下界是「本插件实测可用过的最低版本」——三个包都在 `0.2.0-rc.2` 上实测过,所以下界写 `rc.2` 而不是 `rc.1`(凭记忆写更低的下界,等于宣称一段没人验过的兼容性)。 **不写上界的代价要说清楚**:DSH 出到 `0.3` 时,即使 API 变了插件也会被加载,问题会推迟到 运行时才暴露,而不是在加载时被 `preflight()` 干净地禁用。这个选择是在「`0.3.0` 尚不存在、 上限纯属猜测」与「未来不兼容会静默加载」之间取的偏向前者;等 `0.3` 真正发布并重新实测之后, 应当把上界补回来。 5. **`peerDependenciesMeta.optional` 不救场。** 兼容性校验只遍历 `peerDependencies` 本身, 不读 `peerDependenciesMeta`。把某个 DSH 包标成 optional 不会让它跳过校验。 6. **不兼容的后果是「被禁用」而非「安装失败」。** `preflight()` 把该行 `row.disabled = true` 后**照常加载其余行**,stderr 打印 `disabling profile plugin