release-skill 0.7.6 → 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 +20 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +15 -12
- package/README.zh-CN.md +14 -11
- 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 +1725 -1156
- 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 +4 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1725 -1156
- 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-prepare/SKILL.md +4 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1725 -1156
- 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 +4 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1725 -1156
- 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 +4 -0
- package/bin/release-skill-cli.mjs +116 -47
- package/bin/release-skill.bundle.mjs +1725 -1156
- package/package.json +1 -1
- package/platform-manifest.json +4 -4
- package/references/02-project-config.md +18 -0
- package/schemas/release-plan.schema.json +47 -0
- package/schemas/release-project.schema.json +24 -0
- 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 +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/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": {
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -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'], {
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { promisify } from 'node:util';
|
|
|
25
25
|
|
|
26
26
|
const execFile = promisify(execFileCb);
|
|
27
27
|
|
|
28
|
-
import { classifyPathInput } from 'skill-family-harness-node';
|
|
28
|
+
import { classifyPathInput, writeFileAtomic } from 'skill-family-harness-node';
|
|
29
29
|
import { loadProjectConfig } from '../core/config.mjs';
|
|
30
30
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
31
31
|
import { runHook } from '../core/hooks.mjs';
|
|
@@ -62,6 +62,7 @@ import { PKG_ROOT } from '../core/pkg-root.mjs';
|
|
|
62
62
|
import { writeFrozenMarker, FROZEN_MARKER_FILENAME } from '../core/frozen-marker.mjs';
|
|
63
63
|
import {
|
|
64
64
|
SOURCE_INPUT_ALGORITHM_VERSION,
|
|
65
|
+
createPublicSourceAuthorityReceipt,
|
|
65
66
|
computeSourceInputClosure,
|
|
66
67
|
checkSourceInputDirty,
|
|
67
68
|
verifySnapshotSourcesMatchClosure,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
PAYLOAD_SOURCE_TAG_WORKTREE,
|
|
81
82
|
} from '../core/postpublish.mjs';
|
|
82
83
|
import { freezeExecutionBundle, bundleRootForAuthorityDir } from '../core/postpublish-bundle.mjs';
|
|
84
|
+
import { digestReleaseAssetIdentities } from '../core/release-assets.mjs';
|
|
83
85
|
|
|
84
86
|
// ---------------------------------------------------------------------------
|
|
85
87
|
// 安装契约常量
|
|
@@ -1513,7 +1515,7 @@ export async function resolveExternalMarketplaceFreezes({
|
|
|
1513
1515
|
* @param {string} realRoot - The project root for relative path calculation.
|
|
1514
1516
|
* @returns {object[]} Array of external action descriptors.
|
|
1515
1517
|
*/
|
|
1516
|
-
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null) {
|
|
1518
|
+
export function buildExternalActions(unitResults, resolvedVersions, productionAssets, externalFreezes = new Map(), frozenDistributions = null, publicSourceAuthorityReceipt = null) {
|
|
1517
1519
|
const actions = [];
|
|
1518
1520
|
|
|
1519
1521
|
if (!productionAssets) {
|
|
@@ -1785,6 +1787,9 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1785
1787
|
}
|
|
1786
1788
|
|
|
1787
1789
|
// GitHub release
|
|
1790
|
+
const releaseAssets = publicSourceAuthorityReceipt?.coordinatorUnitId === unit.id
|
|
1791
|
+
? [publicSourceAuthorityReceipt.asset]
|
|
1792
|
+
: null;
|
|
1788
1793
|
actions.push({
|
|
1789
1794
|
id: `github-release-${unit.id}`,
|
|
1790
1795
|
type: 'github-release',
|
|
@@ -1802,10 +1807,12 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
|
|
|
1802
1807
|
.replaceAll('{version}', unitVersion)
|
|
1803
1808
|
.replaceAll('{unit}', unit.id),
|
|
1804
1809
|
notes: unit.production?.releaseNotes ?? `Release ${resolvedTag}`,
|
|
1810
|
+
...(releaseAssets ? { releaseAssets } : {}),
|
|
1805
1811
|
},
|
|
1806
1812
|
expected: {
|
|
1807
1813
|
tag: resolvedTag,
|
|
1808
1814
|
commit: asset.commit,
|
|
1815
|
+
...(releaseAssets ? { releaseAssetsDigest: digestReleaseAssetIdentities(releaseAssets) } : {}),
|
|
1809
1816
|
},
|
|
1810
1817
|
status: 'PENDING',
|
|
1811
1818
|
});
|
|
@@ -3648,7 +3655,68 @@ export async function prepareRelease(options) {
|
|
|
3648
3655
|
// 构建冻结分发映射:unitId -> frozen distributions
|
|
3649
3656
|
const frozenDistributionsMap = new Map(units.map((u) => [u.id, u.distributions]));
|
|
3650
3657
|
|
|
3651
|
-
let
|
|
3658
|
+
let publicSourceAuthorityReceipt = null;
|
|
3659
|
+
if (config.publicSourceAuthorityReceipt) {
|
|
3660
|
+
if (!production || !sourceAuthority) {
|
|
3661
|
+
throw new ReleaseError(
|
|
3662
|
+
CONFIG_INVALID,
|
|
3663
|
+
'publicSourceAuthorityReceipt requires a production prepare with source-authority closure enabled',
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
const unitsById = new Map(units.map((unit) => [unit.id, unit]));
|
|
3667
|
+
const subjects = config.publicSourceAuthorityReceipt.subjectUnitIds.map((unitId) => {
|
|
3668
|
+
const unit = unitsById.get(unitId);
|
|
3669
|
+
const npmDistribution = unit?.distributions?.find((distribution) => distribution.type === 'npm');
|
|
3670
|
+
const npm = unit?.frozenSnapshot?.npm;
|
|
3671
|
+
if (!unit || !npmDistribution || !npm) {
|
|
3672
|
+
throw new ReleaseError(
|
|
3673
|
+
GATE_FAILED,
|
|
3674
|
+
`public source-authority subject unit "${unitId}" is missing its frozen npm tarball`,
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
return {
|
|
3678
|
+
packageName: npmDistribution.package,
|
|
3679
|
+
version: unit.targetVersion,
|
|
3680
|
+
filename: basename(npm.tarballPath),
|
|
3681
|
+
sha256: npm.tarballSha256,
|
|
3682
|
+
};
|
|
3683
|
+
});
|
|
3684
|
+
const builtReceipt = createPublicSourceAuthorityReceipt({
|
|
3685
|
+
sourceRepository: sourceAuthority.sourceRepository,
|
|
3686
|
+
sourceBaseCommit: baseline.gitHead,
|
|
3687
|
+
subjects,
|
|
3688
|
+
});
|
|
3689
|
+
const assetName = 'source-authority-receipt.json';
|
|
3690
|
+
const assetDirectory = resolve(runDir, 'release-assets');
|
|
3691
|
+
await mkdir(assetDirectory, { recursive: true });
|
|
3692
|
+
await writeFileAtomic(assetDirectory, assetName, builtReceipt.bytes, { mode: 0o644 });
|
|
3693
|
+
publicSourceAuthorityReceipt = {
|
|
3694
|
+
coordinatorUnitId: config.publicSourceAuthorityReceipt.coordinatorUnitId,
|
|
3695
|
+
subjectUnitIds: [...config.publicSourceAuthorityReceipt.subjectUnitIds],
|
|
3696
|
+
asset: {
|
|
3697
|
+
name: assetName,
|
|
3698
|
+
path: relative(realRoot, resolve(assetDirectory, assetName)),
|
|
3699
|
+
sha256: builtReceipt.sha256,
|
|
3700
|
+
},
|
|
3701
|
+
};
|
|
3702
|
+
await evidence.append({
|
|
3703
|
+
phase: 'public-source-authority-receipt',
|
|
3704
|
+
status: 'frozen',
|
|
3705
|
+
coordinatorUnitId: publicSourceAuthorityReceipt.coordinatorUnitId,
|
|
3706
|
+
subjectUnitIds: publicSourceAuthorityReceipt.subjectUnitIds,
|
|
3707
|
+
assetPath: publicSourceAuthorityReceipt.asset.path,
|
|
3708
|
+
assetSha256: publicSourceAuthorityReceipt.asset.sha256,
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
let externalActions = buildExternalActions(
|
|
3713
|
+
unitResults,
|
|
3714
|
+
resolvedVersions,
|
|
3715
|
+
productionAssets,
|
|
3716
|
+
externalMarketplaceFreezes,
|
|
3717
|
+
frozenDistributionsMap,
|
|
3718
|
+
publicSourceAuthorityReceipt,
|
|
3719
|
+
);
|
|
3652
3720
|
|
|
3653
3721
|
// Compute overall snapshot digest
|
|
3654
3722
|
const overallSnapshotDigest = sha256Hex(snapshotDigests.join(':'));
|
|
@@ -3823,6 +3891,7 @@ export async function prepareRelease(options) {
|
|
|
3823
3891
|
} : {}),
|
|
3824
3892
|
units,
|
|
3825
3893
|
externalActions,
|
|
3894
|
+
...(publicSourceAuthorityReceipt ? { publicSourceAuthorityReceipt } : {}),
|
|
3826
3895
|
...(frozenPostPublish ? { postPublish: frozenPostPublish } : {}),
|
|
3827
3896
|
...(sourceAuthority ? { sourceAuthority } : {}),
|
|
3828
3897
|
createdAt: production ? createdAtTimestamp : (clock ? clock() : new Date().toISOString()),
|
package/src/commands/publish.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
GATE_FAILED,
|
|
62
62
|
CONFIG_MISSING,
|
|
63
63
|
BASELINE_CHANGED,
|
|
64
|
+
CONTENT_MISMATCH,
|
|
64
65
|
PARTIAL_RELEASE,
|
|
65
66
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
66
67
|
} from '../core/errors.mjs';
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
verifyFrozenSnapshot,
|
|
79
80
|
} from '../snapshot/frozen.mjs';
|
|
80
81
|
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
82
|
+
import { normalizeReleaseAssets } from '../core/release-assets.mjs';
|
|
81
83
|
|
|
82
84
|
function assertInsideAssetRoot(assetRoot, candidate, label) {
|
|
83
85
|
const rel = relative(assetRoot, candidate);
|
|
@@ -608,6 +610,17 @@ export async function publishRelease(options) {
|
|
|
608
610
|
}, root);
|
|
609
611
|
}
|
|
610
612
|
}
|
|
613
|
+
for (const action of plan.externalActions ?? []) {
|
|
614
|
+
for (const releaseAsset of normalizeReleaseAssets(action.parameters?.releaseAssets)) {
|
|
615
|
+
const verifiedAsset = await verifyFrozenFile({
|
|
616
|
+
root,
|
|
617
|
+
filePath: releaseAsset.path,
|
|
618
|
+
expectedSha256: releaseAsset.sha256,
|
|
619
|
+
label: `frozen GitHub Release asset "${releaseAsset.name}"`,
|
|
620
|
+
});
|
|
621
|
+
assertInsideAssetRoot(assetRoot, verifiedAsset.physical, 'frozen GitHub Release asset');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
611
624
|
await evidence.append({ phase: 'safety-gate', gate: 'frozen-artifacts', status: 'passed' });
|
|
612
625
|
}
|
|
613
626
|
|
|
@@ -987,6 +1000,29 @@ export async function publishRelease(options) {
|
|
|
987
1000
|
);
|
|
988
1001
|
}
|
|
989
1002
|
|
|
1003
|
+
if (
|
|
1004
|
+
plan.publicSourceAuthorityReceipt
|
|
1005
|
+
&& remoteResult.observation?.observedCommit !== plan.baseline.headCommit
|
|
1006
|
+
) {
|
|
1007
|
+
const observedCommit = remoteResult.observation?.observedCommit ?? null;
|
|
1008
|
+
await evidence.append({
|
|
1009
|
+
phase: 'safety-gate',
|
|
1010
|
+
gate: 'source-authority',
|
|
1011
|
+
status: 'failed',
|
|
1012
|
+
expectedCommit: plan.baseline.headCommit,
|
|
1013
|
+
observedCommit,
|
|
1014
|
+
});
|
|
1015
|
+
throw new ReleaseError(
|
|
1016
|
+
CONTENT_MISMATCH,
|
|
1017
|
+
'source authority commit gate failed: remote observedCommit does not match the public receipt sourceBaseCommit',
|
|
1018
|
+
{
|
|
1019
|
+
gate: 'source-authority',
|
|
1020
|
+
expectedCommit: plan.baseline.headCommit,
|
|
1021
|
+
observedCommit,
|
|
1022
|
+
},
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
990
1026
|
// Create receipt for successful verification
|
|
991
1027
|
sourceAuthorityReceipt = createSourceAuthorityReceipt({
|
|
992
1028
|
plan,
|
package/src/commands/verify.mjs
CHANGED
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
POST_PUBLISH_VERIFY_FAILED,
|
|
47
47
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
48
48
|
} from '../core/errors.mjs';
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
consumePublicSourceAuthorityReceipt,
|
|
51
|
+
verifySourceAuthorityReceipt,
|
|
52
|
+
} from '../core/source-authority.mjs';
|
|
53
|
+
import { verifyFrozenFile } from '../snapshot/frozen.mjs';
|
|
50
54
|
import {
|
|
51
55
|
deriveBaselineAdvances,
|
|
52
56
|
applyBaselineAdvances,
|
|
@@ -61,6 +65,7 @@ import {
|
|
|
61
65
|
normalizeRegistry,
|
|
62
66
|
registryTokenKey,
|
|
63
67
|
resolveNpmRegistryAuthToken,
|
|
68
|
+
verifyFrozenNpmTarballContract,
|
|
64
69
|
} from '../adapters/npm.mjs';
|
|
65
70
|
import { runConsumerVerificationGates } from '../core/verification-gates.mjs';
|
|
66
71
|
import {
|
|
@@ -109,6 +114,40 @@ export const VERIFICATION_RESOLVED_TYPES = Object.freeze({
|
|
|
109
114
|
NOT_REQUIRED_UNCHANGED: 'NOT_REQUIRED_UNCHANGED',
|
|
110
115
|
});
|
|
111
116
|
|
|
117
|
+
/** Re-read every frozen subject tarball before consuming the public receipt. */
|
|
118
|
+
export async function verifyPublicSourceAuthorityReceiptOffline({ plan, root }) {
|
|
119
|
+
const descriptor = plan.publicSourceAuthorityReceipt;
|
|
120
|
+
const verifiedAsset = await verifyFrozenFile({
|
|
121
|
+
root,
|
|
122
|
+
filePath: descriptor.asset.path,
|
|
123
|
+
expectedSha256: descriptor.asset.sha256,
|
|
124
|
+
label: 'public source-authority receipt',
|
|
125
|
+
});
|
|
126
|
+
const actualSubjects = [];
|
|
127
|
+
for (const unitId of descriptor.subjectUnitIds) {
|
|
128
|
+
const unit = plan.units.find((candidate) => candidate.id === unitId);
|
|
129
|
+
const npmDistribution = unit?.distributions?.find((distribution) => distribution.type === 'npm');
|
|
130
|
+
const npm = unit?.frozenSnapshot?.npm;
|
|
131
|
+
await verifyFrozenNpmTarballContract({
|
|
132
|
+
package: npmDistribution?.package,
|
|
133
|
+
version: unit?.targetVersion,
|
|
134
|
+
tarballPath: npm?.tarballPath,
|
|
135
|
+
tarballSha256: npm?.tarballSha256,
|
|
136
|
+
integrity: npm?.integrity,
|
|
137
|
+
}, root);
|
|
138
|
+
actualSubjects.push({
|
|
139
|
+
packageName: npmDistribution.package,
|
|
140
|
+
version: unit.targetVersion,
|
|
141
|
+
filename: basename(npm.tarballPath),
|
|
142
|
+
sha256: npm.tarballSha256,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return consumePublicSourceAuthorityReceipt(
|
|
146
|
+
await readFile(verifiedAsset.physical),
|
|
147
|
+
actualSubjects,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
112
151
|
/**
|
|
113
152
|
* Map plan action type to adapter ActionType.
|
|
114
153
|
* Must match publish.mjs and reconcile.mjs.
|
|
@@ -1689,6 +1728,19 @@ export async function verifyRelease(options) {
|
|
|
1689
1728
|
|
|
1690
1729
|
await evidence.append({ phase: 'verify', step: 'adapter-verify', status: 'completed' });
|
|
1691
1730
|
|
|
1731
|
+
if (plan.publicSourceAuthorityReceipt) {
|
|
1732
|
+
const consumed = await verifyPublicSourceAuthorityReceiptOffline({ plan, root });
|
|
1733
|
+
await evidence.append({
|
|
1734
|
+
phase: 'public-source-authority-receipt',
|
|
1735
|
+
status: 'passed',
|
|
1736
|
+
verificationMode: 'offline-consumer',
|
|
1737
|
+
sourceRepository: consumed.sourceRepository,
|
|
1738
|
+
sourceBaseCommit: consumed.sourceBaseCommit,
|
|
1739
|
+
subjectCount: consumed.subjects.length,
|
|
1740
|
+
assetSha256: consumed.sha256,
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1692
1744
|
// =======================================================================
|
|
1693
1745
|
// Step 4: Installation smoke test
|
|
1694
1746
|
// =======================================================================
|