release-skill 0.7.5 → 0.7.7
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 +37 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +22 -11
- package/README.zh-CN.md +17 -8
- package/SECURITY.md +12 -6
- 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 +1729 -1159
- package/adapters/claude/schemas/release-plan.schema.json +47 -0
- package/adapters/claude/schemas/release-project.schema.json +24 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/codex/schemas/release-plan.schema.json +47 -0
- package/adapters/codex/schemas/release-project.schema.json +24 -0
- package/adapters/codex/skills/release-assess/SKILL.md +1 -1
- package/adapters/codex/skills/release-config/SKILL.md +1 -1
- package/adapters/codex/skills/release-docs/SKILL.md +1 -1
- package/adapters/codex/skills/release-help/SKILL.md +1 -1
- package/adapters/codex/skills/release-marketplace/SKILL.md +1 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +6 -2
- package/adapters/codex/skills/release-publish/SKILL.md +1 -1
- package/adapters/codex/skills/release-reconcile/SKILL.md +1 -1
- package/adapters/codex/skills/release-setup/SKILL.md +1 -1
- package/adapters/codex/skills/release-verify/SKILL.md +1 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/kimi/schemas/release-plan.schema.json +47 -0
- package/adapters/kimi/schemas/release-project.schema.json +24 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1729 -1159
- package/adapters/workbuddy/schemas/release-plan.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +24 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -1
- package/bin/release-skill-cli.mjs +116 -47
- package/bin/release-skill.bundle.mjs +1729 -1159
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/02-project-config.md +73 -29
- package/schemas/release-plan.schema.json +47 -0
- package/schemas/release-project.schema.json +24 -0
- package/skills/release-prepare/SKILL.md +5 -1
- package/skills-src/release-prepare/SKILL.md +5 -1
- package/src/adapters/git-github.mjs +139 -26
- package/src/commands/prepare.mjs +76 -6
- package/src/commands/publish.mjs +36 -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/src/producers/build-adapters.mjs +1 -1
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.7",
|
|
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.7",
|
|
10
10
|
"manifestRequired": true,
|
|
11
11
|
"projectionSources": {
|
|
12
12
|
"skills-src-root": {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"bytes": 12086
|
|
11
11
|
},
|
|
12
12
|
"02-project-config.md": {
|
|
13
|
-
"digest": "
|
|
14
|
-
"bytes":
|
|
13
|
+
"digest": "ce1986761f2aee9aab2646170cbd9ee3d14b7032db3bfb57b17e7369a45197f2",
|
|
14
|
+
"bytes": 21924
|
|
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": "e00c175b24764487618dfdf20dc295f12a7e809f7dbfc69f6936a1e97bd02ab8",
|
|
30
|
+
"bytes": 18517
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -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> # 源码相对路径(相对项目根目录)
|
|
@@ -70,6 +74,20 @@ releaseUnits: # 发布单元数组,至少 1 个
|
|
|
70
74
|
versionMarkers: # 可选;id 唯一
|
|
71
75
|
- id: <string>
|
|
72
76
|
pattern: <string> # 含 {version} 占位符的精确标记模式
|
|
77
|
+
postPublish: # 可选;发布单元级的发布后分发配置
|
|
78
|
+
commitIdentity:
|
|
79
|
+
name: <string>
|
|
80
|
+
email: <string>
|
|
81
|
+
targets: # 可选;镜像或市场索引目标
|
|
82
|
+
- id: <string>
|
|
83
|
+
kind: payload-mirror # 或 marketplace-index
|
|
84
|
+
remoteUrl: <string> # file:// 或 https://;禁止 ssh
|
|
85
|
+
visibility: public # 或 private / internal
|
|
86
|
+
branch: <string>
|
|
87
|
+
hooks: # 可选;命名 preset 或自定义 command hook
|
|
88
|
+
- id: <string>
|
|
89
|
+
command: [<executable>, <arg>]
|
|
90
|
+
phase: distribute # 或 postVerify
|
|
73
91
|
|
|
74
92
|
verificationGates: # 可选;定义会进入冻结计划摘要
|
|
75
93
|
- id: <stable-id>
|
|
@@ -83,43 +101,69 @@ verificationGates: # 可选;定义会进入冻结计划摘要
|
|
|
83
101
|
envAllowlist: []
|
|
84
102
|
|
|
85
103
|
hooks: # 可选,命令 hooks
|
|
104
|
+
lint: <hook>
|
|
86
105
|
docs: <hook>
|
|
87
106
|
build: <hook>
|
|
88
107
|
test: <hook>
|
|
89
108
|
typecheck: <hook>
|
|
90
|
-
lint: <hook>
|
|
91
|
-
|
|
92
|
-
postPublish: # 可选;发布后分发配置(W2 closure)
|
|
93
|
-
distribute: # 分发行动数组
|
|
94
|
-
- actionType: <string> # "distribute-mirror" | "distribute-marketplace-index"
|
|
95
|
-
adapter: <string> # "distribute-git" | "distribute-marketplace"
|
|
96
|
-
kind: <string> # "payload-mirror" | "marketplace-index-form"
|
|
97
|
-
targetId: <string> # 目标标识
|
|
98
|
-
remoteUrl: <string> # file:// 或 https://;禁止 ssh
|
|
99
|
-
branch: <string> # 分支名,不得以 '-' 开头
|
|
100
|
-
tag: <string> # tag 模板,用于版本验证
|
|
101
|
-
commitIdentity: # 冻结提交身份
|
|
102
|
-
name: <string> # 禁止以 '-' 开头
|
|
103
|
-
email: <string> # RFC 5322 valid
|
|
104
|
-
payloadDir: <string> # 相对路径
|
|
105
|
-
dryRun?: boolean # 可选;只本地执行
|
|
106
|
-
materialize?: { # 可选;缺省时载荷由冻结 publicFiles 经 Foundation 托管投影物化
|
|
107
|
-
command: <string> # 可执行文件
|
|
108
|
-
args: [<string>] # 参数数组,禁止 shell 字符串
|
|
109
|
-
outputMarker?: <string> # 输出标记,用于验证
|
|
110
|
-
requireReport: {} # 前置报告要求
|
|
111
|
-
urlDependsOn: [<string>]> # 依赖动作完成
|
|
112
|
-
onConflict?: <string> # "FAIL" (默认) | "RETRY"
|
|
113
|
-
retryPolicy?: { # 可选;重试策略
|
|
114
|
-
maxAttempts: number
|
|
115
|
-
backoffMs: number
|
|
116
|
-
}
|
|
117
109
|
|
|
118
110
|
policy: # 可选,安全策略
|
|
119
111
|
forbiddenPaths: [<string>]
|
|
120
112
|
forbiddenContentPatterns: [<string>]
|
|
121
113
|
```
|
|
122
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
|
+
|
|
129
|
+
`postPublish` 只能位于某个 `releaseUnits[]` 条目内。下面的完整结构样例会由 schema 契约测试直接读取并校验,避免文档层级再次漂移:
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
# schema-example: postpublish-release-unit
|
|
133
|
+
apiVersion: release-skill/v1
|
|
134
|
+
kind: ReleaseProject
|
|
135
|
+
project:
|
|
136
|
+
name: postpublish-example
|
|
137
|
+
defaultBranch: main
|
|
138
|
+
releaseUnits:
|
|
139
|
+
- id: postpublish-example
|
|
140
|
+
source: .
|
|
141
|
+
publicRepo: example/postpublish-example
|
|
142
|
+
version:
|
|
143
|
+
source: package.json
|
|
144
|
+
tagTemplate: v{version}
|
|
145
|
+
distributions:
|
|
146
|
+
- type: npm
|
|
147
|
+
package: postpublish-example
|
|
148
|
+
registry: https://registry.npmjs.org
|
|
149
|
+
publisher: example
|
|
150
|
+
publicFiles:
|
|
151
|
+
- from: README.md
|
|
152
|
+
to: README.md
|
|
153
|
+
mode: preserve
|
|
154
|
+
requiredPublicFiles: [README.md]
|
|
155
|
+
previousPublicBaseline:
|
|
156
|
+
mode: none
|
|
157
|
+
postPublish:
|
|
158
|
+
commitIdentity:
|
|
159
|
+
name: release-bot
|
|
160
|
+
email: release-bot@example.com
|
|
161
|
+
hooks:
|
|
162
|
+
- id: notify-downstream
|
|
163
|
+
command: [node, scripts/notify-downstream.mjs]
|
|
164
|
+
phase: postVerify
|
|
165
|
+
```
|
|
166
|
+
|
|
123
167
|
---
|
|
124
168
|
|
|
125
169
|
## 3. 公开源码镜像与分发包
|
|
@@ -218,7 +262,7 @@ squash、rebase 和默认分支上的无关后续提交可以通过;冲突解
|
|
|
218
262
|
| `timeoutMs` | 整数 | 否 | 最小 1000,最大 7200000(2 小时) |
|
|
219
263
|
| `envAllowlist` | 字符串数组 | 否 | 每个元素匹配 `^[A-Z_][A-Z0-9_]*$`;无重复元素 |
|
|
220
264
|
|
|
221
|
-
hook
|
|
265
|
+
hook 执行顺序固定为 `lint → docs → build → test → typecheck`,优先运行成本最低的检查。执行规则如下:
|
|
222
266
|
|
|
223
267
|
- 使用 `execFile`,不使用 `exec` 或 `{ shell: true }`。
|
|
224
268
|
- 通过 `realpath` 解析 cwd,拒绝逃逸到项目根目录之外。
|
|
@@ -254,7 +298,7 @@ hooks:
|
|
|
254
298
|
|
|
255
299
|
`snapshot-verify` 获得冻结公开快照的一次性可写副本;副本销毁后重新摘要原快照,失败时还没有远端写入。`consumer-verify` 只在精确 npm 或 Claude/Codex 隔离安装成功后,从安装根执行;结果摘要进入 verify run,失败不得达到 VERIFIED。
|
|
256
300
|
|
|
257
|
-
gate 与 legacy hook
|
|
301
|
+
gate 与 legacy hook 都是没有网络沙箱的本地进程。在 `.release-skill/project.yaml` 中配置命令即完成授权,调用对应命令时直接执行,不再增加触发前确认点。旧参数 `--acknowledge-hook-side-effects` 和 `--acknowledge-gate-side-effects` 只作为无效果的兼容输入保留。release-skill 会限制环境、路径、超时和证据输出,但不能替项目保证命令不会写文件或访问网络。恢复触发前强制确认门属于后续设计变更,0.7.6 不提供该确认门。Git push、tag、默认分支、GitHub Release 和 npm publish 只能是冻结计划里的 adapter action,不能注册为 hook/gate。
|
|
258
302
|
|
|
259
303
|
### 4.2 首次接入 setup
|
|
260
304
|
|
|
@@ -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,
|
|
@@ -13,7 +13,7 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
13
13
|
|
|
14
14
|
运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
|
|
15
15
|
|
|
16
|
-
**Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook
|
|
16
|
+
**Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。旧参数 `--acknowledge-hook-side-effects` 和 `--acknowledge-gate-side-effects` 仍可解析,但只作为无效果的兼容输入,不能改变授权或执行语义。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
|
|
17
17
|
|
|
18
18
|
**阶段通过规则**: 本阶段的通过只能由 CLI exit code 0 和结构化状态码 `PREPARED` 确认。Agent 无权自行宣布计划冻结成功。
|
|
19
19
|
|
|
@@ -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 不做
|
|
@@ -13,7 +13,7 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
13
13
|
|
|
14
14
|
运行项目构建/测试 hook,生成公开快照并扫描泄漏,冻结不可变发布计划。prepare 自身不调用发布 adapter,但会执行用户配置的 hook。
|
|
15
15
|
|
|
16
|
-
**Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook
|
|
16
|
+
**Hook 授权契约(配置时刻即授权,FM-16 处置 A)**: hook 是任意本地进程——在 `.release-skill/project.yaml` 中配置 hook 命令即完成授权,构成「配置时刻即授权」的显式契约。hook 不提供沙箱、无文件系统/网络隔离、触发前无确认点,命令调用本身即授权执行已配置的 hook 和 gate,不再设置额外人工授权环节;hook 可能产生项目目录外的副作用或远端写入,配置者须对其内容负责。旧参数 `--acknowledge-hook-side-effects` 和 `--acknowledge-gate-side-effects` 仍可解析,但只作为无效果的兼容输入,不能改变授权或执行语义。恢复「触发前强制确认门」属于后续加固项(属设计变更,需随新版本引入),当前版本不提供该确认门。
|
|
17
17
|
|
|
18
18
|
**阶段通过规则**: 本阶段的通过只能由 CLI exit code 0 和结构化状态码 `PREPARED` 确认。Agent 无权自行宣布计划冻结成功。
|
|
19
19
|
|
|
@@ -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'], {
|