@bushizhanghan/dsh-publish-gate 0.12.0 → 0.13.1

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1
4
+
5
+ - The tag check now asks `git ls-remote origin` instead of `git tag --points-at HEAD`. When the commit is pushed
6
+ through the GitHub API the local HEAD is not the remote one, so the local form reported `git-tag-missing` on
7
+ seven of my nine plugins whose tag existed all along. It now compares the tag against the shipped branch
8
+ head and stays silent when the remote cannot be reached — a check that cannot see the truth must not claim
9
+ a defect.
10
+ ## 0.13.0
11
+
12
+ - `market-fork-of-listed` / `market-fork-undisclosed` (warn): the repo is a GitHub fork whose parent is
13
+ already in the catalogue, and nothing says so. A submission was blocked for exactly this: the entry
14
+ described the fork as if it were original work, and both plugins claim the same provider id, so installing
15
+ both silently shadows one of them. The check uses the `fork` and `parent.full_name` fields of the repo
16
+ metadata the market layer already fetches.
3
17
  ## 0.12.0
4
18
 
5
19
  - `pack-ships-junk` (warn): the tarball carries `.bak-*`, `.orig`, `.swp`, `.log`, `~` or `.DS_Store` debris.
package/README.md CHANGED
@@ -141,6 +141,8 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
141
141
  | `market-duplicate-entry` | 这个仓库**已经在目录里**。再提一个 PR 就是同一仓库的第二条条目,reviewer 会拒。版本升级根本不需要 PR——目录条目只存 `url` |
142
142
  | `market-npm-name-collision` | 另一个条目也在发同一个 npm 包,安装路径会互相盖 |
143
143
  | `market-repo-too-new` | 仓库创建不满 24 小时。门禁会报 `0.0 days old` 然后**自己重跑**,PR 不会丢,但在那之前什么都不会合 |
144
+ | `market-fork-of-listed` | 这个仓库是某个**已经在目录里**的仓库的 fork。两条条目装的代码抢同一批 id,两个都装就有一个被静默盖掉——而且 reviewer 会把"没写"当成"隐瞒 fork" |
145
+ | `market-fork-undisclosed` | 是 fork,但 README 和 package.json 里都没有上游的 `owner/repo` |
144
146
  | `market-no-repo` | `package.json` 里没有 GitHub 地址,没东西可提交 |
145
147
  | `market-unavailable` | 目录拉不到(warn)——这几条没跑,别把它当干净 |
146
148
 
@@ -221,7 +223,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
221
223
  ## 开发
222
224
 
223
225
  ```sh
224
- node --test # 104 个引擎单测
226
+ node --test # 108 个引擎单测
225
227
  node test/integration.test.mjs # 27 条集成断言(9 个真实插件作 fixture)
226
228
  ```
227
229
 
package/lib/engine.js CHANGED
@@ -931,7 +931,7 @@ export async function checkMarketReadiness(dir, opts = {}) {
931
931
  if (hours < 24) {
932
932
  out.push(
933
933
  F(
934
- 'market-repo-too-new',
934
+ 'market-repo-too-new', 'market-fork-of-listed', 'market-fork-undisclosed',
935
935
  'warn',
936
936
  `the repo is ${hours.toFixed(1)}h old; the submission gate needs 24h`,
937
937
  '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.',
@@ -941,12 +941,45 @@ export async function checkMarketReadiness(dir, opts = {}) {
941
941
  }
942
942
  }
943
943
  }
944
+ // A fork of a repo that is already listed collides with its own upstream:
945
+ // both register the same provider or tool names, and whichever loads second
946
+ // wins or throws. A reviewer blocked a submission for exactly this and the
947
+ // description said nothing about it.
948
+ const parentSlug = typeof repoInfo?.parent?.full_name === 'string' ? repoInfo.parent.full_name : null
949
+ if (repoInfo?.fork === true && parentSlug) {
950
+ const parentListed = entries.some((e) => typeof e?.url === 'string' && e.url.toLowerCase().includes(parentSlug.toLowerCase()))
951
+ if (parentListed) {
952
+ out.push(
953
+ F(
954
+ 'market-fork-of-listed',
955
+ 'warn',
956
+ `this repo is a fork of ${parentSlug}, which is already in the catalogue`,
957
+ 'Both entries install code that claims the same ids, so a user who installs both gets one silently shadowing the other — and a reviewer reads the missing note as an undisclosed fork.',
958
+ `Say in the entry description that it is a fork of ${parentSlug} and that the two must not be installed together.`,
959
+ ),
960
+ )
961
+ }
962
+ const readme = safeText(join(dir, 'README.md')) || ''
963
+ const desc = typeof json?.description === 'string' ? json.description : ''
964
+ if (!readme.includes(parentSlug) && !desc.includes(parentSlug)) {
965
+ out.push(
966
+ F(
967
+ 'market-fork-undisclosed',
968
+ 'warn',
969
+ `nothing in README or package.json names the upstream ${parentSlug}`,
970
+ 'A fork that does not say what it forked from looks like original work until someone diffs it, and the upstream author finds out from a collision report.',
971
+ `Mention ${parentSlug} in the README and in the market entry description.`,
972
+ ),
973
+ )
974
+ }
975
+ }
944
976
  return out
945
977
  }
946
978
 
947
979
  // =====================================================================
948
980
  // Source sanity: the entry protocol
949
981
  // =====================================================================
982
+
950
983
  export function checkEntrySource(dir) {
951
984
  const out = []
952
985
  const path = join(dir, 'lib', 'index.js')
@@ -2270,11 +2303,43 @@ export async function checkTraceability(dir, opts = {}) {
2270
2303
  }
2271
2304
  }
2272
2305
 
2273
- // 2. No tag on HEAD: nothing points at these bytes afterwards.
2306
+ // 2. A tag on the bytes that actually shipped. When the commit was pushed
2307
+ // through the GitHub API the local HEAD is not the remote one, so a local
2308
+ // `git tag --points-at HEAD` reports a missing tag that exists — ask the
2309
+ // remote instead, and say nothing when it cannot be reached.
2310
+ const remote = opts.remote === undefined ? await readRemoteRefs(dir) : opts.remote
2274
2311
  const atHead = opts.atHead === undefined ? await gitOut(dir, ['tag', '--points-at', 'HEAD']) : opts.atHead
2275
- const tags = typeof atHead === 'string' ? atHead.split('\n').map((t) => t.trim()).filter(Boolean) : []
2312
+ const localTags = typeof atHead === 'string' ? atHead.split('\n').map((t) => t.trim()).filter(Boolean) : []
2276
2313
  const wanted = [`v${version}`, version]
2277
- if (typeof atHead === 'string' && !wanted.some((t) => tags.includes(t))) {
2314
+ const tagOnRemote = remote ? remote.tags[`v${version}`] || remote.tags[version] || null : null
2315
+ const tagIsOnShipped = Boolean(remote && tagOnRemote && tagOnRemote === remote.head)
2316
+ const localHasIt = wanted.some((t) => localTags.includes(t))
2317
+
2318
+ if (remote) {
2319
+ // The remote is the truth: the tag either points at the shipped commit or not.
2320
+ if (tagIsOnShipped) return out
2321
+ const exists = Boolean(tagOnRemote) || (opts.tagExists !== undefined ? Boolean(opts.tagExists) : Boolean(await gitOut(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/v${version}`])))
2322
+ out.push(
2323
+ exists
2324
+ ? F(
2325
+ 'git-tag-elsewhere',
2326
+ 'warn',
2327
+ `tag v${version} does not point at the shipped commit`,
2328
+ `The tag is on ${String(tagOnRemote || 'another commit').slice(0, 12)} but the branch head is ${String(remote.head).slice(0, 12)}. A release built from that tag is not what you just published.`,
2329
+ `Move it: git tag -f v${version} <shipped-sha> && git push -f origin v${version}`,
2330
+ )
2331
+ : F(
2332
+ 'git-tag-missing',
2333
+ 'warn',
2334
+ `no git tag v${version} on the shipped commit`,
2335
+ 'After publishing, the only record of these bytes is the registry. A GitHub release, a changelog entry or a market listing cannot point at a commit that was never tagged.',
2336
+ `git tag v${version} && git push origin v${version}`,
2337
+ ),
2338
+ )
2339
+ return out
2340
+ }
2341
+
2342
+ if (typeof atHead === 'string' && !localHasIt) {
2278
2343
  const exists = opts.tagExists === undefined ? await gitOut(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/v${version}`]) : opts.tagExists ? 'sha' : null
2279
2344
  out.push(
2280
2345
  exists
@@ -2297,6 +2362,22 @@ export async function checkTraceability(dir, opts = {}) {
2297
2362
  return out
2298
2363
  }
2299
2364
 
2365
+ /** `{ head, tags }` from one `git ls-remote`, or null when it cannot be read. */
2366
+ async function readRemoteRefs(dir) {
2367
+ const raw = await gitOut(dir, ['ls-remote', '--heads', '--tags', 'origin'])
2368
+ if (typeof raw !== 'string' || raw.length === 0) return null
2369
+ const tags = {}
2370
+ let head = null
2371
+ for (const line of raw.split('\n')) {
2372
+ const [sha, ref] = line.split('\t')
2373
+ if (!sha || !ref) continue
2374
+ if (ref === 'refs/heads/main' || ref === 'refs/heads/master') head = sha
2375
+ const m = /^refs\/tags\/(.+?)(?:\^\{\})?$/.exec(ref)
2376
+ if (m) tags[m[1]] = sha // a dereferenced annotated tag overwrites the tag object, which is what we want
2377
+ }
2378
+ return head ? { head, tags } : null
2379
+ }
2380
+
2300
2381
  // =====================================================================
2301
2382
  // Orchestration
2302
2383
  // =====================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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",