@bushizhanghan/dsh-publish-gate 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0
4
+
5
+ - `changelog-not-shipped` (warn): a CHANGELOG.md that exists on disk but is not in the tarball. npm
6
+ force-includes README and LICENSE and nothing else, so the file you keep updating never reaches the
7
+ registry page. Hit it the first time I added a changelog and had to add it to `files` by hand.
8
+ - `git-tag-unverified` (info): origin could not be read, so the tag was not checked. Previously this
9
+ fell back to the local tag list and claimed a missing tag — six of my nine plugins were flagged on a
10
+ day GitHub was unreachable, and all six were tagged. The local list proves nothing when the commit
11
+ was pushed through the API and local HEAD is not the shipped commit.
12
+ - First end-to-end run of the gate with the test layer over all nine plugins in this workspace: every
13
+ suite passed, and the tag rule was the only false positive.
14
+
15
+ - `changelog-not-shipped` (warn): a CHANGELOG.md that exists on disk but is not in the tarball. npm
16
+ force-includes README and LICENSE and nothing else, so the file you keep updating never reaches the
17
+ registry page. Hit it the first time I added a changelog and had to add it to `files` by hand.
18
+ - First end-to-end run of the gate with the test layer over all nine plugins in this workspace.
19
+
20
+ ## 0.16.0
21
+
22
+ - `market-release-behind` (warn): the newest GitHub release is older (or newer) than package.json. A
23
+ catalogue entry may install `releases/latest/download/<asset>`, so a lagging release serves code the
24
+ description no longer describes — a reviewer blocked a submission for exactly that.
25
+ - `straySeverities` + `internal-bad-severity`: a finding whose severity is not error/warn/info is counted
26
+ by nothing and reported nowhere, so the rule looks like it never fires. `publish_gate` now turns any such
27
+ finding into a loud error instead of dropping it.
28
+ - The source scan added in 0.15.0 caught the same mistake again while building this version: a string
29
+ replace aimed at RULE_ORDER matched an earlier `F(...)` call and wrote the new rule name into a severity
30
+ slot. Fixed, and the fix now goes through the RULE_ORDER slice instead of a global replace.
31
+
3
32
  ## 0.15.0
4
33
 
5
34
  - Three rules shipped with a *rule name* in their severity slot: `market-repo-too-new`,
package/README.md CHANGED
@@ -8,6 +8,10 @@
8
8
 
9
9
  门禁分三层:**清单层**看 package.json / patch / 入口源码,**产物层**真的跑一遍 `npm pack --dry-run` 看 npm 到底会发出什么,**运行时层**在独立子进程里 `import()` 入口并 `apply(ctx)` 一遍,看它到底注册了什么。
10
10
 
11
+ ### 自检:看不见的 finding
12
+
13
+ `severity` 不是 `error`/`warn`/`info` 的 finding 会被所有计数器忽略,等于规则从没触发过。`publish_gate` 现在会把这类 finding 提升成一条 `internal-bad-severity` 的 error,测试里还有一条源码扫描盯着 `F(...)` 的第二个参数。这条网加上当天就抓到了我自己的同一个错误。
14
+
11
15
  ### 测试层:门禁真的会跑一遍你的测试
12
16
 
13
17
  `tests-fail` / `tests-timeout` / `tests-not-run` 三条规则会以 `node --test` 在插件目录里真跑一次(不经过 `npm test`,因为脚本内容由作者决定、可能 shell 出去)。子进程会去掉继承来的 `NODE_TEST_CONTEXT`——否则当门禁本身跑在 `node --test` 里时,子进程进入 test-child 模式、一个摘要都不打印,看起来像"没有测试"。
@@ -76,7 +80,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
76
80
  | `readme-wrong-package` | README 的安装命令写的是**另一个包名**(从别的插件抄过来没改)。照着装的人装到别的插件上,而且不会报错——那个包真的存在 |
77
81
 
78
82
  ### 值得在发布前知道的(warn / info)
79
- `npm-scope-mismatch`(登录的账号不是包 scope 的所有者,PUT 会 403,而错误里既不说 token 也不说 scope——我为一个 403 花了一小时)、`unscoped-name`(granular token 的 403)、`repo-missing`、`patch-name-unresolvable`(junction 放错层级)、`inject-unused-service`(`inject` 里写了个拼错的服务名,cordis 一直等它,插件静默不激活)、`no-publish-config-access`(scoped 包没写 `publishConfig.access`,只是提醒,因为命令里本来就带 `--access public`)、`publish-config-registry`(`publishConfig.registry` 指到内网 registry,版本发到别人装不到的地方)、`no-readme` / `readme-no-install` / `readme-no-profile`、`license-mismatch`、`no-tests` / `no-test-files` / `no-test-script`、`no-engines`、`bad-repo-url`、`no-git` / `no-gitignore`、`readme-tool-undocumented`(README 没提这个工具)/ `readme-test-count`(README 写的测试数和真实数不一致)、`changelog-missing-version`(要发的这个版本号在 CHANGELOG 里没有对应标题,npm 照样发,发布说明就永远落后一版)、`changelog-no-versions`(CHANGELOG 里没有版本标题,读者对不上自己装的那版)、`no-changelog`(只是提醒),以及市场条目的 `market-*` 全系列。
83
+ `npm-scope-mismatch`(登录的账号不是包 scope 的所有者,PUT 会 403,而错误里既不说 token 也不说 scope——我为一个 403 花了一小时)、`unscoped-name`(granular token 的 403)、`repo-missing`、`patch-name-unresolvable`(junction 放错层级)、`inject-unused-service`(`inject` 里写了个拼错的服务名,cordis 一直等它,插件静默不激活)、`no-publish-config-access`(scoped 包没写 `publishConfig.access`,只是提醒,因为命令里本来就带 `--access public`)、`publish-config-registry`(`publishConfig.registry` 指到内网 registry,版本发到别人装不到的地方)、`no-readme` / `readme-no-install` / `readme-no-profile`、`license-mismatch`、`no-tests` / `no-test-files` / `no-test-script`、`no-engines`、`bad-repo-url`、`no-git` / `no-gitignore`、`readme-tool-undocumented`(README 没提这个工具)/ `readme-test-count`(README 写的测试数和真实数不一致)、`changelog-missing-version`(要发的这个版本号在 CHANGELOG 里没有对应标题,npm 照样发,发布说明就永远落后一版)、`changelog-no-versions`(CHANGELOG 里没有版本标题,读者对不上自己装的那版)、`no-changelog`(只是提醒)、`changelog-not-shipped`(CHANGELOG 在本地但没进 tarball——npm 只强制带 README 和 LICENSE,你一直更新的那份文件在 registry 页面上看不见),以及市场条目的 `market-*` 全系列。
80
84
 
81
85
  ### 产物层:npm 到底会发出什么
82
86
 
@@ -143,6 +147,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
143
147
  | 规则 | 抓什么 |
144
148
  |---|---|
145
149
  | `market-duplicate-entry` | 这个仓库**已经在目录里**。再提一个 PR 就是同一仓库的第二条条目,reviewer 会拒。版本升级根本不需要 PR——目录条目只存 `url` |
150
+ | `market-release-behind` | warn | 最新 GitHub release 比 package.json 旧(或新) | 目录里写着 1.2.7、release 资产还是 1.2.6 时,市场条目装的是旧代码,而描述是新版写的——维护者正是以「描述与 tarball 不符」驳回过一次提交 |
146
151
  | `market-npm-name-collision` | 另一个条目也在发同一个 npm 包,安装路径会互相盖 |
147
152
  | `market-repo-too-new` | 仓库创建不满 24 小时。门禁会报 `0.0 days old` 然后**自己重跑**,PR 不会丢,但在那之前什么都不会合 |
148
153
  | `market-fork-of-listed` | 这个仓库是某个**已经在目录里**的仓库的 fork。两条条目装的代码抢同一批 id,两个都装就有一个被静默盖掉——而且 reviewer 会把"没写"当成"隐瞒 fork" |
@@ -187,7 +192,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
187
192
  |---|---|
188
193
  | `git-uncommitted` | 会进 tarball 的文件有未提交改动。发出去的字节不在任何 commit 里,以后复现不出来、也没法 bisect |
189
194
  | `git-tag-missing` | HEAD 上没有 `v<version>` tag。发完之后这些字节的唯一记录就是 registry,GitHub release、changelog、市场条目都没法指向一个从没打过 tag 的 commit |
190
- | `git-tag-elsewhere` | tag 存在但不在 HEAD 上:tag 和 tarball 是两份不同的字节,从那个 tag 构建出来的东西不是你刚发的东西 |
195
+ | `git-tag-elsewhere` | tag 存在但不在 HEAD 上:tag 和 tarball 是两份不同的字节,从那个 tag 构建出来的东西不是你刚发的东西 |
196
+ | `git-tag-unverified` | 读不到 `origin`(断网、私有仓库、没配 remote)。这是 **info**,不是 warn:提交是用 GitHub API 推的时候本地 HEAD 不是发出去的那个 commit,本地 tag 列表证明不了任何事。断网那天我有六个插件被误报「没有 tag」,六个都打了 |
191
197
 
192
198
  都是 warn:本地试发、私有包、发完再补 tag 都合理。但「发完之后指不出是哪份代码」这件事,事后补不回来。
193
199
 
@@ -227,7 +233,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
227
233
  ## 开发
228
234
 
229
235
  ```sh
230
- node --test # 113 个引擎单测
236
+ node --test # 117 个引擎单测
231
237
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
232
238
  ```
233
239
 
package/lib/engine.js CHANGED
@@ -27,7 +27,17 @@ const safeText = (p) => {
27
27
  return b === null ? null : b.toString('utf8')
28
28
  }
29
29
 
30
- export const F = (rule, severity, title, detail, fix) => ({ rule, severity, title, detail, fix })
30
+ export const F = (rule, severity, title, detail, fix) => ({ rule, severity, title, detail, fix })
31
+
32
+ /**
33
+ * A finding whose severity is not error/warn/info is invisible: the counters
34
+ * only count those three, so the rule silently never reports. Three of them
35
+ * shipped that way before this existed. The source scan in the test suite
36
+ * catches the literal forms; this catches whatever a future refactor builds.
37
+ */
38
+ export const SEVERITIES = new Set(['error', 'warn', 'info'])
39
+ export const straySeverities = (findings) =>
40
+ [...new Set((findings || []).filter((f) => !SEVERITIES.has(f?.severity)).map((f) => String(f?.rule || '<no rule>')))]
31
41
 
32
42
  /** package.json without the BOM trap. Null when it is missing or unparseable. */
33
43
  export function readPkgJson(dir) {
@@ -857,7 +867,8 @@ export async function checkMarketReadiness(dir, opts = {}) {
857
867
  return out
858
868
  }
859
869
  const json = readPkgJson(dir)
860
- const pkgName = typeof json?.name === 'string' ? json.name : null
870
+ const pkgName = typeof json?.name === 'string' ? json.name : null
871
+ const pkgVersion = typeof json?.version === 'string' ? json.version : null
861
872
 
862
873
  let entries = Array.isArray(opts.entries) ? opts.entries : null
863
874
  try {
@@ -972,6 +983,37 @@ export async function checkMarketReadiness(dir, opts = {}) {
972
983
  ),
973
984
  )
974
985
  }
986
+ }
987
+ // A catalogue entry can point at a GitHub release asset instead of npm. When
988
+ // the newest release is older than this directory, the entry serves code the
989
+ // description no longer matches — a reviewer blocked a submission for exactly
990
+ // that (the entry said 1.2.7, the asset was still 1.2.6).
991
+ let releaseInfo = opts.releaseInfo ?? null
992
+ if (releaseInfo === null) {
993
+ try {
994
+ const res = await fetch(`https://api.github.com/repos/${slug}/releases/latest`, {
995
+ signal: AbortSignal.timeout(timeoutMs),
996
+ headers: { 'user-agent': 'dsh-publish-gate', accept: 'application/vnd.github+json' },
997
+ })
998
+ if (res.ok) releaseInfo = await res.json()
999
+ } catch {
1000
+ // no network, no releases, or a rate limit — the rule stays quiet
1001
+ }
1002
+ }
1003
+ const releaseTag = typeof releaseInfo?.tag_name === 'string' ? releaseInfo.tag_name.replace(/^v/, '') : null
1004
+ if (releaseTag && pkgVersion && releaseTag !== pkgVersion) {
1005
+ const older = compareVersions(releaseTag, pkgVersion) < 0
1006
+ out.push(
1007
+ F(
1008
+ 'market-release-behind',
1009
+ 'warn',
1010
+ `the newest GitHub release is ${releaseTag}, this directory says ${pkgVersion}`,
1011
+ older
1012
+ ? 'An entry that installs releases/latest/download/<asset> serves the older code while the description is written for the newer version — a reviewer reads that as a false claim.'
1013
+ : `The release is ahead of package.json, so what the release ships is not what this directory builds.`,
1014
+ `Cut a ${pkgVersion} release (asset name unchanged) or point the entry at npm.`,
1015
+ ),
1016
+ )
975
1017
  }
976
1018
  return out
977
1019
  }
@@ -1171,6 +1213,22 @@ export async function checkPack(dir, opts = {}) {
1171
1213
  'Add "lib" to files.',
1172
1214
  ),
1173
1215
  )
1216
+ }
1217
+ // npm does not force-include a changelog the way it does README and LICENSE,
1218
+ // so a CHANGELOG.md that is not listed in `files` never reaches the registry
1219
+ // page — the file exists, the author updates it, and nobody installing the
1220
+ // package can see it. I hit this the first time I added one.
1221
+ const changelogOnDisk = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'].find((f) => existsSync(join(dir, f)))
1222
+ if (changelogOnDisk && !paths.includes(changelogOnDisk)) {
1223
+ out.push(
1224
+ F(
1225
+ 'changelog-not-shipped',
1226
+ 'warn',
1227
+ `${changelogOnDisk} is not in the tarball`,
1228
+ 'npm force-includes README and LICENSE but not the changelog, so the file you keep updating is invisible on the package page.',
1229
+ `Add "${changelogOnDisk}" to files.`,
1230
+ ),
1231
+ )
1174
1232
  }
1175
1233
  if (json?.dsh?.bundle?.patch && !paths.includes(String(json.dsh.bundle.patch).replace(/^\.\//, ''))) {
1176
1234
  out.push(
@@ -2399,6 +2457,29 @@ export async function checkTraceability(dir, opts = {}) {
2399
2457
  return out
2400
2458
  }
2401
2459
 
2460
+ // No answer from the remote. Two very different situations hide here: a repo
2461
+ // with no `origin` at all, where the local tag is the only truth and a missing
2462
+ // one is real; and a repo whose `origin` simply could not be reached, where
2463
+ // local HEAD is not the shipped commit (the API pusher builds its own commit)
2464
+ // and the tag usually exists. A check that cannot see the truth must not claim
2465
+ // a defect — this reported six false positives on a day GitHub was down.
2466
+ const remotes = opts.remotes === undefined ? await gitOut(dir, ['remote']) : opts.remotes
2467
+ const hasOrigin = typeof remotes === 'string' && remotes.split('\n').map((r) => r.trim()).includes('origin')
2468
+ if (hasOrigin) {
2469
+ if (!localHasIt) {
2470
+ out.push(
2471
+ F(
2472
+ 'git-tag-unverified',
2473
+ 'info',
2474
+ `could not check whether v${version} is tagged on the remote`,
2475
+ 'origin could not be read, and with an API-pushed commit the local HEAD is not the commit that shipped, so the local tag list proves nothing either way.',
2476
+ 'Re-run when the network is back — the tag may well exist.',
2477
+ ),
2478
+ )
2479
+ }
2480
+ return out
2481
+ }
2482
+
2402
2483
  if (typeof atHead === 'string' && !localHasIt) {
2403
2484
  const exists = opts.tagExists === undefined ? await gitOut(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/v${version}`]) : opts.tagExists ? 'sha' : null
2404
2485
  out.push(
@@ -2481,9 +2562,9 @@ const RULE_ORDER = [
2481
2562
  'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'npm-not-logged-in', 'npm-scope-mismatch', 'npm-whoami-unavailable', 'registry-unreachable', 'publish-not-served',
2482
2563
  'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
2483
2564
  'market-no-name', 'market-name-shape', 'market-no-category', 'market-bad-category', 'market-no-desc', 'market-no-en', 'market-no-zh', 'unscoped-name', 'no-git', 'no-readme', 'readme-no-install', 'readme-no-profile', 'readme-wrong-package',
2484
- 'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new',
2485
- 'no-changelog', 'changelog-no-versions', 'changelog-missing-version',
2486
- 'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
2565
+ 'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new', 'market-release-behind',
2566
+ 'no-changelog', 'changelog-no-versions', 'changelog-missing-version', 'changelog-not-shipped',
2567
+ 'git-uncommitted', 'git-tag-missing', 'git-tag-unverified', 'git-tag-elsewhere',
2487
2568
  'readme-tool-undocumented', 'readme-test-count',
2488
2569
  'no-tests', 'no-test-files', 'no-test-script', 'no-engines', 'bad-repo-url', 'no-gitignore',
2489
2570
  ]
package/lib/index.js CHANGED
@@ -29,7 +29,8 @@ import {
29
29
  latestPublished,
30
30
  renderReport,
31
31
  runArtifactChecks,
32
- checkTestRun,
32
+ checkTestRun,
33
+ straySeverities,
33
34
  runStaticChecks,
34
35
  } from './engine.js'
35
36
 
@@ -116,6 +117,18 @@ export function apply(ctx, config = {}) {
116
117
  const tests = args.skipTests ? null : await checkTestRun(dir, { timeoutMs: cfg.testTimeoutMs })
117
118
  if (tests) findings.push(...tests)
118
119
  if (!args.skipNetwork) findings.push(...(await networkGate(dir)))
120
+ // A finding with an unexpected severity is invisible to the counters
121
+ // below, so the rule silently never reports. Make that loud instead.
122
+ const stray = straySeverities(findings)
123
+ if (stray.length > 0) {
124
+ findings.push({
125
+ rule: 'internal-bad-severity',
126
+ severity: 'error',
127
+ title: `these rules built a finding with an unknown severity: ${stray.join(", ")}`,
128
+ detail: 'The gate counted them as nothing, so they never appeared in a report. This is a bug in publish-gate, not in the plugin being gated.',
129
+ fix: 'Report it: the severity slot of F(...) took something other than error/warn/info.',
130
+ })
131
+ }
119
132
  const counts = {
120
133
  error: findings.filter((f) => f.severity === 'error').length,
121
134
  warn: findings.filter((f) => f.severity === 'warn').length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Pre-publish gate for DSH plugins: manifest, dependencies, artifact, runtime, browser half, traceability and market readiness — plus an input fuzzer that drives every declared parameter with the wrong type. 发布前的七层门禁 + 入参模糊测试。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",