@bushizhanghan/dsh-publish-gate 0.18.0 → 0.20.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 +18 -0
- package/README.md +6 -4
- package/lib/engine.js +34 -3
- package/lib/index.js +31 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
- `publish_gate_commands` now agrees with the rules: a prerelease version gets `npm publish --tag next`
|
|
6
|
+
instead of a bare publish that would move `latest`.
|
|
7
|
+
- New step `tag the shipped commit` (`git tag v<version> && git push origin v<version>`). The gate has
|
|
8
|
+
warned about a missing tag since 0.5.0 and the command list never mentioned tagging — I wrote a
|
|
9
|
+
separate script to do it because the instructions never said to.
|
|
10
|
+
|
|
11
|
+
## 0.19.0
|
|
12
|
+
|
|
13
|
+
- `prerelease-published-as-latest` (warn): a `-beta` / `-rc` version with no `publishConfig.tag`.
|
|
14
|
+
npm publishes to `latest` whatever the suffix says, so `npm i <name>` starts handing everyone an
|
|
15
|
+
unfinished build and nothing warns.
|
|
16
|
+
- `publish-tag-not-latest` (warn): the version is on the registry but `dist-tags.latest` points at an
|
|
17
|
+
older one. That is what `--tag next` does when you meant to release: the release looks shipped and
|
|
18
|
+
is not what anyone installs.
|
|
19
|
+
- README: dropped a made-up `test/…` example path — `readme-path-missing` caught my own prose.
|
|
20
|
+
|
|
3
21
|
## 0.18.0
|
|
4
22
|
|
|
5
23
|
- `readme-path-missing` (warn): a README that names `test/…`, `lib/…`, `scripts/…` files that no longer
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
45
45
|
| `publish_gate_market` | 校验市场条目 YAML(url/name/category/description 双语);传 `dir` 则改查只有线上才能回答的三件事:这个仓库**已经在目录里**(再开 PR 就是重复条目)、另一个条目也在发同一个 npm 包、仓库还**不满 24 小时**(提交门禁会失败并自己重跑) |
|
|
46
46
|
| `publish_gate_next_version` | 当前版本、是否已被 registry 占用、建议 bump 到多少(本地比 npm 旧时从 npm 的 latest 往上加,不会推荐一个已被占用的版本) |
|
|
47
47
|
| `publish_gate_doctor` | 扫整个目录,按 blocker 数排序,附带每个插件的真实包体积,看哪个还不能发 |
|
|
48
|
-
| `publish_gate_commands` | 按顺序列出发布命令,并标出这台机器已经走到哪一步。命令是**这台机器上真能跑的形式**:Windows 下 `npm publish` 走 `cmd /c`(PowerShell 的 `npm.ps1` 常被执行策略拦掉),`npm publish` 后面紧跟一条 `publish_gate_verify
|
|
48
|
+
| `publish_gate_commands` | 按顺序列出发布命令,并标出这台机器已经走到哪一步。命令是**这台机器上真能跑的形式**:Windows 下 `npm publish` 走 `cmd /c`(PowerShell 的 `npm.ps1` 常被执行策略拦掉),`npm publish` 后面紧跟一条 `publish_gate_verify`;版本是预发版时命令自带 `--tag next`;最后一步是给发出去的 commit 打 tag |
|
|
49
49
|
|
|
50
50
|
## 检查清单
|
|
51
51
|
|
|
@@ -75,12 +75,14 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
75
75
|
| `no-license` | 缺 LICENSE |
|
|
76
76
|
| `version-taken` | 这个版本已经在 registry 上,再发是 403 |
|
|
77
77
|
| `version-behind-latest` | 本地版本比 registry 上的 `latest` **旧**。npm 会照收,但 `latest` 继续指向新版本——包上传了,按名字装的人永远拿不到,且没有任何报错 |
|
|
78
|
+
| `publish-tag-not-latest` | 版本已经在 registry 上了,但 `dist-tags.latest` 还指着旧的。`--tag next` 发出去的版本就是这样:看着发出去了,按名字装的人拿到的还是旧的 |
|
|
79
|
+
| `prerelease-published-as-latest` | 版本是 `-beta` / `-rc`,却没写 `publishConfig.tag`。npm 不看后缀,照样发到 `latest`——所有人 `npm i` 装到的都是没做完的构建 |
|
|
78
80
|
| `registry-unreachable` | registry 回了 500/429 —— 版本检查**没跑**,所以别把它当干净 |
|
|
79
81
|
| `npm-not-logged-in` | 这台机器上 npm 没有可用登录(`npm whoami` 报 ENEEDAUTH)。`npm publish` 会在 tarball 打好之后才失败 |
|
|
80
82
|
| `readme-wrong-package` | README 的安装命令写的是**另一个包名**(从别的插件抄过来没改)。照着装的人装到别的插件上,而且不会报错——那个包真的存在 |
|
|
81
83
|
|
|
82
84
|
### 值得在发布前知道的(warn / info)
|
|
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-path-missing`(README
|
|
85
|
+
`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-path-missing`(README 里点名的仓库内文件路径已经不存在了,照着敲就是 ENOENT)、`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-*` 全系列。
|
|
84
86
|
|
|
85
87
|
### 产物层:npm 到底会发出什么
|
|
86
88
|
|
|
@@ -233,8 +235,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
|
|
|
233
235
|
## 开发
|
|
234
236
|
|
|
235
237
|
```sh
|
|
236
|
-
node --test #
|
|
237
|
-
node test/integration.test.mjs #
|
|
238
|
+
node --test # 120 个引擎单测
|
|
239
|
+
node test/integration.test.mjs # 28 条集成断言(9 个真实插件作 fixture)
|
|
238
240
|
```
|
|
239
241
|
|
|
240
242
|
## License
|
package/lib/engine.js
CHANGED
|
@@ -191,6 +191,21 @@ export function checkPackageJson(dir) {
|
|
|
191
191
|
out.push(F('invalid-version', 'error', `version "${json.version}" is not semver`, 'npm publish rejects it.', 'Use MAJOR.MINOR.PATCH.'))
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
|
|
195
|
+
// npm publishes to `latest` unless told otherwise, and a prerelease version
|
|
196
|
+
// does not change that. `npm i <name>` then resolves to a beta and every user
|
|
197
|
+
// is on an unfinished build; nothing warns, and the author sees a green publish.
|
|
198
|
+
if (json.version && /^\d+\.\d+\.\d+-/.test(json.version) && (!json.publishConfig?.tag || json.publishConfig.tag === 'latest')) {
|
|
199
|
+
out.push(
|
|
200
|
+
F(
|
|
201
|
+
'prerelease-published-as-latest',
|
|
202
|
+
'warn',
|
|
203
|
+
`version "${json.version}" is a prerelease but it will be published to \`latest\``,
|
|
204
|
+
'npm publishes to `latest` by default and a `-beta`/`-rc` suffix does not change that, so `npm i <name>` starts handing everyone an unfinished build.',
|
|
205
|
+
'Publish with `npm publish --tag next`, or set publishConfig.tag to next.',
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
194
209
|
// type: module — the whole plugin surface is ESM
|
|
195
210
|
if (json.type !== 'module') {
|
|
196
211
|
out.push(
|
|
@@ -767,11 +782,27 @@ export async function checkVersionTaken(name, version, { registry = 'https://reg
|
|
|
767
782
|
)
|
|
768
783
|
}
|
|
769
784
|
|
|
785
|
+
|
|
786
|
+
// A publish with `--tag next` leaves `latest` where it was: the new version
|
|
787
|
+
// is on the registry, `npm i <name>` still gets the old one, and nothing
|
|
788
|
+
// fails. The release looks shipped and is not what anyone installs.
|
|
789
|
+
const latestTag = json?.['dist-tags']?.latest
|
|
790
|
+
if (json?.versions?.[version] && latestTag && latestTag !== version) {
|
|
791
|
+
out.push(
|
|
792
|
+
F(
|
|
793
|
+
'publish-tag-not-latest',
|
|
794
|
+
'warn',
|
|
795
|
+
`${name}@${version} is published but \`latest\` still points at ${latestTag}`,
|
|
796
|
+
'Installing by name keeps getting the old version. This is what `--tag next` (or publishConfig.tag) does when you meant to release.',
|
|
797
|
+
`Move it: npm dist-tag add ${name}@${version} latest`,
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
|
+
}
|
|
770
801
|
// npm accepts a publish of an OLDER version without a word. It lands in
|
|
771
802
|
// `versions`, but `dist-tags.latest` keeps pointing at the newer one, so
|
|
772
803
|
// anyone installing by name never receives it. The tarball is uploaded and
|
|
773
804
|
// invisible — which reads exactly like "my update did not reach anyone".
|
|
774
|
-
const latest =
|
|
805
|
+
const latest = latestTag
|
|
775
806
|
if (latest && compareVersions(version, latest) < 0) {
|
|
776
807
|
out.push(
|
|
777
808
|
F(
|
|
@@ -2564,7 +2595,7 @@ export function runStaticChecks(dir) {
|
|
|
2564
2595
|
}
|
|
2565
2596
|
|
|
2566
2597
|
const RULE_ORDER = [
|
|
2567
|
-
'missing-file', 'unreadable', 'utf8-bom', 'json-parse', 'yaml-bom', 'invalid-name', 'invalid-version',
|
|
2598
|
+
'missing-file', 'unreadable', 'utf8-bom', 'json-parse', 'yaml-bom', 'invalid-name', 'invalid-version', 'prerelease-published-as-latest',
|
|
2568
2599
|
'missing-field', 'not-esm', 'files-omits-lib', 'files-omits-patch', 'no-exports', 'client-without-export', 'client-without-platform',
|
|
2569
2600
|
'entry-path-missing', 'bundle-patch-not-declared', 'no-publish-config-access', 'private-field-set', 'publish-config-registry',
|
|
2570
2601
|
'client-file-missing', 'client-no-module-loader', 'client-parse-failed', 'client-load-not-called',
|
|
@@ -2581,7 +2612,7 @@ const RULE_ORDER = [
|
|
|
2581
2612
|
'undeclared-import', 'dep-in-dev-only', 'unused-dependency',
|
|
2582
2613
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
2583
2614
|
'pack-unexpected-dir', 'pack-ships-junk', 'pack-script-build', 'pack-unavailable',
|
|
2584
|
-
'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'npm-not-logged-in', 'npm-scope-mismatch', 'npm-whoami-unavailable', 'registry-unreachable', 'publish-not-served',
|
|
2615
|
+
'no-license', 'license-mismatch', 'version-taken', 'version-behind-latest', 'publish-tag-not-latest', 'npm-not-logged-in', 'npm-scope-mismatch', 'npm-whoami-unavailable', 'registry-unreachable', 'publish-not-served',
|
|
2585
2616
|
'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
|
|
2586
2617
|
'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', 'readme-path-missing',
|
|
2587
2618
|
'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new', 'market-release-behind',
|
package/lib/index.js
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
compareVersions,
|
|
29
29
|
latestPublished,
|
|
30
30
|
renderReport,
|
|
31
|
-
runArtifactChecks,
|
|
32
|
-
checkTestRun,
|
|
31
|
+
runArtifactChecks,
|
|
32
|
+
checkTestRun,
|
|
33
33
|
straySeverities,
|
|
34
34
|
runStaticChecks,
|
|
35
35
|
} from './engine.js'
|
|
@@ -104,7 +104,7 @@ export function apply(ctx, config = {}) {
|
|
|
104
104
|
skipNetwork: { type: 'boolean', description: 'Skip the npm registry and GitHub checks (default false).' },
|
|
105
105
|
skipPack: { type: 'boolean', description: 'Skip `npm pack --dry-run` (default false). Faster, but says nothing about the real tarball.' },
|
|
106
106
|
skipTools: { type: 'boolean', description: 'Skip the isolated import of the plugin entry (default false).' },
|
|
107
|
-
skipClient: { type: 'boolean', description: 'Skip the isolated load of the browser bundle (default false).' },
|
|
107
|
+
skipClient: { type: 'boolean', description: 'Skip the isolated load of the browser bundle (default false).' },
|
|
108
108
|
skipTests: { type: 'boolean', description: 'Skip running the test suite (default false). The suite is the only check that reads the code rather than the paperwork.' },
|
|
109
109
|
},
|
|
110
110
|
},
|
|
@@ -114,21 +114,21 @@ export function apply(ctx, config = {}) {
|
|
|
114
114
|
const findings = runStaticChecks(dir)
|
|
115
115
|
const artifact = await runArtifactChecks(dir, { skipPack: args.skipPack, skipTools: args.skipTools, skipClient: args.skipClient })
|
|
116
116
|
findings.push(...artifact.findings)
|
|
117
|
-
const tests = args.skipTests ? null : await checkTestRun(dir, { timeoutMs: cfg.testTimeoutMs })
|
|
118
|
-
if (tests) findings.push(...tests)
|
|
117
|
+
const tests = args.skipTests ? null : await checkTestRun(dir, { timeoutMs: cfg.testTimeoutMs })
|
|
118
|
+
if (tests) findings.push(...tests)
|
|
119
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
|
-
}
|
|
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
|
+
}
|
|
132
132
|
const counts = {
|
|
133
133
|
error: findings.filter((f) => f.severity === 'error').length,
|
|
134
134
|
warn: findings.filter((f) => f.severity === 'warn').length,
|
|
@@ -412,6 +412,11 @@ export function apply(ctx, config = {}) {
|
|
|
412
412
|
const repo = json.repository?.url?.match(/github\.com[:/]([^/]+)\/([^/.]+)/)?.[2] || json.name.replace(/^@[^/]+\//, '')
|
|
413
413
|
const { existsSync } = await import('node:fs')
|
|
414
414
|
const hasGit = existsSync(join(dir, '.git'))
|
|
415
|
+
// npm publishes to `latest` whatever the version suffix says, so a
|
|
416
|
+
// prerelease needs an explicit tag or it goes out to everyone.
|
|
417
|
+
const preTag = /^\d+\.\d+\.\d+-/.test(json.version || '')
|
|
418
|
+
const tag = preTag ? (json.publishConfig?.tag && json.publishConfig.tag !== 'latest' ? json.publishConfig.tag : 'next') : null
|
|
419
|
+
const publishCmd = `npm publish --access public${tag ? ` --tag ${tag}` : ''}`
|
|
415
420
|
const steps = [
|
|
416
421
|
{
|
|
417
422
|
name: 'git init and commit',
|
|
@@ -433,7 +438,7 @@ export function apply(ctx, config = {}) {
|
|
|
433
438
|
},
|
|
434
439
|
{
|
|
435
440
|
name: 'npm publish',
|
|
436
|
-
command: process.platform === 'win32' ?
|
|
441
|
+
command: process.platform === 'win32' ? `cmd /c "${publishCmd}"` : publishCmd,
|
|
437
442
|
done: false,
|
|
438
443
|
note:
|
|
439
444
|
`a bare (unscoped) name can return 403 with a granular token — "${json.name}" is ${json.name.startsWith('@') ? 'scoped, fine' : 'UNSCOPED, rename it'}` +
|
|
@@ -447,7 +452,14 @@ export function apply(ctx, config = {}) {
|
|
|
447
452
|
command: `publish_gate_verify({ dir: "${dir.replace(/\\/g, '\\\\')}" })`,
|
|
448
453
|
done: false,
|
|
449
454
|
note: 'the registry answers 202 and then 404s for about a minute, so a mismatch right after publish is normal; the shasum comparison is exact once it settles',
|
|
450
|
-
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'tag the shipped commit',
|
|
458
|
+
command: `git tag v${json.version} && git push origin v${json.version}`,
|
|
459
|
+
done: false,
|
|
460
|
+
note: 'a GitHub release, a changelog entry and the market listing all point at a commit — none of them can point at one that was never tagged',
|
|
461
|
+
},
|
|
462
|
+
{
|
|
451
463
|
name: 'market entry',
|
|
452
464
|
command: `write data/plugins/${owner}__${repo}.yml with url / name / category / description{en,zh}`,
|
|
453
465
|
done: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bushizhanghan/dsh-publish-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|