@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 +29 -0
- package/README.md +9 -3
- package/lib/engine.js +86 -5
- package/lib/index.js +14 -1
- package/package.json +1 -1
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
|
|
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 #
|
|
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.
|
|
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",
|