@bushizhanghan/dsh-publish-gate 0.15.0 → 0.16.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0
4
+
5
+ - `market-release-behind` (warn): the newest GitHub release is older (or newer) than package.json. A
6
+ catalogue entry may install `releases/latest/download/<asset>`, so a lagging release serves code the
7
+ description no longer describes — a reviewer blocked a submission for exactly that.
8
+ - `straySeverities` + `internal-bad-severity`: a finding whose severity is not error/warn/info is counted
9
+ by nothing and reported nowhere, so the rule looks like it never fires. `publish_gate` now turns any such
10
+ finding into a loud error instead of dropping it.
11
+ - The source scan added in 0.15.0 caught the same mistake again while building this version: a string
12
+ replace aimed at RULE_ORDER matched an earlier `F(...)` call and wrote the new rule name into a severity
13
+ slot. Fixed, and the fix now goes through the RULE_ORDER slice instead of a global replace.
14
+
3
15
  ## 0.15.0
4
16
 
5
17
  - 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 模式、一个摘要都不打印,看起来像"没有测试"。
@@ -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" |
@@ -227,7 +232,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
227
232
  ## 开发
228
233
 
229
234
  ```sh
230
- node --test # 113 个引擎单测
235
+ node --test # 115 个引擎单测
231
236
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
232
237
  ```
233
238
 
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
  }
@@ -2481,7 +2523,7 @@ const RULE_ORDER = [
2481
2523
  '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
2524
  'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
2483
2525
  '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',
2526
+ 'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new', 'market-release-behind',
2485
2527
  'no-changelog', 'changelog-no-versions', 'changelog-missing-version',
2486
2528
  'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
2487
2529
  'readme-tool-undocumented', 'readme-test-count',
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.16.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",