release-skill 0.6.1 → 0.6.3
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/CONTRIBUTING.md +1 -1
- package/INSTALL.md +47 -2
- package/INSTALL.zh-CN.md +29 -2
- package/README.md +129 -9
- package/README.zh-CN.md +111 -9
- 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 +6596 -1586
- package/adapters/claude/schemas/.render-manifest.json +10 -6
- package/adapters/claude/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/claude/schemas/release-plan.schema.json +74 -1
- package/adapters/claude/schemas/release-project.schema.json +73 -1
- package/adapters/claude/schemas/release-run.schema.json +11 -6
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +6596 -1586
- package/adapters/codex/schemas/.render-manifest.json +10 -6
- package/adapters/codex/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/codex/schemas/release-plan.schema.json +74 -1
- package/adapters/codex/schemas/release-project.schema.json +73 -1
- package/adapters/codex/schemas/release-run.schema.json +11 -6
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +6596 -1586
- package/adapters/kimi/schemas/.render-manifest.json +10 -6
- package/adapters/kimi/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/kimi/schemas/release-plan.schema.json +74 -1
- package/adapters/kimi/schemas/release-project.schema.json +73 -1
- package/adapters/kimi/schemas/release-run.schema.json +11 -6
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +6596 -1586
- package/adapters/workbuddy/schemas/.render-manifest.json +10 -6
- package/adapters/workbuddy/schemas/postpublish-approval-record.schema.json +47 -0
- package/adapters/workbuddy/schemas/release-plan.schema.json +74 -1
- package/adapters/workbuddy/schemas/release-project.schema.json +73 -1
- package/adapters/workbuddy/schemas/release-run.schema.json +11 -6
- package/bin/release-skill-cli.mjs +181 -3
- package/bin/release-skill.bundle.mjs +6596 -1586
- package/package.json +2 -1
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +5 -5
- package/references/01-state-machine.md +22 -2
- package/schemas/.render-manifest.json +10 -6
- package/schemas/postpublish-approval-record.schema.json +47 -0
- package/schemas/release-plan.schema.json +74 -1
- package/schemas/release-project.schema.json +73 -1
- package/schemas/release-run.schema.json +11 -6
- package/src/commands/approve.mjs +167 -1
- package/src/commands/distribute.mjs +411 -33
- package/src/commands/lineage.mjs +101 -32
- package/src/commands/postverify.mjs +734 -0
- package/src/commands/prepare.mjs +339 -43
- package/src/commands/publish.mjs +10 -1
- package/src/commands/setup.mjs +715 -0
- package/src/commands/ship.mjs +152 -5
- package/src/commands/verify.mjs +92 -15
- package/src/core/approval.mjs +93 -68
- package/src/core/bounded-output.mjs +46 -0
- package/src/core/derived-artifact-gates.mjs +258 -0
- package/src/core/docs-refresh-preset.mjs +167 -0
- package/src/core/errors.mjs +4 -0
- package/src/core/hooks.mjs +28 -0
- package/src/core/marketplace-registry-entry.mjs +174 -0
- package/src/core/notify-handoff.mjs +76 -0
- package/src/core/postpublish-approval.mjs +110 -0
- package/src/core/postpublish.mjs +424 -7
- package/src/core/preset-executor.mjs +156 -0
- package/src/core/preset-gitwrite.mjs +463 -0
- package/src/core/presets.mjs +706 -0
- package/src/core/proposal-inbox.mjs +630 -0
- package/src/core/run.mjs +91 -6
- package/src/core/skill-resource-closure.mjs +240 -10
- package/src/platforms/registry.mjs +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "release-skill",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "广州市风荷科技有限公司"
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"sync:public-files:check": "node scripts/sync-public-files.mjs --check",
|
|
66
66
|
"build:adapters": "node scripts/build-adapters.mjs",
|
|
67
67
|
"build:adapters:check": "node scripts/build-adapters.mjs --check",
|
|
68
|
+
"build:adapters:apply": "node scripts/build-adapters.mjs --apply",
|
|
68
69
|
"build:bundle": "node scripts/build-bundle.mjs",
|
|
69
70
|
"build:bundle:check": "node scripts/build-bundle.mjs --check",
|
|
70
71
|
"native:build": "node-gyp rebuild --directory native/safe-write",
|
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.6.
|
|
5
|
+
"version": "0.6.3",
|
|
6
6
|
"logicalRoot": "skills-src/",
|
|
7
|
-
"logicalRootDigest": "
|
|
8
|
-
"projectionDigest": "
|
|
9
|
-
"projectionId": "release-skill:platform:0.6.
|
|
7
|
+
"logicalRootDigest": "3767fc36f42ca561f5e839cea7b7e5bc16d08c47867b5bd6086f25efd0cb0f06",
|
|
8
|
+
"projectionDigest": "419f7f9601015459956e10aae74f42240ab2ac248238ff7494f3e761643d1749",
|
|
9
|
+
"projectionId": "release-skill:platform:0.6.3",
|
|
10
10
|
"manifestRequired": true,
|
|
11
11
|
"projectionSources": {
|
|
12
12
|
"skills-src-root": {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"source": "
|
|
2
|
+
"source": "standards",
|
|
3
3
|
"files": {
|
|
4
4
|
"00-target-state.md": {
|
|
5
5
|
"digest": "d66f8c33b2df2bbcb235ce123f786c18c19b3a797197495793d9af566618099d",
|
|
6
6
|
"bytes": 6335
|
|
7
7
|
},
|
|
8
8
|
"01-state-machine.md": {
|
|
9
|
-
"digest": "
|
|
10
|
-
"bytes":
|
|
9
|
+
"digest": "711265e093be13f582ac8be89c391eff79ac22f56c25b91d1ccaef31856ef269",
|
|
10
|
+
"bytes": 12086
|
|
11
11
|
},
|
|
12
12
|
"02-project-config.md": {
|
|
13
13
|
"digest": "f866490d5897c2baf38fd3a6770964c052bea4418f034b75965ecfe49c2f4387",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"bytes": 4682
|
|
23
23
|
},
|
|
24
24
|
"05-evidence-and-errors.md": {
|
|
25
|
-
"digest": "
|
|
26
|
-
"bytes":
|
|
25
|
+
"digest": "f1352256e3b9e2c96c1aa6689fa921a909ab41f80832c5a3356a10590b640929",
|
|
26
|
+
"bytes": 12014
|
|
27
27
|
},
|
|
28
28
|
"06-adapter-contract.md": {
|
|
29
29
|
"digest": "4b1ec9ba843345df47c1d133a8bf1aa644bf12a4ecf760cb1d566a43f1e9a9d2",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 1. 状态定义
|
|
8
8
|
|
|
9
|
-
发布生命周期包含
|
|
9
|
+
发布生命周期包含 9 个主状态和 3 个异常状态,共计 12 个状态。
|
|
10
10
|
|
|
11
11
|
### 1.1 主状态
|
|
12
12
|
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
| **APPROVED** | 人工审阅并批准了冻结的发布计划 | 批准记录绑定到发布计划 hash,且未过期 |
|
|
19
19
|
| **PUBLISHING** | 外部写操作正在按检查点依次执行 | `publish` 命令读取已批准计划并开始执行 |
|
|
20
20
|
| **PUBLISHED** | 所有发布检查点均已成功完成 | 最后一个检查点的 `observe` 验证通过 |
|
|
21
|
+
| **DISTRIBUTING** | 发布后分发正在执行:载荷物化、目标镜像推送与 distribute 阶段 postPublish hooks 按检查点依次进行 | 计划声明 `postPublish`(targets 或 hooks),`ship` 路由到 `distribute` 并通过全部安全门后开始执行 |
|
|
22
|
+
| **DISTRIBUTED** | 分发完成:全部目标与 distribute 阶段 hooks 成功(或远端已一致而幂等跳过) | `distribute` run 无失败且无待批准检查点 |
|
|
21
23
|
| **VERIFIED** | 发布后验证全部通过 | `verify` 命令在全新环境中完成安装、调用、泄漏审计和远端一致性校验 |
|
|
22
24
|
|
|
23
25
|
### 1.2 异常状态
|
|
@@ -48,7 +50,15 @@
|
|
|
48
50
|
| PUBLISHING | PUBLISHED | 所有检查点成功 |
|
|
49
51
|
| PUBLISHING | PARTIAL | 至少一个检查点成功但后续失败 |
|
|
50
52
|
| PUBLISHING | BLOCKED | 外部服务阻断且无法继续 |
|
|
51
|
-
| PUBLISHED |
|
|
53
|
+
| PUBLISHED | DISTRIBUTING | 计划声明 `postPublish`(targets 或 hooks),`ship` 路由到 `distribute` 开始执行 |
|
|
54
|
+
| PUBLISHED | VERIFIED | 无 `postPublish` 声明时,`verify` 的远端、npm、Claude/Codex 等自动化检查全部通过(Kimi/CodeBuddy 人工安装任务不由系统核验且不阻塞);声明了 `postPublish` 时必须先经 DISTRIBUTING/DISTRIBUTED 门禁 |
|
|
55
|
+
| DISTRIBUTING | DISTRIBUTED | 全部目标与 distribute 阶段 hooks 成功(或远端已一致幂等跳过),且无待批准检查点 |
|
|
56
|
+
| DISTRIBUTING | PARTIAL | 至少一个外部 checkpoint 已成功后,目标/hooks 失败或存在待批准 checkpoint |
|
|
57
|
+
| DISTRIBUTING | NEEDS_INPUT | `requiresApproval` hook 缺少 checkpoint 级批准,且尚无任何外部副作用 |
|
|
58
|
+
| DISTRIBUTING | BLOCKED | 零外部写入落地且安全门或 hook 失败(失败关闭) |
|
|
59
|
+
| DISTRIBUTED | VERIFIED | `verify` 的 distribute 门禁通过:存在 DISTRIBUTED(或经 `blocksVerified:false` 合法豁免的 PARTIAL)分发 run,且安装/远端校验全部通过 |
|
|
60
|
+
| NEEDS_INPUT | DISTRIBUTING | checkpoint 批准补齐后重跑 `distribute` |
|
|
61
|
+
| PARTIAL | DISTRIBUTING | `distribute` 重跑即 reconcile:跳过已一致目标,仅重跑安全未完成项 |
|
|
52
62
|
| PUBLISHED | POST_PUBLISH_VERIFY_FAILED | 发布后验证失败(保留 PUBLISHED 事实但标记验证失败) |
|
|
53
63
|
| NEEDS_INPUT | DISCOVERED | 用户提供输入后重新开始评估 |
|
|
54
64
|
| NEEDS_INPUT | ASSESSED | 用户提供输入后恢复评估 |
|
|
@@ -66,6 +76,14 @@
|
|
|
66
76
|
- APPROVED -> PREPARED:当 plan digest、目标版本或已批准 action 列表变化时,批准自动失效。planVersion 2 计划的 digest 只绑定信任边界(冻结产物身份、动作列表、目标版本、配置摘要),工作区基线(Git tree hash、workspace digest)是记录层审计数据,不使批准失效;planVersion 1(旧版)计划维持旧规则,Git tree hash 与 workspace digest 变化同样使批准失效。
|
|
67
77
|
- PUBLISHING -> PARTIAL:当任一检查点成功后下一检查点失败时,自动计算 PARTIAL 状态。
|
|
68
78
|
|
|
79
|
+
### 2.2 分发阶段与 postVerify 阶段(v0.6.3)
|
|
80
|
+
|
|
81
|
+
postPublish hooks 分两个阶段,各自对应独立的 distribute run:
|
|
82
|
+
|
|
83
|
+
- **distribute 阶段**(默认 `phase: distribute`):在 DISTRIBUTING 中执行,载荷只能来自冻结 tagCommit 的 detached worktree(时序契约)。全部目标与 distribute 阶段 hooks 成功后进入 DISTRIBUTED;`verify` 在到达 VERIFIED 前检查 distribute 门禁——存在未 SUCCEEDED 且未经 `blocksVerified: false` 合法豁免的 hook 时不得 VERIFIED。NEEDS_INPUT/BLOCKED 不得静默转 VERIFIED(承袭治理规则)。
|
|
84
|
+
- **postVerify 阶段**(`phase: postVerify`):主 run VERIFIED 后由 ship/verify 路由触发,产出独立的 distribute run(复用现有 run 记录与 checkpoint 机制);失败即该 run PARTIAL、reconcile 可续。主 run 的 VERIFIED 不回退,但 postVerify run 的失败必须以证据显著记录,绝不静默。
|
|
85
|
+
- **checkpoint 级批准**:`requiresApproval: true` 的 hook 执行前需要绑定 `(planDigest, hookId)` 的独立批准记录(24h 过期、5 分钟时钟偏移容忍)。批准未获时:尚无外部 checkpoint 成功 → checkpoint 标记 `AWAITING_APPROVAL`、run 进 NEEDS_INPUT;已有外部 checkpoint 成功 → run 保持 PARTIAL。
|
|
86
|
+
|
|
69
87
|
---
|
|
70
88
|
|
|
71
89
|
## 3. 禁止转换
|
|
@@ -84,6 +102,8 @@
|
|
|
84
102
|
| APPROVED -> VERIFIED | 不能跳过发布直接验证 |
|
|
85
103
|
| APPROVED -> PUBLISHED | 不能跳过发布执行直接到已发布 |
|
|
86
104
|
| PUBLISHING -> VERIFIED | 发布完成后必须先到 PUBLISHED |
|
|
105
|
+
| DISTRIBUTING -> VERIFIED | 分发必须先达 DISTRIBUTED 再经 verify distribute 门禁 |
|
|
106
|
+
| DISTRIBUTING -> APPROVED | 分发进行中不能回退到批准 |
|
|
87
107
|
| PUBLISHING -> APPROVED | 发布进行中不能回退到批准 |
|
|
88
108
|
| PUBLISHING -> PREPARED | 发布进行中不能回退到准备 |
|
|
89
109
|
| PUBLISHED -> PUBLISHING | 已发布的不能重新发布同一计划 |
|
|
@@ -21,17 +21,21 @@
|
|
|
21
21
|
"digest": "b7e14522a5aba818cab191545f23d2323be0e4c80566e9d3e4da576103a00490",
|
|
22
22
|
"bytes": 2390
|
|
23
23
|
},
|
|
24
|
+
"postpublish-approval-record.schema.json": {
|
|
25
|
+
"digest": "e365c9e9f5aa189ca6788e68dbedfa718aa120f2c197534417c21e019b9a29a4",
|
|
26
|
+
"bytes": 1744
|
|
27
|
+
},
|
|
24
28
|
"release-plan.schema.json": {
|
|
25
|
-
"digest": "
|
|
26
|
-
"bytes":
|
|
29
|
+
"digest": "517ac9b1d92d2bf348497ee0e79c9e474a4c60e6ed8ba77830948716d143dfa2",
|
|
30
|
+
"bytes": 49369
|
|
27
31
|
},
|
|
28
32
|
"release-project.schema.json": {
|
|
29
|
-
"digest": "
|
|
30
|
-
"bytes":
|
|
33
|
+
"digest": "d38649d1dc9b1dbc643f4e2c98fd18d111090e9ce221c9c61fa50245f6c4c232",
|
|
34
|
+
"bytes": 45715
|
|
31
35
|
},
|
|
32
36
|
"release-run.schema.json": {
|
|
33
|
-
"digest": "
|
|
34
|
-
"bytes":
|
|
37
|
+
"digest": "a605db801d6994d657049431ad453233974bd10610b7b447b196136bb0692866",
|
|
38
|
+
"bytes": 17847
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://release-skill.dev/schemas/postpublish-approval-record/v1",
|
|
4
|
+
"title": "PostPublish Hook Approval Record",
|
|
5
|
+
"description": "Schema for checkpoint-level approval of a requiresApproval postPublish hook (v0.6.3 R1, design §2.7 ruling 2). Binds the approval to (planDigest, hookId); the plan-level approval-record schema is untouched. runId is audit-only and never participates in binding or validation semantics.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"planDigest",
|
|
9
|
+
"hookId",
|
|
10
|
+
"actor",
|
|
11
|
+
"approvedAt",
|
|
12
|
+
"expiresAt"
|
|
13
|
+
],
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"planDigest": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"pattern": "^[a-f0-9]{64}$",
|
|
19
|
+
"description": "SHA-256 digest of the frozen release plan whose postPublish hooks block this approval binds to"
|
|
20
|
+
},
|
|
21
|
+
"hookId": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$",
|
|
24
|
+
"description": "id of the requiresApproval postPublish hook being approved (must be declared in the frozen plan)"
|
|
25
|
+
},
|
|
26
|
+
"actor": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1,
|
|
29
|
+
"description": "Identity of the approver"
|
|
30
|
+
},
|
|
31
|
+
"approvedAt": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"format": "date-time",
|
|
34
|
+
"description": "ISO 8601 timestamp when approval was granted"
|
|
35
|
+
},
|
|
36
|
+
"expiresAt": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"format": "date-time",
|
|
39
|
+
"description": "ISO 8601 timestamp when approval expires (max 24h after approvedAt; 5-minute clock skew tolerated at validation)"
|
|
40
|
+
},
|
|
41
|
+
"runId": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1,
|
|
44
|
+
"description": "Audit-only identifier of the distribute run present when approval was granted; never a binding"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -863,6 +863,13 @@
|
|
|
863
863
|
"findingCount": {
|
|
864
864
|
"const": 0
|
|
865
865
|
},
|
|
866
|
+
"preparedAt": {
|
|
867
|
+
"type": ["string", "null"],
|
|
868
|
+
"description": "Deterministic freeze timestamp of the prepare run that produced this receipt (production: the baseline HEAD commit committer date in canonical UTC form; non-production: null). Never a wall-clock sample, so identical sources freeze byte-identical receipts."
|
|
869
|
+
},
|
|
870
|
+
"exitCode": {
|
|
871
|
+
"const": 0
|
|
872
|
+
},
|
|
866
873
|
"receiptDigest": {
|
|
867
874
|
"type": "string",
|
|
868
875
|
"pattern": "^[a-f0-9]{64}$"
|
|
@@ -877,6 +884,8 @@
|
|
|
877
884
|
"referenceCount",
|
|
878
885
|
"sourceOnlyCount",
|
|
879
886
|
"findingCount",
|
|
887
|
+
"preparedAt",
|
|
888
|
+
"exitCode",
|
|
880
889
|
"receiptDigest"
|
|
881
890
|
]
|
|
882
891
|
}
|
|
@@ -1298,7 +1307,6 @@
|
|
|
1298
1307
|
"description": "冻结的发布后分发块:声明本身 + tag/tagCommit/unitId/payloadSource 绑定。整体参与 plan digest(planVersion 2 记录层剥离不包含该块),任何配置变更都会改变 planDigest。tagCommit 仅在 prepare 能冻结 create-tag 预期 commit 时存在(production 资产);缺省时 distribute fail-closed。",
|
|
1299
1308
|
"required": [
|
|
1300
1309
|
"materialize",
|
|
1301
|
-
"targets",
|
|
1302
1310
|
"commitIdentity",
|
|
1303
1311
|
"tag",
|
|
1304
1312
|
"unitId",
|
|
@@ -1316,6 +1324,13 @@
|
|
|
1316
1324
|
"$ref": "#/definitions/planPostPublishTarget"
|
|
1317
1325
|
}
|
|
1318
1326
|
},
|
|
1327
|
+
"hooks": {
|
|
1328
|
+
"type": "array",
|
|
1329
|
+
"description": "冻结的发布后钩子声明(v0.6.3 R1);随整块参与 plan digest",
|
|
1330
|
+
"items": {
|
|
1331
|
+
"$ref": "#/definitions/planPostPublishHook"
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1319
1334
|
"commitIdentity": {
|
|
1320
1335
|
"type": "object",
|
|
1321
1336
|
"required": [
|
|
@@ -1364,6 +1379,64 @@
|
|
|
1364
1379
|
}
|
|
1365
1380
|
}
|
|
1366
1381
|
},
|
|
1382
|
+
"planPostPublishHook": {
|
|
1383
|
+
"type": "object",
|
|
1384
|
+
"description": "冻结的发布后钩子声明项(v0.6.3 R1):preset 与 command 运行时互斥;phase 缺省 distribute;blocksVerified 缺省 true 且仅 preset 可声明 false",
|
|
1385
|
+
"required": [
|
|
1386
|
+
"id"
|
|
1387
|
+
],
|
|
1388
|
+
"additionalProperties": false,
|
|
1389
|
+
"properties": {
|
|
1390
|
+
"id": {
|
|
1391
|
+
"type": "string",
|
|
1392
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
|
1393
|
+
},
|
|
1394
|
+
"preset": {
|
|
1395
|
+
"type": "string",
|
|
1396
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
|
1397
|
+
},
|
|
1398
|
+
"command": {
|
|
1399
|
+
"type": "array",
|
|
1400
|
+
"minItems": 1,
|
|
1401
|
+
"items": {
|
|
1402
|
+
"type": "string"
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
"phase": {
|
|
1406
|
+
"type": "string",
|
|
1407
|
+
"enum": [
|
|
1408
|
+
"distribute",
|
|
1409
|
+
"postVerify"
|
|
1410
|
+
]
|
|
1411
|
+
},
|
|
1412
|
+
"config": {
|
|
1413
|
+
"type": "object"
|
|
1414
|
+
},
|
|
1415
|
+
"cwd": {
|
|
1416
|
+
"type": "string",
|
|
1417
|
+
"pattern": "^(?:\\.$|(?!\\/)(?!\\.\\/)(?!.*\\\\)(?!.*:)(?!\\.\\.(?:\\/|$))(?!.*\\/\\.\\.\\/)(?!.*\\/\\.\\.$)(?!.*\\/\\.\\/)(?!.*\\/\\/)(?!.*\\0)(?!.*\\/\\.$)(?!.*\\/\\/$)(?!.*\\/$).*)$"
|
|
1418
|
+
},
|
|
1419
|
+
"timeoutMs": {
|
|
1420
|
+
"type": "integer",
|
|
1421
|
+
"minimum": 1000,
|
|
1422
|
+
"maximum": 7200000
|
|
1423
|
+
},
|
|
1424
|
+
"envAllowlist": {
|
|
1425
|
+
"type": "array",
|
|
1426
|
+
"items": {
|
|
1427
|
+
"type": "string",
|
|
1428
|
+
"pattern": "^(?!.*(?:TOKEN|SECRET|PASSWORD|PASSPHRASE|API_KEY|CREDENTIAL))[A-Z_][A-Z0-9_]*$"
|
|
1429
|
+
},
|
|
1430
|
+
"uniqueItems": true
|
|
1431
|
+
},
|
|
1432
|
+
"requiresApproval": {
|
|
1433
|
+
"type": "boolean"
|
|
1434
|
+
},
|
|
1435
|
+
"blocksVerified": {
|
|
1436
|
+
"type": "boolean"
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1367
1440
|
"planPostPublishMaterialize": {
|
|
1368
1441
|
"type": "object",
|
|
1369
1442
|
"required": [
|
|
@@ -889,7 +889,6 @@
|
|
|
889
889
|
"description": "发布后分发声明:materialize 钩子从冻结 tag 工作树物化载荷,targets 将载荷镜像或渲染市场索引推送到独立 git 远端。分发在 publish 之后由 distribute 命令执行,时序契约禁止读取工作区状态。",
|
|
890
890
|
"required": [
|
|
891
891
|
"materialize",
|
|
892
|
-
"targets",
|
|
893
892
|
"commitIdentity"
|
|
894
893
|
],
|
|
895
894
|
"additionalProperties": false,
|
|
@@ -904,6 +903,13 @@
|
|
|
904
903
|
"$ref": "#/definitions/postPublishTarget"
|
|
905
904
|
}
|
|
906
905
|
},
|
|
906
|
+
"hooks": {
|
|
907
|
+
"type": "array",
|
|
908
|
+
"description": "发布后钩子声明(v0.6.3 R1):每项为命名 preset(+config)或自定义 command 钩子(executable+参数数组,禁止 shell 字符串)。schema 层只校验形状;preset 与 command 互斥、preset 注册表归属等语义在运行时 fail-closed 校验(POSTPUBLISH_HOOK_INVALID)",
|
|
909
|
+
"items": {
|
|
910
|
+
"$ref": "#/definitions/postPublishHook"
|
|
911
|
+
}
|
|
912
|
+
},
|
|
907
913
|
"commitIdentity": {
|
|
908
914
|
"type": "object",
|
|
909
915
|
"description": "分发仓库提交者身份(bot 身份),通过 git -c user.name/-c user.email 注入,绝不读取或修改全局 git 配置",
|
|
@@ -936,6 +942,72 @@
|
|
|
936
942
|
}
|
|
937
943
|
}
|
|
938
944
|
},
|
|
945
|
+
"postPublishHook": {
|
|
946
|
+
"type": "object",
|
|
947
|
+
"description": "发布后钩子声明项:preset(命名预设 + config)与 command(自定义命令)在运行时互斥校验;phase 缺省为 distribute(VERIFIED 之前执行),postVerify 在 verify 通过后作为独立 run 执行(R3);blocksVerified 缺省 true,仅 preset 可声明 false",
|
|
948
|
+
"required": [
|
|
949
|
+
"id"
|
|
950
|
+
],
|
|
951
|
+
"additionalProperties": false,
|
|
952
|
+
"properties": {
|
|
953
|
+
"id": {
|
|
954
|
+
"type": "string",
|
|
955
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
|
956
|
+
},
|
|
957
|
+
"preset": {
|
|
958
|
+
"type": "string",
|
|
959
|
+
"pattern": "^[a-z0-9][a-z0-9._-]*$",
|
|
960
|
+
"description": "预设注册表中的预设名(R2 起);R1 无注册表,任何 preset 引用在运行时 fail-closed"
|
|
961
|
+
},
|
|
962
|
+
"command": {
|
|
963
|
+
"type": "array",
|
|
964
|
+
"minItems": 1,
|
|
965
|
+
"items": {
|
|
966
|
+
"type": "string"
|
|
967
|
+
},
|
|
968
|
+
"description": "executable+参数数组(禁止 shell 字符串);在冻结 tag 工作树中执行"
|
|
969
|
+
},
|
|
970
|
+
"phase": {
|
|
971
|
+
"type": "string",
|
|
972
|
+
"enum": [
|
|
973
|
+
"distribute",
|
|
974
|
+
"postVerify"
|
|
975
|
+
],
|
|
976
|
+
"description": "执行阶段;缺省 distribute"
|
|
977
|
+
},
|
|
978
|
+
"config": {
|
|
979
|
+
"type": "object",
|
|
980
|
+
"description": "preset 钩子的配置对象;语义由预设注册表校验(R2 起)"
|
|
981
|
+
},
|
|
982
|
+
"cwd": {
|
|
983
|
+
"type": "string",
|
|
984
|
+
"pattern": "^(?:\\.$|(?!\\/)(?!\\.\\/)(?!.*\\\\)(?!.*:)(?!\\.\\.(?:\\/|$))(?!.*\\/\\.\\.\\/)(?!.*\\/\\.\\.$)(?!.*\\/\\.\\/)(?!.*\\/\\/)(?!.*\\0)(?!.*\\/\\.$)(?!.*\\/\\/$)(?!.*\\/$).*)$",
|
|
985
|
+
"description": "command 钩子执行目录(相对冻结 tag 工作树);preset 钩子禁止声明(运行时 fail-closed)"
|
|
986
|
+
},
|
|
987
|
+
"timeoutMs": {
|
|
988
|
+
"type": "integer",
|
|
989
|
+
"minimum": 1000,
|
|
990
|
+
"maximum": 7200000
|
|
991
|
+
},
|
|
992
|
+
"envAllowlist": {
|
|
993
|
+
"type": "array",
|
|
994
|
+
"items": {
|
|
995
|
+
"type": "string",
|
|
996
|
+
"pattern": "^(?!.*(?:TOKEN|SECRET|PASSWORD|PASSPHRASE|API_KEY|CREDENTIAL))[A-Z_][A-Z0-9_]*$"
|
|
997
|
+
},
|
|
998
|
+
"uniqueItems": true,
|
|
999
|
+
"description": "允许透传给钩子进程的环境变量白名单;TOKEN/SECRET 等敏感子串在 schema 层即被拒绝"
|
|
1000
|
+
},
|
|
1001
|
+
"requiresApproval": {
|
|
1002
|
+
"type": "boolean",
|
|
1003
|
+
"description": "执行前需要 checkpoint 级批准(绑定 planDigest + hookId);public-write preset 缺省 true"
|
|
1004
|
+
},
|
|
1005
|
+
"blocksVerified": {
|
|
1006
|
+
"type": "boolean",
|
|
1007
|
+
"description": "失败时是否阻断 VERIFIED;缺省 true;仅 preset 可声明 false(运行时 fail-closed 校验)"
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
939
1011
|
"postPublishMaterialize": {
|
|
940
1012
|
"type": "object",
|
|
941
1013
|
"description": "载荷物化钩子:executable+参数数组(禁止 shell 字符串),stdout 中 outputMarker 行之后为载荷目录路径;可选 requireReport 对 stdout 首个 JSON 对象做字段断言",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"PARTIAL",
|
|
44
44
|
"BLOCKED",
|
|
45
45
|
"DISTRIBUTING",
|
|
46
|
-
"DISTRIBUTED"
|
|
46
|
+
"DISTRIBUTED",
|
|
47
|
+
"NEEDS_INPUT"
|
|
47
48
|
],
|
|
48
49
|
"description": "Current status of the run"
|
|
49
50
|
},
|
|
@@ -55,7 +56,8 @@
|
|
|
55
56
|
"publish",
|
|
56
57
|
"reconcile",
|
|
57
58
|
"verify",
|
|
58
|
-
"distribute"
|
|
59
|
+
"distribute",
|
|
60
|
+
"postverify"
|
|
59
61
|
],
|
|
60
62
|
"description": "The command that initiated this run"
|
|
61
63
|
},
|
|
@@ -133,7 +135,8 @@
|
|
|
133
135
|
"codebuddy-marketplace-install",
|
|
134
136
|
"set-default-branch",
|
|
135
137
|
"distribute-probe",
|
|
136
|
-
"distribute-mirror"
|
|
138
|
+
"distribute-mirror",
|
|
139
|
+
"postpublish-hook"
|
|
137
140
|
],
|
|
138
141
|
"description": "Type of external action"
|
|
139
142
|
},
|
|
@@ -149,9 +152,10 @@
|
|
|
149
152
|
"PASSED_AUTOMATIC",
|
|
150
153
|
"PASSED_MANUAL",
|
|
151
154
|
"NOT_REQUIRED_UNCHANGED",
|
|
152
|
-
"NO_CHANGE"
|
|
155
|
+
"NO_CHANGE",
|
|
156
|
+
"AWAITING_APPROVAL"
|
|
153
157
|
],
|
|
154
|
-
"description": "Result status of this checkpoint (uncertain = started, result unknown; deferred = marketplace action postponed to verify phase; PASSED_AUTOMATIC/PASSED_MANUAL/NOT_REQUIRED_UNCHANGED 为验证阶段专用状态; NO_CHANGE = distribute 目标暂存差异为空,未产生 commit/tag/push)"
|
|
158
|
+
"description": "Result status of this checkpoint (uncertain = started, result unknown; deferred = marketplace action postponed to verify phase; PASSED_AUTOMATIC/PASSED_MANUAL/NOT_REQUIRED_UNCHANGED 为验证阶段专用状态; NO_CHANGE = distribute 目标暂存差异为空,未产生 commit/tag/push; AWAITING_APPROVAL = requiresApproval postPublish hook 尚无 checkpoint 批准,v0.6.3 R1)"
|
|
155
159
|
},
|
|
156
160
|
"preObserve": {
|
|
157
161
|
"type": "string",
|
|
@@ -621,7 +625,8 @@
|
|
|
621
625
|
"enum": [
|
|
622
626
|
"reconcile",
|
|
623
627
|
"verify",
|
|
624
|
-
"distribute"
|
|
628
|
+
"distribute",
|
|
629
|
+
"postverify"
|
|
625
630
|
]
|
|
626
631
|
}
|
|
627
632
|
},
|
package/src/commands/approve.mjs
CHANGED
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
validatePlanActionCompleteness,
|
|
26
26
|
} from '../core/plan.mjs';
|
|
27
27
|
import { ReleaseError, PLAN_DIGEST_MISMATCH, GATE_FAILED } from '../core/errors.mjs';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
computeApprovalDigest,
|
|
30
|
+
validateApprovalRecordSchema,
|
|
31
|
+
MAX_APPROVAL_MS,
|
|
32
|
+
} from '../core/approval.mjs';
|
|
33
|
+
import { validatePostPublishApproval } from '../core/postpublish-approval.mjs';
|
|
34
|
+
import { normalizePostPublishHook, effectiveHookRequiresApproval } from '../core/postpublish.mjs';
|
|
29
35
|
import { WORKSPACE_DIGEST_ALGORITHM } from '../core/baseline.mjs';
|
|
30
36
|
|
|
31
37
|
// ---------------------------------------------------------------------------
|
|
@@ -280,3 +286,163 @@ export async function approvePlan(options) {
|
|
|
280
286
|
latestApprovalPath: writePath,
|
|
281
287
|
});
|
|
282
288
|
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Approve a requiresApproval postPublish hook (checkpoint-level approval,
|
|
292
|
+
* v0.6.3 R1, design §2.7 ruling 2).
|
|
293
|
+
*
|
|
294
|
+
* The record binds (planDigest, hookId) — the plan-level approval schema is
|
|
295
|
+
* untouched. Hook config changes change the plan digest, so approvals
|
|
296
|
+
* invalidate naturally with the plan. The result carries the approved hook's
|
|
297
|
+
* normalized entry summary (review N-6) so the checkpoint approval interface
|
|
298
|
+
* can display exactly what was approved.
|
|
299
|
+
*
|
|
300
|
+
* @param {Object} options
|
|
301
|
+
* @param {string} options.planPath - Absolute path to the frozen release plan.
|
|
302
|
+
* @param {string} options.hookId - id of the declared hook to approve.
|
|
303
|
+
* @param {string} options.actor - Identity of the approver.
|
|
304
|
+
* @param {number} [options.expiresInMs] - Validity window in ms (default and max 24h).
|
|
305
|
+
* @param {() => string} [options.clock] - Clock function returning ISO-8601 strings.
|
|
306
|
+
* @param {string} [options.runId] - Audit-only distribute run id (never a binding).
|
|
307
|
+
*
|
|
308
|
+
* @returns {Promise<object>} { planDigest, hookId, actor, approvedAt,
|
|
309
|
+
* expiresAt, approvalDigest, approvalPath, hookSummary }.
|
|
310
|
+
*
|
|
311
|
+
* @throws {ReleaseError} GATE_FAILED when the hook is undeclared, does not
|
|
312
|
+
* require approval, or the window exceeds 24h.
|
|
313
|
+
*/
|
|
314
|
+
export async function approvePostPublishHook(options) {
|
|
315
|
+
const {
|
|
316
|
+
planPath,
|
|
317
|
+
hookId,
|
|
318
|
+
actor,
|
|
319
|
+
expiresInMs = MAX_APPROVAL_MS,
|
|
320
|
+
clock,
|
|
321
|
+
runId,
|
|
322
|
+
} = options ?? {};
|
|
323
|
+
|
|
324
|
+
const clockFn = typeof clock === 'function' ? clock : defaultClock;
|
|
325
|
+
|
|
326
|
+
if (!planPath || typeof planPath !== 'string') {
|
|
327
|
+
throw new ReleaseError(GATE_FAILED, 'planPath must be a non-empty string');
|
|
328
|
+
}
|
|
329
|
+
if (!hookId || typeof hookId !== 'string') {
|
|
330
|
+
throw new ReleaseError(GATE_FAILED, 'hookId must be a non-empty string');
|
|
331
|
+
}
|
|
332
|
+
if (!actor || typeof actor !== 'string') {
|
|
333
|
+
throw new ReleaseError(GATE_FAILED, 'actor must be a non-empty string');
|
|
334
|
+
}
|
|
335
|
+
if (typeof expiresInMs !== 'number' || !Number.isFinite(expiresInMs) || expiresInMs <= 0) {
|
|
336
|
+
throw new ReleaseError(GATE_FAILED, 'expiresInMs must be a positive finite number', { expiresInMs });
|
|
337
|
+
}
|
|
338
|
+
if (expiresInMs > MAX_APPROVAL_MS) {
|
|
339
|
+
throw new ReleaseError(
|
|
340
|
+
GATE_FAILED,
|
|
341
|
+
`expiresInMs must not exceed 24 hours (${MAX_APPROVAL_MS}ms), got ${expiresInMs}ms`,
|
|
342
|
+
{ expiresInMs },
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- Load and validate the frozen plan ---
|
|
347
|
+
let planRaw;
|
|
348
|
+
try {
|
|
349
|
+
planRaw = await readFile(planPath, 'utf8');
|
|
350
|
+
} catch (err) {
|
|
351
|
+
throw new ReleaseError(GATE_FAILED, `cannot read release plan: ${err.message}`, { planPath, cause: err.code });
|
|
352
|
+
}
|
|
353
|
+
let plan;
|
|
354
|
+
try {
|
|
355
|
+
plan = JSON.parse(planRaw);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
throw new ReleaseError(GATE_FAILED, `release plan is not valid JSON: ${err.message}`, { planPath });
|
|
358
|
+
}
|
|
359
|
+
const actualDigest = computePlanDigest(plan);
|
|
360
|
+
assertImmutablePlanAuthority(planPath, plan);
|
|
361
|
+
validatePlan(plan);
|
|
362
|
+
|
|
363
|
+
// --- Locate the declared hook (fail-closed) ---
|
|
364
|
+
const hooks = plan.postPublish?.hooks ?? [];
|
|
365
|
+
const hook = hooks.find((entry) => entry.id === hookId);
|
|
366
|
+
if (!hook) {
|
|
367
|
+
throw new ReleaseError(
|
|
368
|
+
GATE_FAILED,
|
|
369
|
+
`hook "${hookId}" is not declared in the frozen plan's postPublish hooks`,
|
|
370
|
+
{ hookId, declaredHookIds: hooks.map((entry) => entry.id) },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
// Effective requiresApproval (§2.6 grading, R4 review M-1): preset hooks
|
|
374
|
+
// may inherit the preset-declared default (proposal-inbox git-push,
|
|
375
|
+
// declared OR inferred from a remoteUrl address -> true) without an
|
|
376
|
+
// explicit declaration. Grading by the literal value here would refuse to
|
|
377
|
+
// mint approvals for exactly the hooks the runtime gates — the mint and
|
|
378
|
+
// validatePostPublishApproval must agree on the effective value.
|
|
379
|
+
const effectiveRequiresApproval = effectiveHookRequiresApproval(hook);
|
|
380
|
+
if (effectiveRequiresApproval !== true) {
|
|
381
|
+
throw new ReleaseError(
|
|
382
|
+
GATE_FAILED,
|
|
383
|
+
`hook "${hookId}" does not require approval (effective requiresApproval is not true)`,
|
|
384
|
+
{ hookId, requiresApproval: effectiveRequiresApproval },
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --- Build the checkpoint approval record ---
|
|
389
|
+
const approvedAt = clockFn();
|
|
390
|
+
const approvedAtDate = new Date(approvedAt);
|
|
391
|
+
if (Number.isNaN(approvedAtDate.getTime())) {
|
|
392
|
+
throw new ReleaseError(GATE_FAILED, `invalid approvedAt timestamp: "${approvedAt}"`, { approvedAt });
|
|
393
|
+
}
|
|
394
|
+
const expiresAt = new Date(approvedAtDate.getTime() + expiresInMs).toISOString();
|
|
395
|
+
|
|
396
|
+
const approvalRecord = {
|
|
397
|
+
planDigest: actualDigest,
|
|
398
|
+
hookId,
|
|
399
|
+
actor,
|
|
400
|
+
approvedAt,
|
|
401
|
+
expiresAt,
|
|
402
|
+
...(runId ? { runId } : {}),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Full fail-closed re-validation (schema + bindings + time window).
|
|
406
|
+
validatePostPublishApproval(plan, approvalRecord, { clock: clockFn });
|
|
407
|
+
|
|
408
|
+
// --- Write the digest-addressed authority ---
|
|
409
|
+
const planDir = dirname(resolve(planPath));
|
|
410
|
+
const releaseDir = basename(planDir) === 'plans' && basename(planPath) === `${actualDigest}.json`
|
|
411
|
+
? dirname(planDir)
|
|
412
|
+
: planDir;
|
|
413
|
+
const json = JSON.stringify(approvalRecord, null, 2);
|
|
414
|
+
const approvalDigest = computeApprovalDigest(json);
|
|
415
|
+
const immutableApprovalPath = resolve(
|
|
416
|
+
releaseDir,
|
|
417
|
+
'approvals',
|
|
418
|
+
'postpublish',
|
|
419
|
+
actualDigest,
|
|
420
|
+
`${approvalDigest}.json`,
|
|
421
|
+
);
|
|
422
|
+
await prepareAuthorityDirectory(dirname(immutableApprovalPath));
|
|
423
|
+
await assertAuthorityFileTarget(immutableApprovalPath);
|
|
424
|
+
try {
|
|
425
|
+
await writeFile(immutableApprovalPath, json, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error.code !== 'EEXIST') throw error;
|
|
428
|
+
const existing = await readFile(immutableApprovalPath, 'utf8');
|
|
429
|
+
if (existing !== json) {
|
|
430
|
+
throw new ReleaseError(
|
|
431
|
+
GATE_FAILED,
|
|
432
|
+
'postpublish approval authority digest collision: existing bytes differ',
|
|
433
|
+
{ planDigest: actualDigest, approvalDigest, immutableApprovalPath },
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Review N-6: the normalized entry summary is the display surface of the
|
|
439
|
+
// checkpoint approval interface (defaults applied, digest-bound).
|
|
440
|
+
const hookSummary = normalizePostPublishHook(hook);
|
|
441
|
+
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
...approvalRecord,
|
|
444
|
+
approvalDigest,
|
|
445
|
+
approvalPath: immutableApprovalPath,
|
|
446
|
+
hookSummary,
|
|
447
|
+
});
|
|
448
|
+
}
|