@lqc123qwe/car-runtime 1.2.0 → 1.3.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.
@@ -4,10 +4,17 @@ on:
4
4
  release:
5
5
  types: [published]
6
6
  permissions:
7
- contents: read
7
+ # 1.3-S3(W4-2):contents 提升为 write——gh release upload 资产上传需要;
8
+ # id-token: write 为 keyless cosign(OIDC + Rekor)凭据源(D-15 正式轨)
9
+ contents: write
8
10
  id-token: write
9
- # J-16 release-merge(M2 §2.3):tag/Release 触发 → npm publish --provenance。
10
- # SBOM/SHA256SUMS/cosign 由 scripts/release-pipeline.ts 承载(本地/沙箱外轨,M3 §2.1.2 单源编译已入 CI j01)。
11
+ # J-16 release-merge(M2 §2.3)+ 1.3 W4 全自动化:
12
+ # 构建制品(release-artifacts.ts——与本地 release-pipeline.ts 单一事实源,R-3)
13
+ # → cosign keyless 签名 tgz(OIDC + Rekor 透明日志;私钥不出本地,D-15 正式轨)
14
+ # → 上传五件套到 Release(gh release upload --clobber;消除 1.0 错挂/1.2 树漂移的人工拖拽面)
15
+ # → npm publish --provenance(原步不动)。
16
+ # 步骤序红线:upload 先于 publish——上传失败 job 即红,publish 不带病出街。
17
+ # 回退预案(R-1,演练预案非静默):keyless 签名/上传失败时可临时人工拖拽资产,事后修复重发。
11
18
  jobs:
12
19
  j16-release-merge:
13
20
  runs-on: ubuntu-latest
@@ -17,6 +24,24 @@ jobs:
17
24
  with:
18
25
  node-version: 22
19
26
  registry-url: https://registry.npmjs.org/
27
+ - name: Build release artifacts (single-source with local pipeline, W4-1)
28
+ run: node --experimental-transform-types scripts/release-artifacts.ts
29
+ - name: Install cosign (pinned, R-4)
30
+ uses: sigstore/cosign-installer@v3.7.0
31
+ - name: Keyless sign tarball (OIDC + Rekor, D-15 formal track)
32
+ run: |
33
+ VERSION=$(node -p "require('./package.json').version")
34
+ cosign sign-blob --yes --bundle "dist/car-runtime-${VERSION}.tgz.sig.bundle" "dist/car-runtime-${VERSION}.tgz"
35
+ echo "{\"stage\":\"cosign-keyless\",\"detail\":\"signed car-runtime-${VERSION}.tgz (keyless OIDC + Rekor)\",\"ts\":$(date +%s%3N)}" >> dist/release-audit.jsonl
36
+ - name: "Upload release assets (W4 - zero manual drag-and-drop)"
37
+ env:
38
+ GH_TOKEN: ${{ github.token }}
39
+ run: |
40
+ VERSION=$(node -p "require('./package.json').version")
41
+ for f in dist/car-runtime-${VERSION}.tgz dist/car-runtime-${VERSION}.tgz.sig.bundle dist/SHA256SUMS dist/sbom.json dist/release-audit.jsonl; do
42
+ # 上传目标 = 包版本号(D-14 裸 tag 形态:release tag == version);dispatch 触发亦可用
43
+ gh release upload "$VERSION" "$f" --clobber
44
+ done
20
45
  - name: Publish to npm with provenance
21
46
  # dist-tag 按版本形态动态选择:prerelease(如 1.0.0-rc.1)→ rc;稳定版(1.0.0)→ latest。
22
47
  # M5 §1.1 口径:1.0 发布时 latest→1.0.0,rc tag 保留指向 rc.1 不回指——稳定版禁止 --tag rc。
package/README.md CHANGED
@@ -179,10 +179,13 @@ node --experimental-transform-types --test test/*.spec.ts
179
179
  - **1.2 工程收口(2026-09-27)**:M7 §4/§5 + M8 §4.4 登记后续三子项全部出清。全量 **263(260 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 错 + secrets-scan 101 文件 0 命中。QA 两轮制执行。**1.2-BUG-1(OTLP metric 名混 labels)为生产调用点核查直接产出**——meter 桥接入即暴露 M8 最小实现掩盖的缺陷。
180
180
  - **边界口径(防外推)**:OTel span 面(turn/step span)**仍无生产调用点**(随 W1 登记后续);trace 传播(parentSpanId/共享 trace)随 W1 设计;mcp-serve 出站现仅 metrics(counter 桥)。
181
181
  - **遗留登记**:W1 = 1.3 头号候选(含 D-12a TLS loopback 豁免实装、car run 装配面重做〔demo_tool 撞名观察项出清〕);car run demo_tool 撞名 DEPCYCLE 为预存在边缘(登记不修,W1 出清)。规划:`car-docs/10-内核v1/1.2-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.2-收口终验报告.md`。
182
+ - **1.2.0 正式发布(2026-09-27,S5 收官)**:npm `@lqc123qwe/car-runtime@1.2.0` 在架——**dist-tag latest → 1.2.0,rc 保留指向 1.0.0-rc.1**;SLSA provenance(slsa.dev/provenance/v1)。Release `1.2.0`(用户创建,六资产)→ **j16 run 36312617477 success** → **post-publish REMOTE_CHECK=1 复跑 12 PASS / 1 DRY-RUN / 0 FAIL(G-09 实查 PASS)** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH `6d6d4da1…` + cosign Verified OK)。发布工程事件:registry CDN 缓存滞后登记(cache-buster 口径);制品自 tag 树重建后再挂(消除 1.1 的树漂移)。
183
+
184
+ - **1.3 发布资产自动化(W4,2026-09-27 收口)**:`scripts/release-artifacts.ts` 制品构建单一事实源(本地流水线/CI j16 双端复用;SHA256SUMS 裸字节自检 G-11 口径 + SBOM G-05 同构 + releaseAssetNames 五件套清单——G-10/CI/测试三方同源)+ release.yml 重构(构建 → cosign keyless 签名〔D-15 正式轨:OIDC+Rekor,CI 零 keypair 材料〕→ `gh release upload --clobber` 五件套 → npm publish 原步不动;**upload 先于 publish**)+ D-14 资产集迁移(car-release.pub 移出——keyless 验证锚 = bundle 证书 + Rekor)+ D-14 tag 形态统一(裸版本号)。s30 六测(构建/清单/同源/yml 结构与步骤序红线)。全量 **269(266 PASS + 3 skip / 0 fail)**。1.3.0 发布 = W4 验收载体(**全链零人工拖拽资产**)。W1 三度延后登记 **1.4 头号候选**(设计账本 D-12a loopback 豁免 + D-13 --demo 保留)。规划:`car-docs/10-内核v1/1.3-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.3-收口终验报告.md`。
182
185
 
183
186
  ## 状态
184
187
 
185
- - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 发布编排中)**
188
+ - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 269(266 PASS + 3 skip,2026-09-27 收口,发布全链去人工拖拽)**
186
189
  - **1.0.0 正式发布(2026-09-26,S29/S30)**:npm `@lqc123qwe/car-runtime@1.0.0` 在架——**dist-tag latest → 1.0.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(Actions OIDC,透明日志在案);GitHub Release v1.0.0 六资产(tgz/bundle/SHA256SUMS/SBOM/审计/公钥);**post-publish 全链 13 PASS / 0 DRY-RUN / 0 FAIL** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)。**宽限期时钟:2026-09-26 起算,2026-12-25 到期 verifier 默认值 warn→enforce 翻转(计划内变更)**。终验报告:`car-docs/10-内核v1/M5-S30-1.0收口终验报告.md`。发布工程事件:j16 首跑 E403(npm 2FA 策略 vs granular token 无 bypass——清单预设回退路径兑现)→ bypass token re-run 绿;G-09 期望 tag 动态化(`4c84b17`)在真实发布兑现价值。
187
190
  - **1.0.0-rc.1 已发布(2026-09-09)**:npm `@lqc123qwe/car-runtime@1.0.0-rc.1`(rc + latest 双 tag,SLSA provenance 在案)+ GitHub Release 6 制品;发布预演 **13 PASS / 0 DRY-RUN / 0 FAIL**(c762b72)。S22 GO 清单已全绿收口。
188
191
  - **轨道 A 三项全部完成**:E-1 WSL2 逃逸矩阵 **20/20 PASS 零逃逸**(2026-09-07,G-07 转 PASS)| E-3 远端发布通道 **13/13 全门禁 PASS**(2026-09-09,G-08/09/10 转 PASS)| E-2 Windows koffi FFI **四判据双环境全 PASS**(S24/S25/S26,2026-09-09~10;本机非提权 + windows-latest 提权 runner 双绿,CI 门禁已接入)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 1.3-S1 · 发布制品构建(W4):本地 release-pipeline.ts 与 CI j16 双端复用的单一事实源
3
+ *
4
+ * 口径(1.3 规划 W4-1/W4-4 / D-14 / D-15):
5
+ * - 同一仓树 → 逐字节同制品(git archive HEAD;G-11 教训:哈希对裸字节求);
6
+ * - **资产集五件套**(releaseAssetNames 单一事实源——G-10 need-list、CI upload、测试三方同源):
7
+ * car-runtime-<v>.tgz / car-runtime-<v>.tgz.sig.bundle / SHA256SUMS / sbom.json / release-audit.jsonl
8
+ * car-release.pub 自 1.3 移除(D-14:keyless 下验证锚 = bundle 证书 + Rekor,不再是本地公钥);
9
+ * - **签名不在此层**(D-15 双轨):bundle 由签名轨产出——本地 keypair no-tlog(预演态,
10
+ * release-pipeline G-08)/ CI keyless OIDC+Rekor(正式态,j16);本脚本只构建与审计;
11
+ * - 构建审计:CLI 入口(CI 调用形态)写 release-audit.jsonl(archive/sbom/sha256 记录);
12
+ * 程序化调用(pipeline)经 onRecord 接入调用方审计流。
13
+ * - 零依赖(node:crypto/fs/child_process)。
14
+ */
15
+ import { execSync } from 'node:child_process'
16
+ import { createHash } from 'node:crypto'
17
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
18
+ import { join } from 'node:path'
19
+ import { pathToFileURL } from 'node:url'
20
+
21
+ export interface BuildRecord { stage: string; detail: string; ts: number }
22
+
23
+ export interface BuildResult {
24
+ tgzPath: string
25
+ sha256: string
26
+ sumsPath: string
27
+ sbomPath: string
28
+ components: number
29
+ bytes: number
30
+ records: BuildRecord[]
31
+ }
32
+
33
+ const sha256 = (s: string | Buffer) => createHash('sha256').update(s).digest('hex')
34
+
35
+ function walk(dir: string, ext: string): string[] {
36
+ let out: string[] = []
37
+ for (const name of readdirSync(dir)) {
38
+ const p = join(dir, name)
39
+ if (statSync(p).isDirectory()) out = out.concat(walk(p, ext))
40
+ else if (p.endsWith(ext)) out.push(p)
41
+ }
42
+ return out
43
+ }
44
+
45
+ /** 资产集清单(五件套;D-14:car-release.pub 移除,签名 bundle 由签名轨产出后入列) */
46
+ export function releaseAssetNames(version: string): string[] {
47
+ return [
48
+ `car-runtime-${version}.tgz`,
49
+ `car-runtime-${version}.tgz.sig.bundle`,
50
+ 'SHA256SUMS',
51
+ 'sbom.json',
52
+ 'release-audit.jsonl',
53
+ ]
54
+ }
55
+
56
+ export function buildReleaseArtifacts(opts: {
57
+ version: string
58
+ root?: string
59
+ dist?: string
60
+ onRecord?: (stage: string, detail: string) => void
61
+ }): BuildResult {
62
+ const root = opts.root ?? new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
63
+ const dist = opts.dist ?? join(root, 'dist')
64
+ const version = opts.version
65
+ const records: BuildRecord[] = []
66
+ const rec = (stage: string, detail: string) => {
67
+ const r = { stage, detail, ts: Date.now() }
68
+ records.push(r)
69
+ opts.onRecord?.(stage, detail)
70
+ }
71
+
72
+ mkdirSync(dist, { recursive: true })
73
+ const tgzPath = join(dist, `car-runtime-${version}.tgz`)
74
+ execSync(`git archive --format=tar.gz -o "${tgzPath}" HEAD`, { cwd: root })
75
+ const bytes = readFileSync(tgzPath)
76
+ rec('archive', `git archive HEAD → car-runtime-${version}.tgz(${bytes.length} bytes,树哈希锚定)`)
77
+
78
+ // SHA256SUMS + 自检(G-11 口径:对裸字节求哈希;门禁不可写死为 true——落盘读回复核)
79
+ const sumsPath = join(dist, 'SHA256SUMS')
80
+ writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${version}.tgz\n`)
81
+ const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
82
+ if (recorded !== sha256(bytes) || recordedName !== `car-runtime-${version}.tgz`) {
83
+ throw new Error(`CAR-E-ARTIFACT: SHA256SUMS 自检失败——记录 ${recorded} ≠ 制品裸字节哈希(G-11 教训口径)`)
84
+ }
85
+ rec('sha256sums', `SHA256SUMS ${recorded!.slice(0, 16)}…(裸字节复核通过)`)
86
+
87
+ // SBOM(G-05 同构:TS 直跑形态制品清单 + 零运行时依赖)
88
+ const files = [...walk(join(root, 'src'), '.ts'), join(root, 'package.json'), join(root, 'README.md')]
89
+ const sbom = {
90
+ bomFormat: 'CAR-SBOM', specVersion: '0.1', version,
91
+ components: files.map(f => ({ type: 'file', path: f.replace(root, '').replace(/\\/g, '/'), sha256: sha256(readFileSync(f, 'utf-8')) })),
92
+ runtimeDependencies: [],
93
+ }
94
+ const sbomPath = join(dist, 'sbom.json')
95
+ writeFileSync(sbomPath, JSON.stringify(sbom, null, 2))
96
+ rec('sbom', `${files.length} 个组件(运行时依赖 0 项)`)
97
+
98
+ return { tgzPath, sha256: sha256(bytes), sumsPath, sbomPath, components: files.length, bytes: bytes.length, records }
99
+ }
100
+
101
+ /** CLI 入口(CI j16 调用形态):构建 + 写构建审计 release-audit.jsonl(五件套之五) */
102
+ function main(): void {
103
+ const version = (JSON.parse(readFileSync(join(new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'), 'package.json'), 'utf-8')) as { version: string }).version
104
+ const result = buildReleaseArtifacts({ version })
105
+ const auditPath = join(result.tgzPath, '..', 'release-audit.jsonl')
106
+ writeFileSync(auditPath, result.records.map(r => JSON.stringify(r)).join('\n') + '\n')
107
+ console.log(`release artifacts: v${version} → tgz(${result.bytes}B, sha256 ${result.sha256.slice(0, 16)}…) + SHA256SUMS + sbom(${result.components} components) + release-audit.jsonl`)
108
+ console.log(`assets (D-14 五件套): ${releaseAssetNames(version).join(', ')}`)
109
+ }
110
+
111
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
@@ -12,6 +12,7 @@ import { execSync, execFileSync } from 'node:child_process'
12
12
  import { createHash } from 'node:crypto'
13
13
  import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'node:fs'
14
14
  import { join } from 'node:path'
15
+ import { buildReleaseArtifacts, releaseAssetNames } from './release-artifacts.ts'
15
16
 
16
17
  const ROOT = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
17
18
  const DIST = join(ROOT, 'dist')
@@ -76,16 +77,13 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
76
77
  // ── 阶段 2:构建与制品 ──
77
78
  {
78
79
  const stage = 'S2-构建与制品'
79
- mkdirSync(DIST, { recursive: true })
80
- // TS 直跑形态制品清单(原生编译为零——node:sqlite 内置)
81
- const files = [...walk(join(ROOT, 'src'), '.ts'), join(ROOT, 'package.json'), join(ROOT, 'README.md')]
82
- const sbom = {
83
- bomFormat: 'CAR-SBOM', specVersion: '0.1', version: VERSION,
84
- components: files.map(f => ({ type: 'file', path: f.replace(ROOT, '').replace(/\\/g, '/'), sha256: sha256(readFileSync(f, 'utf-8')) })),
85
- runtimeDependencies: [],
86
- }
87
- writeFileSync(join(DIST, 'sbom.json'), JSON.stringify(sbom, null, 2))
88
- record(stage, 'G-05 SBOM 生成', true, `${files.length} 个组件(运行时依赖 0 项)`)
80
+ // 1.3-S2(W4-1):制品构建抽至 release-artifacts.ts(本地流水线与 CI j16 双端单一事实源——
81
+ // 同仓树逐字节同制品);此处产出 tgz + SHA256SUMS + sbom,G-11 在 S6 独立复核
82
+ const built = buildReleaseArtifacts({
83
+ version: VERSION,
84
+ onRecord: (s, detail) => auditLog(stage, s.toUpperCase(), 'PASS', { detail }),
85
+ })
86
+ record(stage, 'G-05 SBOM 生成', true, `${built.components} 个组件(运行时依赖 0 项;tar.gz ${built.bytes} bytes 已构建——W4 单一事实源)`)
89
87
  // 版本一致性:精确 semver(含 prerelease——1.0-rc.x 是 rc 渠道合法形态;禁止 build 元数据与 loose 版本)
90
88
  const ok = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(VERSION) && !/\+/.test(VERSION)
91
89
  record(stage, 'G-06 版本一致性', ok, `version=${VERSION}(精确 semver 含 prerelease——rc 渠道合法形态;npm 不可变语义:禁止覆盖已发布版本)`)
@@ -117,7 +115,7 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
117
115
  }
118
116
 
119
117
  // ── 阶段 4-5:provenance 与远端归档(远端依赖,DRY-RUN)──
120
- // G-08 cosign 签名已移至 S6 之后本地实签(见 S6b)
118
+ // G-08 双轨语义(1.3 D-15):本地 keypair no-tlog = 预演态(S6b);CI j16 keyless OIDC+Rekor = 正式态
121
119
  // G-10:REMOTE_CHECK=1 且具备 GH_TOKEN 时实查 Release 资产齐备性,否则 DRY-RUN
122
120
  {
123
121
  const stage = 'S4-provenance与远端归档'
@@ -135,12 +133,13 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
135
133
  } catch { return [] }
136
134
  }
137
135
  const assets = [...new Set([...fetchAssets(`v${VERSION}`), ...fetchAssets(VERSION)])]
138
- const need = [`car-runtime-${VERSION}.tgz`, `car-runtime-${VERSION}.tgz.sig.bundle`, 'SHA256SUMS', 'sbom.json', 'release-audit.jsonl', 'car-release.pub']
136
+ // 资产清单单一事实源(1.3 W4-4/R-3):releaseAssetNames(五件套,无 car-release.pub)
137
+ const need = releaseAssetNames(VERSION)
139
138
  const missing = need.filter(n => !assets.includes(n))
140
139
  g10Ok = missing.length === 0
141
140
  g10Dry = false
142
141
  g10Detail = g10Ok
143
- ? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM+审计+公钥)`
142
+ ? `Release v${VERSION} 归档齐备(${assets.length} assets:tgz+签名bundle+SHA256SUMS+SBOM+审计——D-14 五件套)`
144
143
  : `Release 资产缺失:${missing.join(', ')}`
145
144
  } catch { /* 远端不可达或 Release 不存在 → 保持 DRY-RUN */ }
146
145
  }
@@ -171,22 +170,18 @@ const record = (stage: string, gate: string, ok: boolean, detail = '', dryRun =
171
170
  record(stage, 'G-09 npm provenance(OIDC 可信发布)', g9Ok, g9Detail, g9Dry)
172
171
  }
173
172
 
174
- // ── 阶段 6:本地制品封装 + 校验和 ──
173
+ // ── 阶段 6:制品校验(G-11:对 S2 产出的制品做独立复核,不重建不写死) ──
175
174
  const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
176
175
  {
177
176
  const stage = 'S6-制品封装'
178
- execSync(`git archive --format=tar.gz -o "${TARBALL}" HEAD`, { cwd: ROOT })
179
- const bytes = readFileSync(TARBALL)
180
177
  // 校验和必须对「裸字节」求哈希——曾误写为 sha256(bytes.toString('base64')),
181
178
  // 记录值成为 base64 字符串的哈希,外部 sha256sum 校验必然失败(rc.1 已发生)。
182
- const sumsPath = join(DIST, 'SHA256SUMS')
183
- writeFileSync(sumsPath, `${sha256(bytes)} car-runtime-${VERSION}.tgz\n`)
184
- // 自检:从落盘文件读回,复核其确实等于制品裸字节哈希(门禁不可写死为 true)
185
- const [recorded, recordedName] = readFileSync(sumsPath, 'utf-8').trim().split(/\s+/)
179
+ const bytes = readFileSync(TARBALL)
186
180
  const actual = sha256(bytes)
181
+ const [recorded, recordedName] = readFileSync(join(DIST, 'SHA256SUMS'), 'utf-8').trim().split(/\s+/)
187
182
  const ok = recorded === actual && recordedName === `car-runtime-${VERSION}.tgz`
188
183
  record(stage, 'G-11 制品校验和(SHA-256)', ok, ok
189
- ? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded.slice(0, 16)}…(已按裸字节复核,外部 sha256sum 可验证)`
184
+ ? `car-runtime-${VERSION}.tgz (${bytes.length} bytes) → SHA256SUMS ${recorded!.slice(0, 16)}…(S2 产出独立复核,外部 sha256sum 可验证)`
190
185
  : `校验和自检失败:SHA256SUMS 记录 ${recorded} ≠ 制品裸字节哈希 ${actual}`)
191
186
  }
192
187
 
@@ -202,7 +197,7 @@ const TARBALL = join(DIST, `car-runtime-${VERSION}.tgz`)
202
197
  '--signing-config', sigConfig, '--bundle', bundle, TARBALL], { env })
203
198
  execFileSync(cosignBin, ['verify-blob', '--key', join(keysDir, 'car-release.pub'),
204
199
  '--insecure-ignore-tlog', '--bundle', bundle, TARBALL], { env })
205
- record(stage, 'G-08 cosign 签名', existsSync(bundle), `本地 keypair 实签+验签 PASS(no-tlog 模式,bundle: car-runtime-${VERSION}.tgz.sig.bundle);正式发布可在 CI 切 keyless OIDC + Rekor`)
200
+ record(stage, 'G-08 cosign 签名', existsSync(bundle), `本地 keypair 实签+验签 PASS(no-tlog 模式,bundle: car-runtime-${VERSION}.tgz.sig.bundle)——D-15 预演轨;正式轨 = CI j16 keyless OIDC+Rekor`)
206
201
  }
207
202
 
208
203
  // ── 阶段 7:dist-tag 晋级模拟 + 回滚演练(发布渠道语义)──
@@ -229,5 +224,5 @@ console.log('\n===== 发布工程预演结果 =====')
229
224
  for (const r of results) console.log(` [${r.verdict.padEnd(7)}] ${r.stage} / ${r.gate}${r.detail ? ' — ' + r.detail : ''}`)
230
225
  const pass = results.filter(r => r.verdict === 'PASS').length
231
226
  const dry = results.filter(r => r.verdict === 'DRY-RUN').length
232
- console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist/:SBOM + tar.gz + SHA256SUMS + registry-sim + release-audit.jsonl)`)
233
- console.log('DRY-RUN 项正式执行前置:GitHub 仓库 + OIDC + npm publish 凭据 + cosign + Linux CI runner(逃逸矩阵 G-07)')
227
+ console.log(`\n合计:${pass} PASS / ${dry} DRY-RUN / 0 FAIL(制品:dist/ 五件套——tar.gz + 签名bundle + SHA256SUMS + sbom + release-audit)`)
228
+ console.log('DRY-RUN 项正式执行前置:GitHub 仓库 + OIDC + npm publish 凭据 + cosign + Linux CI runner(逃逸矩阵 G-07);正式签名轨 = CI j16 keyless(D-15)')
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 1.3 · 发布资产自动化(S30):release-artifacts.ts 制品构建 + release.yml 结构校验
3
+ *
4
+ * 覆盖(1.3-迭代规划 W4-1/W4-3/W4-4;防幽灵门禁——CI 新步骤必须有本地可验证断言):
5
+ * - buildReleaseArtifacts:五件套构建(dist 注入临时目录)、SHA256SUMS 裸字节自检(G-11 口径)、
6
+ * SBOM 同构(G-05 口径)、构建审计行完整、sha256 与外部 sha256sum 口径一致
7
+ * - releaseAssetNames 单一事实源:五件套、**不含 car-release.pub**(D-14 资产集迁移钉死)
8
+ * - release.yml 结构断言:contents: write / id-token: write / cosign keyless(--yes + bundle)/
9
+ * gh release upload --clobber / npm publish --provenance / **upload 先于 publish**(W4 步骤序红线)/
10
+ * cosign-installer 钉版(R-4)
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+ import { createHash } from 'node:crypto'
15
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
16
+ import { tmpdir } from 'node:os'
17
+ import { join, dirname } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { buildReleaseArtifacts, releaseAssetNames } from '../scripts/release-artifacts.ts'
20
+
21
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
22
+
23
+ function withTempDir(name: string, fn: (dir: string) => Promise<void> | void): Promise<void> {
24
+ const dir = mkdtempSync(join(tmpdir(), `car-s30-${name}-`))
25
+ try {
26
+ const r = fn(dir)
27
+ const cleanup = () => { try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ } }
28
+ if (r instanceof Promise) return r.finally(cleanup)
29
+ cleanup()
30
+ return Promise.resolve()
31
+ } catch (e) {
32
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* 红线 8 */ }
33
+ throw e
34
+ }
35
+ }
36
+
37
+ test('S30: buildReleaseArtifacts——五件套之四件落盘(bundle 由签名轨产出)+ SHA256SUMS 裸字节自检', () => {
38
+ withTempDir('build', dir => {
39
+ const records: Array<{ stage: string; detail: string }> = []
40
+ const r = buildReleaseArtifacts({
41
+ version: '0.0.0-s30test',
42
+ dist: dir,
43
+ onRecord: (stage, detail) => records.push({ stage, detail }),
44
+ })
45
+ // 制品在位
46
+ assert.ok(existsSync(r.tgzPath), 'tgz 在位')
47
+ assert.ok(existsSync(r.sumsPath), 'SHA256SUMS 在位')
48
+ assert.ok(existsSync(r.sbomPath), 'sbom.json 在位')
49
+ // G-11 口径:外部 sha256sum 可验证(裸字节哈希)
50
+ const bytes = readFileSync(r.tgzPath)
51
+ assert.equal(createHash('sha256').update(bytes).digest('hex'), r.sha256)
52
+ assert.equal(readFileSync(r.sumsPath, 'utf-8'), `${r.sha256} car-runtime-0.0.0-s30test.tgz\n`)
53
+ assert.match(readFileSync(r.sumsPath, 'utf-8').trim(), /^[0-9a-f]{64} car-runtime-0\.0\.0-s30test\.tgz$/)
54
+ // G-05 同构:SBOM 组件面 + 零运行时依赖
55
+ const sbom = JSON.parse(readFileSync(r.sbomPath, 'utf-8')) as { bomFormat: string; components: unknown[]; runtimeDependencies: unknown[] }
56
+ assert.equal(sbom.bomFormat, 'CAR-SBOM')
57
+ assert.ok(sbom.components.length >= 10, 'src/*.ts + package.json + README')
58
+ assert.deepEqual(sbom.runtimeDependencies, [], '零运行时依赖红线')
59
+ // 构建审计完整(archive / sha256sums / sbom 三段记录)
60
+ assert.deepEqual(records.map(x => x.stage), ['archive', 'sha256sums', 'sbom'])
61
+ assert.equal(r.bytes, bytes.length)
62
+ })
63
+ })
64
+
65
+ test('S30: releaseAssetNames 单一事实源——五件套(D-14:car-release.pub 移除,bundle 在列)', () => {
66
+ const names = releaseAssetNames('9.9.9')
67
+ assert.deepEqual(names, [
68
+ 'car-runtime-9.9.9.tgz',
69
+ 'car-runtime-9.9.9.tgz.sig.bundle',
70
+ 'SHA256SUMS',
71
+ 'sbom.json',
72
+ 'release-audit.jsonl',
73
+ ])
74
+ assert.equal(names.includes('car-release.pub'), false, 'D-14 资产集迁移:公钥不再是验证锚')
75
+ })
76
+
77
+ test('S30: G-10/CI/测试三方同源——pipeline 引用 releaseAssetNames(静态断言防漂移,R-3)', () => {
78
+ const pipeline = readFileSync(join(ROOT, 'scripts', 'release-pipeline.ts'), 'utf-8')
79
+ assert.match(pipeline, /import \{ buildReleaseArtifacts, releaseAssetNames \} from '\.\/release-artifacts\.ts'/)
80
+ assert.match(pipeline, /releaseAssetNames\(VERSION\)/, 'G-10 need-list 改引单一事实源')
81
+ // car-release.pub 在本地预演轨 keypair 验签(keysDir)合法存在——D-15 双轨;资产清单面已由 releaseAssetNames 收口
82
+ // G-11 保留独立复核(门禁不写死为 true)
83
+ assert.match(pipeline, /S6-制品封装/)
84
+ assert.match(pipeline, /S2 产出独立复核/)
85
+ })
86
+
87
+ // ==================== release.yml 结构校验(W4-2 防幽灵门禁) ====================
88
+
89
+ const yml = readFileSync(join(ROOT, '.github', 'workflows', 'release.yml'), 'utf-8')
90
+
91
+ test('S30: release.yml 权限面——contents: write(上传需要)+ id-token: write(keyless 凭据)', () => {
92
+ const permBlock = yml.slice(yml.indexOf('permissions:'), yml.indexOf('jobs:'))
93
+ assert.match(permBlock, /contents:\s*write/)
94
+ assert.match(permBlock, /id-token:\s*write/)
95
+ })
96
+
97
+ test('S30: release.yml 步骤面——构建/签名/上传/publish 四段齐备且序正确(upload 先于 publish)', () => {
98
+ // 四段关键词
99
+ assert.match(yml, /node --experimental-transform-types scripts\/release-artifacts\.ts/, '构建步骤调单一事实源脚本')
100
+ assert.match(yml, /sigstore\/cosign-installer@v3\.7\.0/, 'cosign 钉版(R-4)')
101
+ assert.match(yml, /cosign sign-blob --yes --bundle/, 'keyless 签名(--yes 免交互 + bundle 产出)')
102
+ assert.match(yml, /gh release upload "\$VERSION" "\$f" --clobber/, '五件套上传(--clobber 幂等;目标=版本号——D-14 裸 tag 形态)')
103
+ assert.match(yml, /npm publish --provenance --tag "\$TAG"/, 'publish 步骤不变(provenance 口径)')
104
+ // 步骤序红线:upload 步在 publish 步之前(上传失败 job 红,publish 不带病出街)
105
+ const uploadAt = yml.indexOf('gh release upload')
106
+ const publishAt = yml.indexOf('npm publish --provenance')
107
+ assert.ok(uploadAt > -1 && publishAt > -1 && uploadAt < publishAt, 'upload 先于 publish(W4 步骤序红线)')
108
+ // 签名记录追加进构建审计(五件套之五的内容完整性)
109
+ assert.match(yml, /release-audit\.jsonl/, '审计文件在签名步被追加')
110
+ })
111
+
112
+ test('S30: release.yml 解析安全——name 行不得含未引号冒号(1.3-BUG-2:`name: x (y: z)` 令 YAML 解析崩溃,release 事件静默失效)', () => {
113
+ // startup failure 形态:run.name = 文件路径、jobs 为空、release 事件不触发——结构断言在此钉死本失败类
114
+ const offenders = yml.split('\n').filter(l => /^\s*- name: [^'"].*:\s/.test(l))
115
+ assert.deepEqual(offenders, [], `以下 step name 含未引号冒号+空格:${JSON.stringify(offenders)}`)
116
+ // 上传目标 = 包版本号(D-14 裸 tag:release tag == version;dispatch 触发亦可用)
117
+ assert.match(yml, /gh release upload "\$VERSION" "\$f" --clobber/)
118
+ })
119
+
120
+ test('S30: release.yml keyless 验证口径——sign-blob 无 --key(keyless 非 keypair)', () => {
121
+ // 正式轨不得使用本地 keypair(私钥不出本地红线);keypair 仅存在于本地预演轨 release-pipeline.ts
122
+ const signStep = yml.slice(yml.indexOf('cosign sign-blob'))
123
+ assert.doesNotMatch(signStep, /--key/, 'CI 签名步无 keypair 材料')
124
+ assert.doesNotMatch(yml, /car-release\.key|COSIGN_PASSWORD/, 'CI 无本地私钥/口令引用')
125
+ const pipeline = readFileSync(join(ROOT, 'scripts', 'release-pipeline.ts'), 'utf-8')
126
+ assert.match(pipeline, /car-release\.key/, '本地预演轨保留 keypair(D-15 双轨语义)')
127
+ })