@bushizhanghan/dsh-publish-gate 0.8.1 → 0.9.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 +14 -0
- package/README.md +3 -2
- package/lib/engine.js +59 -1
- package/lib/index.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
- `checkChangelog()`: `changelog-missing-version` (warn), `changelog-no-versions` (warn), `no-changelog` (info).
|
|
6
|
+
The changelog is the only place a user can learn what a version changed; a bump without an entry is silent
|
|
7
|
+
drift, because npm publishes it anyway and the release notes stay one version behind. Headings accept
|
|
8
|
+
`## 1.2.3`, `### [v1.2.3]` and `# v1.2.3`.
|
|
9
|
+
- An `engines-missing` rule was written and then deleted: `no-engines` already existed. Two rules for one
|
|
10
|
+
condition would have reported the same gap twice.
|
|
11
|
+
## 0.8.2
|
|
12
|
+
|
|
13
|
+
- `publish_gate_verify` now returns `pending: true` when the wait runs out with the version still invisible.
|
|
14
|
+
Measured on 0.8.1 itself: the version appeared **four minutes** after `npm publish`, so one call cannot
|
|
15
|
+
outwait it — the cap is 90 s because the host kills a tool call at 120 s. `pending` says "run it again",
|
|
16
|
+
which is a different answer from "the registry holds different bytes".
|
|
3
17
|
## 0.8.1
|
|
4
18
|
|
|
5
19
|
- `publish_gate_verify` gained `waitMs` (capped at 90 s) and reports `waitedMs`. npm answers 202 and keeps
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
72
72
|
| `readme-wrong-package` | README 的安装命令写的是**另一个包名**(从别的插件抄过来没改)。照着装的人装到别的插件上,而且不会报错——那个包真的存在 |
|
|
73
73
|
|
|
74
74
|
### 值得在发布前知道的(warn / info)
|
|
75
|
-
`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
|
|
75
|
+
`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-*` 全系列。
|
|
76
76
|
|
|
77
77
|
### 产物层:npm 到底会发出什么
|
|
78
78
|
|
|
@@ -207,6 +207,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
207
207
|
- **产物层用 `npm pack --dry-run`,不用自己推 `files` 规则**:npm 的入包规则(`main` / `exports` 强制入包、README/LICENSE 强制入包、`.npmignore` 优先级)比自己实现的准。唯一的代价是它需要这台机器上有 npm,所以跑不起来时降级成 `pack-unavailable` 而不是报错。
|
|
208
208
|
- **运行时层真的执行插件代码**:这是唯一能回答「装上能不能用」的办法。代价是 `apply()` 会跑,所以:假 ctx 是 Proxy 不落盘、20 秒硬超时、`skipTools` 一键跳过。
|
|
209
209
|
- **`--ignore-scripts`**:dry run 不跑 `prepack`,否则检查会触发一次构建。代价是清单可能和真实产物不同,所以有 `pack-script-build` 把这件事说出来。
|
|
210
|
+
- **发完那几十秒不算失败**:npm 收下包会先回 202,然后六十秒到几分钟里这个版本在 registry 上根本不存在。这个插件自己的 0.8.1 实测是**四分钟**才出现。所以 `publish_gate_verify` 加了 `waitMs` 轮询;一轮等不到就返回 `pending: true`——那是「还没轮到」,不是「没发成功」。单次最多等 90 秒,因为宿主 120 秒就掐掉工具调用,剩下的只能再跑一次。
|
|
210
211
|
- **`publish_gate_verify` 比 shasum,不比版本号**:版本号相同而字节不同(发了又改、或本地根本没发)是最常见的自欺。`npm pack --dry-run` 的 shasum 和 registry 的 `dist.shasum` 是同一套算法,所以这个比较是精确的。
|
|
211
212
|
- **只读**:修不修由你定,每条 finding 都带 `fix` 文案。门禁的职责是把话说清楚,不是替你做决定——所以没有 `--fix`。
|
|
212
213
|
- **确定性**:全部规则是文件内容、子进程或 registry/GitHub API 检查,没有 LLM 调用——一条规则答错,你可以直接看规则。
|
|
@@ -215,7 +216,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
215
216
|
## 开发
|
|
216
217
|
|
|
217
218
|
```sh
|
|
218
|
-
node --test #
|
|
219
|
+
node --test # 97 个引擎单测
|
|
219
220
|
node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
|
|
220
221
|
```
|
|
221
222
|
|
package/lib/engine.js
CHANGED
|
@@ -556,6 +556,56 @@ export function checkDocs(dir) {
|
|
|
556
556
|
return out
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* The changelog is the only place a user can learn what a version changed, and
|
|
561
|
+
* a version bump without an entry is invisible drift: `npm publish` says
|
|
562
|
+
* nothing, and the release notes stay one version behind forever.
|
|
563
|
+
*/
|
|
564
|
+
export function checkChangelog(dir) {
|
|
565
|
+
const out = []
|
|
566
|
+
const json = readPkgJson(dir)
|
|
567
|
+
if (!json?.version) return out
|
|
568
|
+
const file = ['CHANGELOG.md', 'CHANGELOG', 'HISTORY.md'].find((name) => existsSync(join(dir, name)))
|
|
569
|
+
if (!file) {
|
|
570
|
+
out.push(
|
|
571
|
+
F(
|
|
572
|
+
'no-changelog',
|
|
573
|
+
'info',
|
|
574
|
+
'no CHANGELOG.md',
|
|
575
|
+
'Every published version is permanent, so users need a place to read what changed. This is a nudge, not a defect.',
|
|
576
|
+
'Add CHANGELOG.md with one `## <version>` heading per release, and put it in `files`.',
|
|
577
|
+
),
|
|
578
|
+
)
|
|
579
|
+
return out
|
|
580
|
+
}
|
|
581
|
+
const text = safeText(join(dir, file)) || ''
|
|
582
|
+
const headings = [...text.matchAll(/^#{1,3}\s*\[?v?(\d+\.\d+\.\d+)/gm)].map((m) => m[1])
|
|
583
|
+
if (!headings.length) {
|
|
584
|
+
out.push(
|
|
585
|
+
F(
|
|
586
|
+
'changelog-no-versions',
|
|
587
|
+
'warn',
|
|
588
|
+
`${file} has no version headings`,
|
|
589
|
+
'A changelog without `## 1.2.3` headings cannot be matched to a release, so a reader cannot tell which entry belongs to the version they installed.',
|
|
590
|
+
'Give every release a `## <version>` heading.',
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
return out
|
|
594
|
+
}
|
|
595
|
+
if (!headings.includes(json.version)) {
|
|
596
|
+
out.push(
|
|
597
|
+
F(
|
|
598
|
+
'changelog-missing-version',
|
|
599
|
+
'warn',
|
|
600
|
+
`${file} has no entry for ${json.version} (newest heading is ${headings[0]})`,
|
|
601
|
+
'The version you are about to publish has no changelog entry. npm publishes it anyway, and the notes now describe an older release.',
|
|
602
|
+
`Add a \`## ${json.version}\` section at the top of ${file}.`,
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
}
|
|
606
|
+
return out
|
|
607
|
+
}
|
|
608
|
+
|
|
559
609
|
// =====================================================================
|
|
560
610
|
// npm / GitHub / market state
|
|
561
611
|
// =====================================================================
|
|
@@ -1936,7 +1986,13 @@ export async function waitForPublished(name, version, localPack, opts = {}) {
|
|
|
1936
1986
|
await new Promise((r) => setTimeout(r, Math.min(intervalMs, waitMs - (Date.now() - started))))
|
|
1937
1987
|
findings = await probe(net)
|
|
1938
1988
|
}
|
|
1939
|
-
|
|
1989
|
+
const served = findings.some((f) => f.rule === 'publish-served')
|
|
1990
|
+
const mismatch = findings.some((f) => f.rule === 'publish-served-mismatch')
|
|
1991
|
+
// `pending` means the wait ran out with the version still invisible — the
|
|
1992
|
+
// normal outcome for a publish npm has not finished processing, NOT a
|
|
1993
|
+
// failure. Measured on this plugin's own 0.8.1: the version appeared four
|
|
1994
|
+
// minutes after `npm publish`, longer than one call can wait.
|
|
1995
|
+
return { findings, waitedMs: Date.now() - started, pending: !served && !mismatch }
|
|
1940
1996
|
}
|
|
1941
1997
|
|
|
1942
1998
|
/** The async half of the gate: everything that needs a subprocess or the network. */
|
|
@@ -2166,6 +2222,7 @@ export function runStaticChecks(dir) {
|
|
|
2166
2222
|
...checkBuildFreshness(dir),
|
|
2167
2223
|
...checkDependencies(dir),
|
|
2168
2224
|
...checkDocs(dir),
|
|
2225
|
+
...checkChangelog(dir),
|
|
2169
2226
|
...checkTests(dir),
|
|
2170
2227
|
...checkNpmState(dir),
|
|
2171
2228
|
...checkGitState(dir),
|
|
@@ -2194,6 +2251,7 @@ const RULE_ORDER = [
|
|
|
2194
2251
|
'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
2195
2252
|
'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',
|
|
2196
2253
|
'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new',
|
|
2254
|
+
'no-changelog', 'changelog-no-versions', 'changelog-missing-version',
|
|
2197
2255
|
'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
|
|
2198
2256
|
'readme-tool-undocumented', 'readme-test-count',
|
|
2199
2257
|
'no-tests', 'no-test-files', 'no-test-script', 'no-engines', 'bad-repo-url', 'no-gitignore',
|
package/lib/index.js
CHANGED
|
@@ -199,7 +199,7 @@ export function apply(ctx, config = {}) {
|
|
|
199
199
|
waitMs: {
|
|
200
200
|
type: 'number',
|
|
201
201
|
description:
|
|
202
|
-
'Poll until the registry serves the bytes, up to this many milliseconds (capped at 90000). npm
|
|
202
|
+
'Poll until the registry serves the bytes, up to this many milliseconds (capped at 90000 — the host kills a tool call at 120000). npm answers 202 and keeps the version invisible for 60-180 s after a publish, measured up to 4 minutes, so a single shot reports "not published" on a release that already succeeded. When the wait runs out with `pending: true`, run it again; that is not a failure.',
|
|
203
203
|
},
|
|
204
204
|
},
|
|
205
205
|
},
|
|
@@ -212,7 +212,7 @@ export function apply(ctx, config = {}) {
|
|
|
212
212
|
const { pack, findings: packFindings } = await checkPack(dir)
|
|
213
213
|
if (!pack) return { ok: false, error: `npm pack --dry-run did not run, so there is nothing to compare: ${packFindings.map((f) => f.title).join('; ')}` }
|
|
214
214
|
const waitMs = Math.min(Math.max(0, Number(args.waitMs) || 0), 90_000)
|
|
215
|
-
const { findings, waitedMs } = await waitForPublished(json.name, version, pack, {
|
|
215
|
+
const { findings, waitedMs, pending } = await waitForPublished(json.name, version, pack, {
|
|
216
216
|
registry: cfg.registry,
|
|
217
217
|
timeoutMs: cfg.networkTimeoutMs,
|
|
218
218
|
waitMs,
|
|
@@ -224,6 +224,7 @@ export function apply(ctx, config = {}) {
|
|
|
224
224
|
version,
|
|
225
225
|
published: served || findings.some((f) => f.rule === 'publish-served-mismatch'),
|
|
226
226
|
shasumMatch: served,
|
|
227
|
+
pending,
|
|
227
228
|
localShasum: pack.shasum,
|
|
228
229
|
unpackedSize: pack.unpackedSize,
|
|
229
230
|
files: (pack.files || []).length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|