@bushizhanghan/dsh-publish-gate 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/lib/engine.js +89 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -161,6 +161,20 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
161
161
 
162
162
  两条都只是 warn:有可能你就是要发手改的产物(我干过)。但发之前你得知道自己在发什么。
163
163
 
164
+ ### 可追溯性:一周后你还能指出线上那包是哪份代码吗
165
+
166
+ 发布是字节唯一一次离开你机器的时刻。如果这些字节既不在任何 commit 里、也没有 tag,那么一周后——包括你自己在内——没人说得清 registry 上那包是从哪份源码来的。
167
+
168
+ 这不是假想:我发过一版 `1.2.7` 到 npm,仓库的 GitHub release 还停在 `1.2.6`,市场条目指向旧 tarball,维护者直接打回。
169
+
170
+ | 规则 | 抓什么 |
171
+ |---|---|
172
+ | `git-uncommitted` | 会进 tarball 的文件有未提交改动。发出去的字节不在任何 commit 里,以后复现不出来、也没法 bisect |
173
+ | `git-tag-missing` | HEAD 上没有 `v<version>` tag。发完之后这些字节的唯一记录就是 registry,GitHub release、changelog、市场条目都没法指向一个从没打过 tag 的 commit |
174
+ | `git-tag-elsewhere` | tag 存在但不在 HEAD 上:tag 和 tarball 是两份不同的字节,从那个 tag 构建出来的东西不是你刚发的东西 |
175
+
176
+ 都是 warn:本地试发、私有包、发完再补 tag 都合理。但「发完之后指不出是哪份代码」这件事,事后补不回来。
177
+
164
178
  ## 一个真实的例子
165
179
 
166
180
  对一个刚从别人那儿接手、准备发布的插件跑 `publish_gate`:
@@ -196,7 +210,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
196
210
  ## 开发
197
211
 
198
212
  ```sh
199
- node --test # 75 个引擎单测
213
+ node --test # 80 个引擎单测
200
214
  node test/integration.test.mjs # 26 条集成断言(9 个真实插件作 fixture)
201
215
  ```
202
216
 
package/lib/engine.js CHANGED
@@ -1678,8 +1678,12 @@ export async function runArtifactChecks(dir, opts = {}) {
1678
1678
  const toolResult = opts.skipTools ? { findings: [], tools: null } : await checkToolRegistrations(dir, opts)
1679
1679
  const clientResult = opts.skipClient ? { findings: [], client: null } : await checkClientRuntime(dir, opts)
1680
1680
  const names = toolResult.tools ? toolResult.tools.map((t) => t.name).filter(Boolean) : null
1681
+ const trace = await checkTraceability(dir, {
1682
+ packFiles: packResult.pack?.files?.map((f) => f.path) || [],
1683
+ ...opts,
1684
+ })
1681
1685
  return {
1682
- findings: [...packResult.findings, ...toolResult.findings, ...clientResult.findings, ...checkReadmeTruth(dir, names)],
1686
+ findings: [...packResult.findings, ...toolResult.findings, ...clientResult.findings, ...checkReadmeTruth(dir, names), ...trace],
1683
1687
  pack: packResult.pack,
1684
1688
  tools: toolResult.tools,
1685
1689
  client: clientResult.client,
@@ -1797,6 +1801,89 @@ export function checkDependencies(dir) {
1797
1801
  return out
1798
1802
  }
1799
1803
 
1804
+ // =====================================================================
1805
+ // Traceability: can you point at the exact bytes you are publishing?
1806
+ // =====================================================================
1807
+
1808
+ const gitOut = async (dir, args) => {
1809
+ try {
1810
+ const { stdout } = await runFile('git', ['-C', dir, ...args], { timeout: 8000, windowsHide: true })
1811
+ return String(stdout).trim()
1812
+ } catch {
1813
+ return null
1814
+ }
1815
+ }
1816
+
1817
+ /**
1818
+ * Publishing is the one moment where the bytes leave your machine. If they are
1819
+ * not in a commit and not under a tag, then a week later nobody — including you —
1820
+ * can say which source produced the live tarball. This really happened: a 1.2.7
1821
+ * went to npm while the repo's release still said 1.2.6, and the marketplace
1822
+ * entry pointed at the old tarball.
1823
+ *
1824
+ * `status` / `atHead` / `tagExists` inject the git answers so the rules are
1825
+ * testable without a repository.
1826
+ */
1827
+ export async function checkTraceability(dir, opts = {}) {
1828
+ const out = []
1829
+ const json = readPkgJson(dir)
1830
+ const version = typeof opts.version === 'string' ? opts.version : json?.version
1831
+ if (!version) return out
1832
+
1833
+ const inside = opts.inside ?? (opts.status === undefined ? await gitOut(dir, ['rev-parse', '--is-inside-work-tree']) : 'true')
1834
+ if (inside !== 'true') return out // checkGitState already reported no-git
1835
+
1836
+ // 1. Dirty tree: the tarball carries bytes that exist in no commit.
1837
+ const status = opts.status === undefined ? await gitOut(dir, ['status', '--porcelain']) : opts.status
1838
+ if (typeof status === 'string') {
1839
+ const packed = new Set((opts.packFiles || []).map((p) => String(p).replace(/\\/g, '/')))
1840
+ const dirty = status
1841
+ .split('\n')
1842
+ .map((line) => line.slice(3).trim())
1843
+ .map((p) => (p.includes(' -> ') ? p.split(' -> ').pop().trim() : p))
1844
+ .filter(Boolean)
1845
+ // Without a tarball listing, lib/ is the honest proxy: that is the code.
1846
+ const relevant = packed.size ? dirty.filter((p) => packed.has(p)) : dirty.filter((p) => p.startsWith('lib/'))
1847
+ if (relevant.length) {
1848
+ out.push(
1849
+ F(
1850
+ 'git-uncommitted',
1851
+ 'warn',
1852
+ `${relevant.length} file(s) going into the tarball are not committed`,
1853
+ `The published bytes exist in no commit, so they can never be reproduced or bisected later. First: ${relevant.slice(0, 3).join(', ')}.`,
1854
+ 'Commit (or stash) them before publishing.',
1855
+ ),
1856
+ )
1857
+ }
1858
+ }
1859
+
1860
+ // 2. No tag on HEAD: nothing points at these bytes afterwards.
1861
+ const atHead = opts.atHead === undefined ? await gitOut(dir, ['tag', '--points-at', 'HEAD']) : opts.atHead
1862
+ const tags = typeof atHead === 'string' ? atHead.split('\n').map((t) => t.trim()).filter(Boolean) : []
1863
+ const wanted = [`v${version}`, version]
1864
+ if (typeof atHead === 'string' && !wanted.some((t) => tags.includes(t))) {
1865
+ const exists = opts.tagExists === undefined ? await gitOut(dir, ['rev-parse', '--verify', '--quiet', `refs/tags/v${version}`]) : opts.tagExists ? 'sha' : null
1866
+ out.push(
1867
+ exists
1868
+ ? F(
1869
+ 'git-tag-elsewhere',
1870
+ 'warn',
1871
+ `tag v${version} exists but does not point at HEAD`,
1872
+ 'The tag and the tarball are different bytes. A release built from that tag would not be what you just published.',
1873
+ `Move it: git tag -f v${version} && git push -f origin v${version}`,
1874
+ )
1875
+ : F(
1876
+ 'git-tag-missing',
1877
+ 'warn',
1878
+ `no git tag v${version} on HEAD`,
1879
+ '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.',
1880
+ `git tag v${version} && git push origin v${version}`,
1881
+ ),
1882
+ )
1883
+ }
1884
+ return out
1885
+ }
1886
+
1800
1887
  // =====================================================================
1801
1888
  // Orchestration
1802
1889
  // =====================================================================
@@ -1840,6 +1927,7 @@ const RULE_ORDER = [
1840
1927
  'publish-served-mismatch', 'publish-served', 'market-empty', 'market-no-url', 'market-url-not-github',
1841
1928
  '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',
1842
1929
  'market-no-repo', 'market-unavailable', 'market-duplicate-entry', 'market-npm-name-collision', 'market-repo-too-new',
1930
+ 'git-uncommitted', 'git-tag-missing', 'git-tag-elsewhere',
1843
1931
  'readme-tool-undocumented', 'readme-test-count',
1844
1932
  'no-tests', 'no-test-files', 'no-test-script', 'no-engines', 'bad-repo-url', 'no-gitignore',
1845
1933
  ]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.4.0",
4
- "description": "Pre-publish gate for DSH plugins, in five layers: manifest, dependencies, artifact, runtime and the browser half. 发布前的五层门禁:清单、依赖、产物、运行时、浏览器半边。",
3
+ "version": "0.5.0",
4
+ "description": "Pre-publish gate for DSH plugins in five layers: manifest, dependencies, artifact, runtime, browser half — plus traceability (can you point at the bytes you shipped?). 发布前门禁:清单、依赖、产物、运行时、浏览器半边,外加可追溯性。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {