@bushizhanghan/dsh-publish-gate 0.14.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 +23 -0
- package/README.md +6 -1
- package/lib/engine.js +56 -17
- package/lib/index.js +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
15
|
+
## 0.15.0
|
|
16
|
+
|
|
17
|
+
- Three rules shipped with a *rule name* in their severity slot: `market-repo-too-new`,
|
|
18
|
+
`test-script-dir-arg` and `tool-missing-output-schema` were built as
|
|
19
|
+
`F(rule, "some-other-rule", "a-third-rule", "warn", title, ...)`, because a string replace aimed at
|
|
20
|
+
RULE_ORDER matched an earlier `F(...)` call instead and pushed the new names into its arguments.
|
|
21
|
+
Nothing failed: a finding whose severity is not error/warn/info falls out of every counter and never
|
|
22
|
+
reaches the report, so the rules looked like they simply never fired. Fixed all three, added the six
|
|
23
|
+
rule names that never made it into RULE_ORDER, and added a source scan that fails when any `F(...)`
|
|
24
|
+
call passes something other than an error/warn/info literal (or a ternary of two of them).
|
|
25
|
+
|
|
3
26
|
## 0.14.0
|
|
4
27
|
|
|
5
28
|
- New test layer: `checkTestRun` runs `node --test` in the plugin directory and reports `tests-fail`
|
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 #
|
|
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 {
|
|
@@ -931,7 +942,7 @@ export async function checkMarketReadiness(dir, opts = {}) {
|
|
|
931
942
|
if (hours < 24) {
|
|
932
943
|
out.push(
|
|
933
944
|
F(
|
|
934
|
-
'market-repo-too-new',
|
|
945
|
+
'market-repo-too-new',
|
|
935
946
|
'warn',
|
|
936
947
|
`the repo is ${hours.toFixed(1)}h old; the submission gate needs 24h`,
|
|
937
948
|
'The gate fails with a "repo too new" message and reruns on its own, so the PR is not lost — but nothing merges until it passes.',
|
|
@@ -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
|
}
|
|
@@ -1044,10 +1086,7 @@ export function checkTests(dir) {
|
|
|
1044
1086
|
// while the tests themselves are fine. Auto-discovery needs no argument.
|
|
1045
1087
|
const testScript = pkgJson?.scripts?.test || ''
|
|
1046
1088
|
const dirArg = testScript.match(/\bnode\s+--test\s+(?!-)(\S+)/)
|
|
1047
|
-
if (dirArg) out.push(F('test-script-dir-arg',
|
|
1048
|
-
'tests-fail',
|
|
1049
|
-
'tests-timeout',
|
|
1050
|
-
'tests-not-run', 'warn', `test script passes a path to node --test ("${dirArg[1]}")`, 'Node 22+ treats that argument as a module path, not a directory to glob: `node --test test/` throws MODULE_NOT_FOUND, so npm test can never pass even though the tests are fine.', 'Drop the path — "test": "node --test" auto-discovers test/**/*.test.mjs.'))
|
|
1089
|
+
if (dirArg) out.push(F('test-script-dir-arg', 'warn', `test script passes a path to node --test ("${dirArg[1]}")`, 'Node 22+ treats that argument as a module path, not a directory to glob: `node --test test/` throws MODULE_NOT_FOUND, so npm test can never pass even though the tests are fine.', 'Drop the path — "test": "node --test" auto-discovers test/**/*.test.mjs.'))
|
|
1051
1090
|
return out
|
|
1052
1091
|
}
|
|
1053
1092
|
|
|
@@ -1069,12 +1108,12 @@ export async function checkTestRun(dir, opts = {}) {
|
|
|
1069
1108
|
let stdout = ''
|
|
1070
1109
|
let code = 0
|
|
1071
1110
|
let timedOut = false
|
|
1072
|
-
// NODE_TEST_CONTEXT is inherited from whoever called us: when this gate runs
|
|
1073
|
-
// inside `node --test` itself, the child starts in test-child mode and prints
|
|
1074
|
-
// no summary at all, which reads as "no tests" — a false alarm.
|
|
1075
|
-
const env = { ...process.env }
|
|
1076
|
-
delete env.NODE_TEST_CONTEXT
|
|
1077
|
-
try {
|
|
1111
|
+
// NODE_TEST_CONTEXT is inherited from whoever called us: when this gate runs
|
|
1112
|
+
// inside `node --test` itself, the child starts in test-child mode and prints
|
|
1113
|
+
// no summary at all, which reads as "no tests" — a false alarm.
|
|
1114
|
+
const env = { ...process.env }
|
|
1115
|
+
delete env.NODE_TEST_CONTEXT
|
|
1116
|
+
try {
|
|
1078
1117
|
const r = await runFile(process.execPath, ['--test'], { cwd: dir, env, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, windowsHide: true })
|
|
1079
1118
|
stdout = String(r.stdout || '')
|
|
1080
1119
|
} catch (error) {
|
|
@@ -1634,7 +1673,7 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1634
1673
|
),
|
|
1635
1674
|
)
|
|
1636
1675
|
}
|
|
1637
|
-
if (!t.hasOutputSchema) out.push(F('tool-missing-output-schema', '
|
|
1676
|
+
if (!t.hasOutputSchema) out.push(F('tool-missing-output-schema', 'warn', `tool "${label}" declares no output.schema`, 'The host validates tool output against output.schema; without one the contract is implicit and a malformed result reaches the caller.', "Add output: { schema: { type: 'json' }, render }."))
|
|
1638
1677
|
if (t.cardKind && typeof t.cardKind === 'object') {
|
|
1639
1678
|
out.push(F('tool-present-call-throws', 'error', `tool "${label}" presentCall() throws`, `presentCall renders the transcript card and must be total: ${t.cardKind.threw}`, 'Do not read args fields that may be absent.'))
|
|
1640
1679
|
} else if (typeof t.cardKind === 'string' && !CARD_KINDS.has(t.cardKind)) {
|
|
@@ -2471,12 +2510,12 @@ const RULE_ORDER = [
|
|
|
2471
2510
|
'client-factory-throws', 'client-no-apply', 'client-apply-throws', 'client-no-registration', 'client-double-load',
|
|
2472
2511
|
'client-inject-missing-slots', 'client-inject-missing-sidebarRightTabs', 'client-unavailable',
|
|
2473
2512
|
'no-patch-file', 'no-insert-row', 'insert-missing-name', 'insert-name-mismatch',
|
|
2474
|
-
'patch-name-unresolvable', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
|
|
2513
|
+
'patch-name-unresolvable', 'tests-fail', 'tests-timeout', 'tests-not-run', 'no-name-export', 'no-apply-export', 'no-inject', 'require-in-esm',
|
|
2475
2514
|
'entry-import-failed', 'apply-threw', 'tools-unavailable',
|
|
2476
2515
|
'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
|
|
2477
2516
|
'inject-unused-service',
|
|
2478
2517
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
|
2479
|
-
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-undeclared-param', 'tool-input-throws', 'tool-call-throws', 'tool-output-invalid', 'tool-call-slow',
|
|
2518
|
+
'tool-bad-card-kind', 'tool-no-description', 'tool-description-thin', 'tool-no-parameters', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-undeclared-param', 'tool-input-throws', 'tool-call-throws', 'tool-output-invalid', 'tool-call-slow',
|
|
2480
2519
|
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
|
|
2481
2520
|
'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
|
|
2482
2521
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
@@ -2484,7 +2523,7 @@ const RULE_ORDER = [
|
|
|
2484
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',
|
|
2485
2524
|
'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
2486
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',
|
|
2487
|
-
'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',
|
|
2488
2527
|
'no-changelog', 'changelog-no-versions', 'changelog-missing-version',
|
|
2489
2528
|
'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
|
|
2490
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.
|
|
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",
|