release-skill 0.7.6 → 0.7.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +14 -11
- package/README.zh-CN.md +13 -10
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1728 -1155
- package/adapters/claude/schemas/.render-manifest.json +6 -6
- package/adapters/claude/schemas/release-plan.schema.json +47 -0
- package/adapters/claude/schemas/release-project.schema.json +24 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +4 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1728 -1155
- package/adapters/codex/schemas/.render-manifest.json +6 -6
- package/adapters/codex/schemas/release-plan.schema.json +47 -0
- package/adapters/codex/schemas/release-project.schema.json +24 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +4 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1728 -1155
- package/adapters/kimi/schemas/.render-manifest.json +6 -6
- package/adapters/kimi/schemas/release-plan.schema.json +47 -0
- package/adapters/kimi/schemas/release-project.schema.json +24 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +4 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1728 -1155
- package/adapters/workbuddy/schemas/.render-manifest.json +6 -6
- package/adapters/workbuddy/schemas/release-plan.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +24 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +4 -0
- package/bin/release-skill-cli.mjs +116 -47
- package/bin/release-skill.bundle.mjs +1728 -1155
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +6 -6
- package/references/01-state-machine.md +2 -2
- package/references/02-project-config.md +18 -0
- package/references/06-adapter-contract.md +4 -3
- package/schemas/.render-manifest.json +6 -6
- package/schemas/release-plan.schema.json +47 -0
- package/schemas/release-project.schema.json +24 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-prepare/SKILL.md +4 -0
- package/skills-src/release-prepare/SKILL.md +4 -0
- package/src/adapters/git-github.mjs +139 -26
- package/src/commands/prepare.mjs +72 -3
- package/src/commands/publish.mjs +51 -0
- package/src/commands/verify.mjs +53 -1
- package/src/core/config.mjs +31 -0
- package/src/core/plan.mjs +78 -0
- package/src/core/release-assets.mjs +54 -0
- package/src/core/source-authority.mjs +127 -3
package/package.json
CHANGED
package/platform-manifest.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
3
|
"platformId": "release-skill",
|
|
4
4
|
"platformName": "release-skill Plugin Platform",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.8",
|
|
6
6
|
"logicalRoot": "skills-src/",
|
|
7
|
-
"logicalRootDigest": "
|
|
8
|
-
"projectionDigest": "
|
|
9
|
-
"projectionId": "release-skill:platform:0.7.
|
|
7
|
+
"logicalRootDigest": "b261a4cc49e6dc52dabddb083c22e414902fb269d5879ad01ef8146ceffd7bc7",
|
|
8
|
+
"projectionDigest": "6629de035a25005fd4676b09a0526a46f992a33d974cadd542b0c964d7f906ff",
|
|
9
|
+
"projectionId": "release-skill:platform:0.7.8",
|
|
10
10
|
"manifestRequired": true,
|
|
11
11
|
"projectionSources": {
|
|
12
12
|
"skills-src-root": {
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"bytes": 6335
|
|
7
7
|
},
|
|
8
8
|
"01-state-machine.md": {
|
|
9
|
-
"digest": "
|
|
10
|
-
"bytes":
|
|
9
|
+
"digest": "5afc8e9c29c0d5fb0de0db8b248bbb812fc715af185ac594a172fe68d9a24fed",
|
|
10
|
+
"bytes": 12617
|
|
11
11
|
},
|
|
12
12
|
"02-project-config.md": {
|
|
13
|
-
"digest": "
|
|
14
|
-
"bytes":
|
|
13
|
+
"digest": "09e3c470dea54fe9437528620cc169596e75b5162e137683923347c9cb3702a6",
|
|
14
|
+
"bytes": 23167
|
|
15
15
|
},
|
|
16
16
|
"03-readme-quality.md": {
|
|
17
17
|
"digest": "d74e41052592443ebbd450d0ace5f9b9b19c7a94db50f952b17c1c5a4c5dc230",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"bytes": 12014
|
|
27
27
|
},
|
|
28
28
|
"06-adapter-contract.md": {
|
|
29
|
-
"digest": "
|
|
30
|
-
"bytes":
|
|
29
|
+
"digest": "fd58e8a438aaa9a7a4a939ac190dbdd5d11c3fcaca08feaced4a612a6d6dcf3f",
|
|
30
|
+
"bytes": 19206
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -148,7 +148,7 @@ postPublish hooks 分两个阶段,各自对应独立的 distribute run:
|
|
|
148
148
|
|
|
149
149
|
工作区基线(Git tree hash、workspace digest、捕获时间)与生命周期状态(计划 status、createdAt、action 运行时 status)是**记录层审计数据**:继续写入计划与审批文件以备查,但不参与 planVersion 2 的 digest、不使批准失效。publish/reconcile 检测到基线漂移时记录 warning 级证据事件并继续执行,产物完整性由发布前的冻结产物重验兜底。planVersion 1(旧版)计划维持旧规则:Git tree hash 与 workspace digest 变化同样使批准失效,基线漂移硬失败(BASELINE_CHANGED)。
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
远端冲突状态不在 approval 绑定字段中。远端对象已存在但与冻结计划完全一致时,`publish`/`reconcile` 把对应检查点标为 `SKIPPED`;存在且不一致,或无法可靠判断时,preflight 阶段阻断执行并要求人工决策。npm 版本只有在观察到有效且与冻结计划相同的 integrity 时才能进入 `SKIPPED`;integrity 缺失、空白、无法解析或不同都在任何 Adapter execute 前失败关闭。
|
|
152
152
|
|
|
153
153
|
失效后系统回退到 PREPARED 状态,要求重新批准。
|
|
154
154
|
|
|
@@ -161,7 +161,7 @@ postPublish hooks 分两个阶段,各自对应独立的 distribute run:
|
|
|
161
161
|
3. 创建并推送签名或可追溯 tag。
|
|
162
162
|
4. 发布 npm 包。
|
|
163
163
|
5. 创建 GitHub Release。
|
|
164
|
-
6. 写入远端 URL、commit、tag、包版本、integrity
|
|
164
|
+
6. 写入远端 URL、commit、tag、包版本、integrity 和执行结果。若 npm 检查点因执行前观察结果为 `CONSISTENT` 而跳过,只把本次观察到的 integrity 保存到检查点的 `remoteRef.integrity`;包名、版本、registry、预期 integrity 和 tarball 摘要继续以冻结计划为准。
|
|
165
165
|
|
|
166
166
|
任一步失败都停止后续动作,保存检查点并进入 PARTIAL。已完成的检查点通过 `observe` 验证后在 `reconcile` 中幂等跳过。
|
|
167
167
|
|
|
@@ -27,6 +27,10 @@ project:
|
|
|
27
27
|
defaultBranch: <string> # workspace 远端默认分支,必填;不限定为 main
|
|
28
28
|
sourceRepository: <owner/repo> # workspace 源仓库;生产发布必填
|
|
29
29
|
|
|
30
|
+
publicSourceAuthorityReceipt: # 可选;把源码坐标与多个 npm tarball 绑定为公开 Release asset
|
|
31
|
+
coordinatorUnitId: <release-unit-id>
|
|
32
|
+
subjectUnitIds: [<release-unit-id>]
|
|
33
|
+
|
|
30
34
|
releaseUnits: # 发布单元数组,至少 1 个
|
|
31
35
|
- id: <string> # 唯一标识
|
|
32
36
|
source: <string> # 源码相对路径(相对项目根目录)
|
|
@@ -108,6 +112,20 @@ policy: # 可选,安全策略
|
|
|
108
112
|
forbiddenContentPatterns: [<string>]
|
|
109
113
|
```
|
|
110
114
|
|
|
115
|
+
`publicSourceAuthorityReceipt` 只适用于 production prepare。`coordinatorUnitId`
|
|
116
|
+
指定承载该文件的 GitHub Release;`subjectUnitIds` 必须引用现有 release unit,且每个
|
|
117
|
+
unit 恰好声明一个 npm distribution。prepare 等所有 subject tarball 冻结后生成
|
|
118
|
+
`source-authority-receipt.json`,其公开结构固定为:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{"schemaVersion":1,"kind":"skill-family.source-authority-receipt","sourceRepository":"owner/workspace","sourceBaseCommit":"<full-commit>","subjects":[{"packageName":"@scope/package","version":"1.2.3","filename":"scope-package-1.2.3.tgz","sha256":"<sha256>"}]}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
该文件使用 canonical JSON,不带自摘要,也不包含 release-skill 的计划、运行或批准字段。
|
|
125
|
+
冻结计划记录文件 SHA-256;publish 将相同字节上传到 coordinator GitHub Release,verify
|
|
126
|
+
重新下载远端字节并核对 SHA-256。离线消费者只需该文件和其中 subjects 对应的精确 tarball,便可
|
|
127
|
+
核对包名、版本、文件名、tarball SHA-256、源码仓库与源码基线提交。
|
|
128
|
+
|
|
111
129
|
`postPublish` 只能位于某个 `releaseUnits[]` 条目内。下面的完整结构样例会由 schema 契约测试直接读取并校验,避免文档层级再次漂移:
|
|
112
130
|
|
|
113
131
|
```yaml
|
|
@@ -205,6 +205,7 @@ marketplace install action 的安装侧载荷校验以 action 参数 `payloadCon
|
|
|
205
205
|
|
|
206
206
|
### 4.1 幂等规则
|
|
207
207
|
|
|
208
|
+
- `publish` 在全局 preflight 发现远端对象已存在时,先用同一套 `observe` 分类判断它是已一致还是冲突;已一致的检查点进入 `SKIPPED`,冲突或无法可靠判断时在任何 execute 前失败关闭。
|
|
208
209
|
- `reconcile` 阶段首先通过 `observe` 查询所有计划操作的远端状态。
|
|
209
210
|
- 状态为 `CONSISTENT` 的操作幂等跳过,不重新执行。
|
|
210
211
|
- 状态为 `MISSING` 的操作安全重试。
|
|
@@ -226,7 +227,7 @@ marketplace install action 的安装侧载荷校验以 action 参数 `payloadCon
|
|
|
226
227
|
- 执行前的 `observe` 结果。
|
|
227
228
|
- 执行结果(成功/失败/跳过)。
|
|
228
229
|
- 执行后的 `observe` 验证结果。
|
|
229
|
-
- 远端资源标识(commit、tag、版本、URL)。
|
|
230
|
+
- 远端资源标识(commit、tag、版本、URL)。npm 检查点因 `preObserve=CONSISTENT` 而跳过时,可在 `remoteRef.integrity` 保存本次观察到的有效 integrity;其他 npm 身份和摘要继续由冻结计划提供。
|
|
230
231
|
|
|
231
232
|
---
|
|
232
233
|
|
|
@@ -238,9 +239,9 @@ marketplace install action 的安装侧载荷校验以 action 参数 `payloadCon
|
|
|
238
239
|
2. 重新验证发布计划摘要。
|
|
239
240
|
3. 检查批准记录是否过期。
|
|
240
241
|
4. 按 planVersion 分叉复验工作区基线:planVersion 1 计划基线变化即失败(`BASELINE_CHANGED`);planVersion 2 计划基线漂移仅记录 warning 级证据事件并继续,产物身份由外部写之前的冻结产物复验保证。
|
|
241
|
-
5.
|
|
242
|
+
5. 执行全局远程 preflight;占用类结果先用只读 pre-observe 仲裁,`CONSISTENT` 放行到幂等跳过路径,`CONFLICTING` 或不可可靠观察时失败关闭。
|
|
242
243
|
6. 按批准的操作列表顺序执行每个检查点。
|
|
243
|
-
7. 每个检查点:preflight -> execute -> observe -> 记录。
|
|
244
|
+
7. 每个检查点:pre-observe -> preflight -> execute -> post-observe -> 记录。pre-observe 为 `CONSISTENT` 时直接记录 `SKIPPED`,不调用 execute 或 post-observe。
|
|
244
245
|
8. 任一检查点失败时停止后续动作,计算 PARTIAL(若已有成功检查点)。
|
|
245
246
|
9. 所有检查点成功后进入 PUBLISHED 状态。
|
|
246
247
|
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
"bytes": 1744
|
|
27
27
|
},
|
|
28
28
|
"release-plan.schema.json": {
|
|
29
|
-
"digest": "
|
|
30
|
-
"bytes":
|
|
29
|
+
"digest": "3d24cb908cdfbfd5d89551b7c6fed5140f6f1448b9920f114264291ca4cf1c52",
|
|
30
|
+
"bytes": 55905
|
|
31
31
|
},
|
|
32
32
|
"release-project.schema.json": {
|
|
33
|
-
"digest": "
|
|
34
|
-
"bytes":
|
|
33
|
+
"digest": "b0ba3c46692622f734a80d3f4e3904180bbfd1096ca2d9b6d6fd957917e96f73",
|
|
34
|
+
"bytes": 47688
|
|
35
35
|
},
|
|
36
36
|
"release-run.schema.json": {
|
|
37
|
-
"digest": "
|
|
38
|
-
"bytes":
|
|
37
|
+
"digest": "4a041b8f6488e7d61b7eb271ebdbc2e043731df41c4437783ef63e77f664c192",
|
|
38
|
+
"bytes": 19002
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -53,6 +53,53 @@
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
+
"publicSourceAuthorityReceipt": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"required": [
|
|
59
|
+
"coordinatorUnitId",
|
|
60
|
+
"subjectUnitIds",
|
|
61
|
+
"asset"
|
|
62
|
+
],
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"properties": {
|
|
65
|
+
"coordinatorUnitId": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"pattern": "^(?!\\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$"
|
|
68
|
+
},
|
|
69
|
+
"subjectUnitIds": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"minItems": 1,
|
|
72
|
+
"uniqueItems": true,
|
|
73
|
+
"items": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"pattern": "^(?!\\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"asset": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"required": [
|
|
81
|
+
"name",
|
|
82
|
+
"path",
|
|
83
|
+
"sha256"
|
|
84
|
+
],
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"properties": {
|
|
87
|
+
"name": {
|
|
88
|
+
"const": "source-authority-receipt.json"
|
|
89
|
+
},
|
|
90
|
+
"path": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1,
|
|
93
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
|
94
|
+
},
|
|
95
|
+
"sha256": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
56
103
|
"baseline": {
|
|
57
104
|
"type": "object",
|
|
58
105
|
"required": [
|
|
@@ -44,6 +44,30 @@
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"publicSourceAuthorityReceipt": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Optional public source-authority receipt uploaded to one coordinator GitHub Release after all subject npm tarballs are frozen.",
|
|
50
|
+
"required": [
|
|
51
|
+
"coordinatorUnitId",
|
|
52
|
+
"subjectUnitIds"
|
|
53
|
+
],
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"properties": {
|
|
56
|
+
"coordinatorUnitId": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"pattern": "^(?!\\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$"
|
|
59
|
+
},
|
|
60
|
+
"subjectUnitIds": {
|
|
61
|
+
"type": "array",
|
|
62
|
+
"minItems": 1,
|
|
63
|
+
"uniqueItems": true,
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"pattern": "^(?!\\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
47
71
|
"releaseUnits": {
|
|
48
72
|
"type": "array",
|
|
49
73
|
"minItems": 1,
|
|
@@ -199,6 +199,11 @@
|
|
|
199
199
|
},
|
|
200
200
|
"url": {
|
|
201
201
|
"type": "string"
|
|
202
|
+
},
|
|
203
|
+
"integrity": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"pattern": "^sha512-[A-Za-z0-9+/]{86}==$",
|
|
206
|
+
"description": "Observed npm dist.integrity when an already-consistent npm publish is skipped"
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
},
|
|
@@ -280,7 +285,42 @@
|
|
|
280
285
|
"minimum": 0,
|
|
281
286
|
"description": "写入目标的载荷文件数"
|
|
282
287
|
}
|
|
283
|
-
}
|
|
288
|
+
},
|
|
289
|
+
"allOf": [
|
|
290
|
+
{
|
|
291
|
+
"if": {
|
|
292
|
+
"properties": {
|
|
293
|
+
"remoteRef": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"required": [
|
|
296
|
+
"integrity"
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"required": [
|
|
301
|
+
"remoteRef"
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
"then": {
|
|
305
|
+
"properties": {
|
|
306
|
+
"actionType": {
|
|
307
|
+
"const": "npm-publish"
|
|
308
|
+
},
|
|
309
|
+
"status": {
|
|
310
|
+
"const": "skipped"
|
|
311
|
+
},
|
|
312
|
+
"preObserve": {
|
|
313
|
+
"const": "CONSISTENT"
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
"required": [
|
|
317
|
+
"actionType",
|
|
318
|
+
"status",
|
|
319
|
+
"preObserve"
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
284
324
|
}
|
|
285
325
|
},
|
|
286
326
|
"sourceAuthorityReceipts": {
|
|
@@ -33,6 +33,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
33
33
|
|
|
34
34
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
35
35
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
36
|
+
配置声明 `publicSourceAuthorityReceipt` 时,prepare 必须在所有 subject npm tarball
|
|
37
|
+
冻结后生成 `source-authority-receipt.json`,并把该文件的路径与 SHA-256 绑定到
|
|
38
|
+
coordinator unit 的 `github-release` action。该能力不支持非生产 prepare;不得手工
|
|
39
|
+
补写 receipt 或把私有 plan/run 字段复制进公开文件。
|
|
36
40
|
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
37
41
|
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
38
42
|
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
@@ -33,6 +33,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
33
33
|
|
|
34
34
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
35
35
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
36
|
+
配置声明 `publicSourceAuthorityReceipt` 时,prepare 必须在所有 subject npm tarball
|
|
37
|
+
冻结后生成 `source-authority-receipt.json`,并把该文件的路径与 SHA-256 绑定到
|
|
38
|
+
coordinator unit 的 `github-release` action。该能力不支持非生产 prepare;不得手工
|
|
39
|
+
补写 receipt 或把私有 plan/run 字段复制进公开文件。
|
|
36
40
|
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
37
41
|
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
38
42
|
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
@@ -8,8 +8,10 @@ import {
|
|
|
8
8
|
createResult,
|
|
9
9
|
matchObservation,
|
|
10
10
|
} from './contract.mjs';
|
|
11
|
-
import { resolveFrozenPath, verifyFrozenDirectoryStructure } from '../snapshot/frozen.mjs';
|
|
11
|
+
import { resolveFrozenPath, verifyFrozenDirectoryStructure, verifyFrozenFile } from '../snapshot/frozen.mjs';
|
|
12
12
|
import { githubRepositoryUrl } from './push-snapshot.mjs';
|
|
13
|
+
import { digestReleaseAssetIdentities, normalizeReleaseAssets } from '../core/release-assets.mjs';
|
|
14
|
+
import { sha256Hex } from '../core/digest.mjs';
|
|
13
15
|
|
|
14
16
|
const execFile = promisify(execFileCb);
|
|
15
17
|
const NAME = 'git-github';
|
|
@@ -91,6 +93,82 @@ async function readRemoteBranchCommit(action, exec) {
|
|
|
91
93
|
return stdout.trim().split(/\s+/)[0] ?? '';
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
async function validateLocalReleaseAssets(action, context) {
|
|
97
|
+
const assets = normalizeReleaseAssets(action.releaseAssets);
|
|
98
|
+
const verified = [];
|
|
99
|
+
for (const asset of assets) {
|
|
100
|
+
const file = await verifyFrozenFile({
|
|
101
|
+
root: context.root,
|
|
102
|
+
filePath: asset.path,
|
|
103
|
+
expectedSha256: asset.sha256,
|
|
104
|
+
label: `frozen GitHub Release asset "${asset.name}"`,
|
|
105
|
+
});
|
|
106
|
+
verified.push({ ...asset, physical: file.physical });
|
|
107
|
+
}
|
|
108
|
+
return verified;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readReleaseWithAssets(action, context, exec) {
|
|
112
|
+
const environment = { ...process.env, GH_HOST: action.githubHost ?? 'github.com' };
|
|
113
|
+
const { stdout } = await exec(
|
|
114
|
+
'gh',
|
|
115
|
+
['api', `repos/${action.repo}/releases/tags/${action.tag}`],
|
|
116
|
+
{ cwd: context.root, shell: false, encoding: 'utf8', env: environment },
|
|
117
|
+
);
|
|
118
|
+
const release = JSON.parse(stdout);
|
|
119
|
+
const commitResult = await exec(
|
|
120
|
+
'gh',
|
|
121
|
+
['api', `repos/${action.repo}/git/ref/tags/${action.tag}`, '--jq', '.object.sha'],
|
|
122
|
+
{ cwd: context.root, shell: false, encoding: 'utf8', env: environment },
|
|
123
|
+
);
|
|
124
|
+
const plannedAssets = normalizeReleaseAssets(action.releaseAssets);
|
|
125
|
+
const observedAssets = [];
|
|
126
|
+
const missingAssets = [];
|
|
127
|
+
for (const planned of plannedAssets) {
|
|
128
|
+
const matches = (release.assets ?? []).filter((asset) => asset?.name === planned.name);
|
|
129
|
+
if (matches.length > 1) throw new Error(`GitHub Release has duplicate asset name "${planned.name}"`);
|
|
130
|
+
if (matches.length === 0) {
|
|
131
|
+
missingAssets.push(planned.name);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const remote = matches[0];
|
|
135
|
+
if (!Number.isInteger(remote.id)) throw new Error(`GitHub Release asset "${planned.name}" has no stable asset id`);
|
|
136
|
+
const downloaded = await exec(
|
|
137
|
+
'gh',
|
|
138
|
+
['api', `repos/${action.repo}/releases/assets/${remote.id}`, '-H', 'Accept: application/octet-stream'],
|
|
139
|
+
{ cwd: context.root, shell: false, encoding: null, env: environment, maxBuffer: 64 * 1024 * 1024 },
|
|
140
|
+
);
|
|
141
|
+
const bytes = Buffer.isBuffer(downloaded.stdout)
|
|
142
|
+
? downloaded.stdout
|
|
143
|
+
: Buffer.from(downloaded.stdout ?? '', 'utf8');
|
|
144
|
+
observedAssets.push({ ...planned, sha256: sha256Hex(bytes) });
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
observation: {
|
|
148
|
+
tag: release.tag_name,
|
|
149
|
+
commit: commitResult.stdout.trim(),
|
|
150
|
+
releaseUrl: release.html_url,
|
|
151
|
+
...(missingAssets.length > 0
|
|
152
|
+
? { exists: false, missingReleaseAssets: missingAssets }
|
|
153
|
+
: { releaseAssetsDigest: digestReleaseAssetIdentities(observedAssets) }),
|
|
154
|
+
},
|
|
155
|
+
missingAssets,
|
|
156
|
+
observedAssets,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assertExistingReleaseCompatible(action, state) {
|
|
161
|
+
if (state.observation.tag !== action.tag || state.observation.commit !== action.commit) {
|
|
162
|
+
throw new Error('existing GitHub Release tag or commit conflicts with the frozen plan');
|
|
163
|
+
}
|
|
164
|
+
const expectedByName = new Map(normalizeReleaseAssets(action.releaseAssets).map((asset) => [asset.name, asset]));
|
|
165
|
+
for (const observed of state.observedAssets) {
|
|
166
|
+
if (expectedByName.get(observed.name)?.sha256 !== observed.sha256) {
|
|
167
|
+
throw new Error(`GitHub Release asset "${observed.name}" bytes conflict with the frozen plan`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
94
172
|
export function createGitGithubAdapter(deps = {}) {
|
|
95
173
|
const exec = deps.exec ?? run;
|
|
96
174
|
return Object.freeze({
|
|
@@ -110,6 +188,7 @@ export function createGitGithubAdapter(deps = {}) {
|
|
|
110
188
|
} else if (action.actionType === ActionType.GITHUB_RELEASE) {
|
|
111
189
|
if (!action.repo || !action.tag) throw new Error('github-release requires repo, tag, and commit');
|
|
112
190
|
validateOid(action.commit);
|
|
191
|
+
const localAssets = await validateLocalReleaseAssets(action, context);
|
|
113
192
|
await exec('gh', ['auth', 'status'], {
|
|
114
193
|
cwd: context.root,
|
|
115
194
|
shell: false,
|
|
@@ -123,12 +202,19 @@ export function createGitGithubAdapter(deps = {}) {
|
|
|
123
202
|
throw new Error(`cannot access GitHub repository ${action.repo}: ${err.message}`);
|
|
124
203
|
});
|
|
125
204
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
205
|
+
if (localAssets.length === 0) {
|
|
206
|
+
await exec('gh', ['api', `repos/${action.repo}/releases/tags/${action.tag}`], {
|
|
207
|
+
cwd: context.root,
|
|
208
|
+
shell: false,
|
|
209
|
+
env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' },
|
|
210
|
+
});
|
|
211
|
+
throw new Error(`GitHub Release already exists for ${action.tag}; human intervention required`);
|
|
212
|
+
}
|
|
213
|
+
const state = await readReleaseWithAssets(action, context, exec);
|
|
214
|
+
assertExistingReleaseCompatible(action, state);
|
|
215
|
+
if (state.missingAssets.length === 0) {
|
|
216
|
+
throw new Error(`GitHub Release and assets already exist for ${action.tag}`);
|
|
217
|
+
}
|
|
132
218
|
} catch (err) {
|
|
133
219
|
if (err.message?.includes('already exists')) throw err;
|
|
134
220
|
if (!isNotFound(err)) throw new Error(`cannot determine GitHub Release uniqueness: ${err.message}`);
|
|
@@ -192,13 +278,36 @@ export function createGitGithubAdapter(deps = {}) {
|
|
|
192
278
|
`refs/tags/${action.tag}`,
|
|
193
279
|
], { shell: false });
|
|
194
280
|
} else if (action.actionType === ActionType.GITHUB_RELEASE) {
|
|
195
|
-
await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
281
|
+
const assets = await validateLocalReleaseAssets(action, context);
|
|
282
|
+
let existing = null;
|
|
283
|
+
if (assets.length > 0) {
|
|
284
|
+
try {
|
|
285
|
+
existing = await readReleaseWithAssets(action, context, exec);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (!isNotFound(error)) throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (existing) {
|
|
291
|
+
assertExistingReleaseCompatible(action, existing);
|
|
292
|
+
const missing = new Set(existing.missingAssets);
|
|
293
|
+
const uploadPaths = assets.filter((asset) => missing.has(asset.name)).map((asset) => asset.physical);
|
|
294
|
+
if (uploadPaths.length > 0) {
|
|
295
|
+
await exec('gh', ['release', 'upload', action.tag, '--repo', ghRepo(action), ...uploadPaths], {
|
|
296
|
+
cwd: context.root,
|
|
297
|
+
shell: false,
|
|
298
|
+
env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
await exec('gh', [
|
|
303
|
+
'release', 'create', action.tag,
|
|
304
|
+
'--repo', ghRepo(action),
|
|
305
|
+
'--verify-tag',
|
|
306
|
+
'--title', action.name ?? `Release ${action.tag}`,
|
|
307
|
+
'--notes', action.notes ?? `Release ${action.tag}`,
|
|
308
|
+
...assets.map((asset) => asset.physical),
|
|
309
|
+
], { cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } });
|
|
310
|
+
}
|
|
202
311
|
} else if (action.actionType === ActionType.SET_DEFAULT_BRANCH) {
|
|
203
312
|
validateDefaultBranchAction(action);
|
|
204
313
|
const { stdout: current } = await exec('gh', ['api', `repos/${action.repo}`, '--jq', '.default_branch'], {
|
|
@@ -242,18 +351,22 @@ export function createGitGithubAdapter(deps = {}) {
|
|
|
242
351
|
const { remoteUrl } = await validateTagAction(action, context, exec);
|
|
243
352
|
observation = { tag: action.tag, commit: await readRemoteTag(action, remoteUrl, exec) };
|
|
244
353
|
} else if (action.actionType === ActionType.GITHUB_RELEASE) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
'
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
354
|
+
if (normalizeReleaseAssets(action.releaseAssets).length > 0) {
|
|
355
|
+
observation = (await readReleaseWithAssets(action, context, exec)).observation;
|
|
356
|
+
} else {
|
|
357
|
+
const { stdout } = await exec('gh', [
|
|
358
|
+
'release', 'view', action.tag,
|
|
359
|
+
'--repo', ghRepo(action),
|
|
360
|
+
'--json', 'tagName,url',
|
|
361
|
+
], { cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } });
|
|
362
|
+
const release = JSON.parse(stdout);
|
|
363
|
+
const commitResult = await exec(
|
|
364
|
+
'gh',
|
|
365
|
+
['api', `repos/${action.repo}/git/ref/tags/${action.tag}`, '--jq', '.object.sha'],
|
|
366
|
+
{ cwd: context.root, shell: false, env: { ...process.env, GH_HOST: action.githubHost ?? 'github.com' } },
|
|
367
|
+
);
|
|
368
|
+
observation = { tag: release.tagName, commit: commitResult.stdout.trim(), releaseUrl: release.url };
|
|
369
|
+
}
|
|
257
370
|
} else if (action.actionType === ActionType.SET_DEFAULT_BRANCH) {
|
|
258
371
|
validateDefaultBranchAction(action);
|
|
259
372
|
const { stdout: current } = await exec('gh', ['api', `repos/${action.repo}`, '--jq', '.default_branch'], {
|