ruvnet-brain 4.0.7 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/install.mjs +6 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +17 -17
- package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
- package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
- package/scripts/candidate-host-evidence.mjs +49 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
- package/scripts/learning-replay-contract.mjs +14 -4
- package/scripts/learning-replay-execution.mjs +4 -3
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/publication-receipt.mjs +126 -34
- package/scripts/release-evidence-aggregate.mjs +55 -0
- package/scripts/release-payload.mjs +100 -0
- package/scripts/release-transaction-provider.mjs +233 -56
- package/scripts/release-transaction.mjs +295 -105
- package/scripts/release.mjs +41 -12
- package/scripts/selfcheck.mjs +20 -5
- package/scripts/staged-host-verifier.mjs +3 -6
|
@@ -3,13 +3,16 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
pollObservation, RECEIPT_PREFIX, TERMINAL_STATES, transactionIdFor,
|
|
8
|
+
} from './release-transaction.mjs';
|
|
7
9
|
|
|
8
10
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
9
11
|
const PACKAGE = 'ruvnet-brain';
|
|
12
|
+
export const ASSET_DOWNLOAD_TIMEOUT_MS = Number(process.env.RUVNET_RELEASE_ASSET_TIMEOUT_MS || 600_000);
|
|
10
13
|
|
|
11
14
|
const command = (name, args, options = {}) => execFileSync(name, args, {
|
|
12
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options,
|
|
15
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
|
|
13
16
|
}).trim();
|
|
14
17
|
const json = (name, args, options) => JSON.parse(command(name, args, options));
|
|
15
18
|
const maybe = (callback, fallback = null) => {
|
|
@@ -24,31 +27,80 @@ const tagSha = (tag, root) => {
|
|
|
24
27
|
|
|
25
28
|
const assetBytes = (asset) => {
|
|
26
29
|
const result = spawnSync('gh', ['api', asset.url, '-H', 'Accept: application/octet-stream'], {
|
|
27
|
-
encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: ASSET_DOWNLOAD_TIMEOUT_MS,
|
|
28
31
|
});
|
|
29
|
-
if (result.error || result.status !== 0)
|
|
32
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
33
|
+
throw new Error(`cannot download transaction asset ${asset.name}: ${result.error?.message || result.signal || `exit ${result.status}`}`);
|
|
34
|
+
}
|
|
30
35
|
return Buffer.from(result.stdout);
|
|
31
36
|
};
|
|
32
37
|
const assetReceipt = (asset) => JSON.parse(assetBytes(asset).toString('utf8'));
|
|
33
38
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
39
|
+
const OBSERVATION_POLICY = {
|
|
40
|
+
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 180_000),
|
|
41
|
+
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 14),
|
|
42
|
+
initialDelayMs: 1_000,
|
|
43
|
+
maxDelayMs: 15_000,
|
|
44
|
+
multiplier: 1.8,
|
|
45
|
+
jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
|
|
49
|
+
const bytes = remoteBytes || localBytes || generatedBytes;
|
|
50
|
+
let value;
|
|
51
|
+
try { value = JSON.parse(Buffer.from(bytes).toString('utf8')); } catch { throw new Error('public receipt is invalid JSON'); }
|
|
52
|
+
if (value.transactionId !== identity.transactionId || value.version !== identity.version
|
|
53
|
+
|| value.candidateSha !== identity.candidateSha || value.verdict !== 'PASS'
|
|
54
|
+
|| value.hosts?.verdict !== 'PASS') {
|
|
55
|
+
throw new Error('public receipt identity conflict');
|
|
56
|
+
}
|
|
57
|
+
return Buffer.from(bytes);
|
|
58
|
+
}
|
|
34
59
|
|
|
35
60
|
export function liveReleaseProvider({
|
|
36
61
|
root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
|
|
37
62
|
} = {}) {
|
|
38
63
|
let releases = [];
|
|
39
64
|
let activeDraft = null;
|
|
65
|
+
const assetDigestCache = new Map();
|
|
40
66
|
const refresh = () => {
|
|
41
|
-
|
|
67
|
+
const pages = json('gh', ['api', `repos/${REPO}/releases?per_page=100`, '--paginate', '--slurp']);
|
|
68
|
+
releases = (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
|
|
42
69
|
return releases;
|
|
43
70
|
};
|
|
44
71
|
const releaseById = (id) => json('gh', ['api', `repos/${REPO}/releases/${id}`]);
|
|
72
|
+
const assetsForRelease = (id) => {
|
|
73
|
+
const pages = json('gh', ['api', `repos/${REPO}/releases/${id}/assets?per_page=100`, '--paginate', '--slurp']);
|
|
74
|
+
return (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
|
|
75
|
+
};
|
|
76
|
+
const hydratedRelease = (release) => ({ ...release, assets: assetsForRelease(release.id) });
|
|
45
77
|
const receiptsFor = (release) => (release?.assets || [])
|
|
46
78
|
.filter(({ name }) => name.startsWith(RECEIPT_PREFIX) && name.endsWith('.json'))
|
|
47
79
|
.map(assetReceipt);
|
|
48
|
-
const
|
|
80
|
+
const expectedAssets = (identity) => ({
|
|
81
|
+
'ruvnet-brain.zip': identity.bundleSha256,
|
|
82
|
+
'ruvnet-brain.zip.sig': identity.bundleSignatureSha256,
|
|
83
|
+
'ruvnet-brain.zip.sha256': identity.bundleDigestSha256,
|
|
84
|
+
[identity.packageAssetName || '']: identity.packageSha256 || null,
|
|
85
|
+
});
|
|
86
|
+
const digestAsset = (asset, force = false) => {
|
|
87
|
+
const key = `${asset.id}:${asset.size}`;
|
|
88
|
+
if (!force && assetDigestCache.has(key)) return assetDigestCache.get(key);
|
|
89
|
+
const digest = sha256(assetBytes(asset));
|
|
90
|
+
assetDigestCache.set(key, digest);
|
|
91
|
+
return digest;
|
|
92
|
+
};
|
|
93
|
+
const assetsExactFor = (release, identity, force = false) => {
|
|
94
|
+
const expected = expectedAssets(identity);
|
|
95
|
+
return Object.entries(expected).filter(([name, digest]) => name && digest).every(([name, digest]) => {
|
|
96
|
+
const asset = release?.assets?.find((item) => item.name === name);
|
|
97
|
+
return asset && digestAsset(asset, force) === digest;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const materializeRemoteAssets = async (draft, identity) => {
|
|
49
101
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-staged-'));
|
|
50
102
|
try {
|
|
51
|
-
const release = releaseById(draft.id);
|
|
103
|
+
const release = hydratedRelease(releaseById(draft.id));
|
|
52
104
|
const required = [
|
|
53
105
|
['bundlePath', 'ruvnet-brain.zip'],
|
|
54
106
|
['bundleSignaturePath', 'ruvnet-brain.zip.sig'],
|
|
@@ -65,13 +117,25 @@ export function liveReleaseProvider({
|
|
|
65
117
|
if (sha256(fs.readFileSync(assets.bundlePath)) !== identity.bundleSha256) {
|
|
66
118
|
throw new Error('staged GitHub bundle digest does not match transaction identity');
|
|
67
119
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
120
|
+
if (identity.bundleSignatureSha256
|
|
121
|
+
&& sha256(fs.readFileSync(assets.bundleSignaturePath)) !== identity.bundleSignatureSha256) {
|
|
122
|
+
throw new Error('staged GitHub bundle signature bytes do not match transaction identity');
|
|
123
|
+
}
|
|
124
|
+
if (identity.bundleDigestSha256
|
|
125
|
+
&& sha256(fs.readFileSync(assets.bundleDigestPath)) !== identity.bundleDigestSha256) {
|
|
126
|
+
throw new Error('staged GitHub digest sidecar bytes do not match transaction identity');
|
|
73
127
|
}
|
|
74
|
-
|
|
128
|
+
const { value: metadata } = await pollObservation(
|
|
129
|
+
async () => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']),
|
|
130
|
+
(value) => value?.version === identity.version
|
|
131
|
+
&& value?.dist?.integrity === identity.packageIntegrity
|
|
132
|
+
&& typeof value?.dist?.tarball === 'string',
|
|
133
|
+
OBSERVATION_POLICY,
|
|
134
|
+
);
|
|
135
|
+
const response = await fetch(metadata.dist.tarball, { signal: AbortSignal.timeout(30_000) });
|
|
136
|
+
if (!response.ok) throw new Error(`npm candidate tarball download failed: HTTP ${response.status}`);
|
|
137
|
+
assets.packagePath = path.join(temp, identity.packageAssetName || `${PACKAGE}-${identity.version}.tgz`);
|
|
138
|
+
fs.writeFileSync(assets.packagePath, Buffer.from(await response.arrayBuffer()), { flag: 'wx', mode: 0o600 });
|
|
75
139
|
const integrity = `sha512-${crypto.createHash('sha512').update(fs.readFileSync(assets.packagePath)).digest('base64')}`;
|
|
76
140
|
if (integrity !== identity.packageIntegrity) throw new Error('staged npm package integrity mismatch');
|
|
77
141
|
return { assets, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
|
|
@@ -82,24 +146,42 @@ export function liveReleaseProvider({
|
|
|
82
146
|
};
|
|
83
147
|
|
|
84
148
|
return {
|
|
149
|
+
observationPolicy: OBSERVATION_POLICY,
|
|
150
|
+
|
|
85
151
|
async discover(identity) {
|
|
86
152
|
const all = refresh();
|
|
87
|
-
const matchingDrafts = all.filter((release) => release.tag_name === identity.tag
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
153
|
+
const matchingDrafts = all.filter((release) => release.tag_name === identity.tag)
|
|
154
|
+
.map((release) => ({
|
|
155
|
+
id: release.id,
|
|
156
|
+
tag: release.tag_name,
|
|
157
|
+
sha: release.draft ? release.target_commitish : tagSha(identity.tag, root),
|
|
158
|
+
draft: release.draft,
|
|
159
|
+
}))
|
|
160
|
+
.filter((release) => release.sha === identity.candidateSha);
|
|
91
161
|
activeDraft = matchingDrafts[0] || null;
|
|
92
|
-
const receipts = matchingDrafts.length === 1
|
|
162
|
+
const receipts = matchingDrafts.length === 1
|
|
163
|
+
? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
|
|
93
164
|
const latestByTransaction = new Map();
|
|
94
|
-
for (const
|
|
95
|
-
const
|
|
96
|
-
|
|
165
|
+
for (const release of all) {
|
|
166
|
+
for (const receipt of receiptsFor(hydratedRelease(release))) {
|
|
167
|
+
const prior = latestByTransaction.get(receipt.transactionId);
|
|
168
|
+
if (!prior || receipt.sequence > prior.receipt.sequence) {
|
|
169
|
+
latestByTransaction.set(receipt.transactionId, { receipt, release });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
97
172
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
173
|
+
const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
174
|
+
const githubLatest = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
175
|
+
const legacySettled = [];
|
|
176
|
+
const pending = [];
|
|
177
|
+
for (const { receipt, release } of latestByTransaction.values()) {
|
|
178
|
+
if (TERMINAL_STATES.has(receipt.state)) continue;
|
|
179
|
+
const settled = receipt.schemaVersion === 1 && release.draft === false
|
|
180
|
+
&& receipt.identity?.version === npmLatest && receipt.identity?.tag === githubLatest;
|
|
181
|
+
if (settled) legacySettled.push(receipt.transactionId);
|
|
182
|
+
else pending.push(receipt);
|
|
183
|
+
}
|
|
184
|
+
return { matchingDrafts, receipts, pending, legacySettled, prior: { npmLatest, githubLatest } };
|
|
103
185
|
},
|
|
104
186
|
|
|
105
187
|
async createDraft(identity) {
|
|
@@ -112,6 +194,63 @@ export function liveReleaseProvider({
|
|
|
112
194
|
return activeDraft;
|
|
113
195
|
},
|
|
114
196
|
|
|
197
|
+
async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
|
|
198
|
+
try {
|
|
199
|
+
const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
|
|
200
|
+
const latestTag = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
201
|
+
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
202
|
+
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
203
|
+
const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
204
|
+
const resolvedSha = release && !release.draft ? tagSha(identity.tag, root) : release?.target_commitish || draft?.sha || null;
|
|
205
|
+
const publicReceipt = release?.assets?.find((asset) => asset.name === 'current-release.json');
|
|
206
|
+
let publicReceiptExact = false;
|
|
207
|
+
let publicHostsExact = false;
|
|
208
|
+
if (publicReceipt) {
|
|
209
|
+
const value = assetReceipt(publicReceipt);
|
|
210
|
+
publicReceiptExact = value?.transactionId === transactionIdFor(identity)
|
|
211
|
+
&& value?.version === identity.version
|
|
212
|
+
&& value?.candidateSha === identity.candidateSha
|
|
213
|
+
&& /^[a-f0-9]{64}$/.test(value?.publicationReceiptSha256 || '');
|
|
214
|
+
publicHostsExact = value?.verdict === 'PASS' && value?.hosts?.verdict === 'PASS'
|
|
215
|
+
&& ['claudeOnly', 'codexOnly', 'dual'].every((mode) => value.hosts?.[mode]?.status === 'PASS'
|
|
216
|
+
&& value.hosts?.[mode]?.doctorExit === 0
|
|
217
|
+
&& value.hosts?.[mode]?.artifactSha256 === identity.packageSha256);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
npm: {
|
|
221
|
+
candidateVersion: candidate?.version || exactVersion?.version || null,
|
|
222
|
+
candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
|
|
223
|
+
candidateTagVersion: candidate?.version || null,
|
|
224
|
+
latestVersion,
|
|
225
|
+
},
|
|
226
|
+
github: release ? {
|
|
227
|
+
tag: release.tag_name,
|
|
228
|
+
sha: resolvedSha,
|
|
229
|
+
draft: release.draft === true,
|
|
230
|
+
published: release.draft === false,
|
|
231
|
+
latest: latestTag === identity.tag,
|
|
232
|
+
latestTag,
|
|
233
|
+
assetsExact: assetsExactFor(release, identity, forceAssets),
|
|
234
|
+
} : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
|
|
235
|
+
publicReceiptExact,
|
|
236
|
+
publicHostsExact,
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { readError: error.message };
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async observeNpm(identity) {
|
|
244
|
+
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
245
|
+
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
246
|
+
return {
|
|
247
|
+
candidateVersion: candidate?.version || exactVersion?.version || null,
|
|
248
|
+
candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
|
|
249
|
+
candidateTagVersion: candidate?.version || null,
|
|
250
|
+
latestVersion: command('npm', ['view', `${PACKAGE}@latest`, 'version']),
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
|
|
115
254
|
async appendReceipt(draft, receipt, name) {
|
|
116
255
|
const anchor = draft || activeDraft;
|
|
117
256
|
if (!anchor) throw new Error('no draft anchor for transaction receipt');
|
|
@@ -127,7 +266,7 @@ export function liveReleaseProvider({
|
|
|
127
266
|
|
|
128
267
|
async readReceipt(draft, sequence) {
|
|
129
268
|
const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
|
|
130
|
-
const release = releaseById((draft || activeDraft).id);
|
|
269
|
+
const release = hydratedRelease(releaseById((draft || activeDraft).id));
|
|
131
270
|
const asset = release.assets?.find((item) => item.name === name);
|
|
132
271
|
if (!asset) throw new Error(`remote receipt ${name} is missing after upload`);
|
|
133
272
|
return assetReceipt(asset);
|
|
@@ -136,7 +275,7 @@ export function liveReleaseProvider({
|
|
|
136
275
|
async uploadAssets(draft, assets) {
|
|
137
276
|
const paths = [assets.bundlePath, assets.bundleSignaturePath, assets.bundleDigestPath, assets.packagePath];
|
|
138
277
|
for (const file of paths) {
|
|
139
|
-
const release = releaseById(draft.id);
|
|
278
|
+
const release = hydratedRelease(releaseById(draft.id));
|
|
140
279
|
const existing = release.assets?.find(({ name }) => name === path.basename(file));
|
|
141
280
|
if (existing) {
|
|
142
281
|
if (sha256(assetBytes(existing)) !== sha256(fs.readFileSync(file))) {
|
|
@@ -154,7 +293,11 @@ export function liveReleaseProvider({
|
|
|
154
293
|
|
|
155
294
|
async stageNpm(identity, packagePath) {
|
|
156
295
|
const existing = maybe(() => command('npm', ['view', `${PACKAGE}@${identity.version}`, 'dist.integrity']), null);
|
|
157
|
-
if (existing)
|
|
296
|
+
if (existing) {
|
|
297
|
+
if (existing !== identity.packageIntegrity) throw new Error('existing npm version has different immutable bytes');
|
|
298
|
+
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
158
301
|
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
|
|
159
302
|
},
|
|
160
303
|
|
|
@@ -173,13 +316,21 @@ export function liveReleaseProvider({
|
|
|
173
316
|
return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
|
|
174
317
|
},
|
|
175
318
|
|
|
176
|
-
async promoteNpm(identity) {
|
|
319
|
+
async promoteNpm(identity, expectedPrior) {
|
|
320
|
+
const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
321
|
+
if (current !== expectedPrior && current !== identity.version) {
|
|
322
|
+
throw new Error(`refusing npm promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
323
|
+
}
|
|
177
324
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, 'latest']);
|
|
178
325
|
},
|
|
179
326
|
async observeNpmLatest() {
|
|
180
327
|
return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
|
|
181
328
|
},
|
|
182
|
-
async makeGithubLatest(draft) {
|
|
329
|
+
async makeGithubLatest(draft, identity, expectedPrior) {
|
|
330
|
+
const current = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
331
|
+
if (current !== expectedPrior && current !== identity.tag) {
|
|
332
|
+
throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
333
|
+
}
|
|
183
334
|
command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
|
|
184
335
|
},
|
|
185
336
|
async observeGithubLatest() {
|
|
@@ -190,15 +341,7 @@ export function liveReleaseProvider({
|
|
|
190
341
|
if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
|
|
191
342
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
|
|
192
343
|
},
|
|
193
|
-
async finalize(identity, receipt,
|
|
194
|
-
const staged = materializeRemoteAssets(activeDraft, identity);
|
|
195
|
-
let hosts;
|
|
196
|
-
try {
|
|
197
|
-
hosts = await hostVerifier.verify({ source: 'final', identity, assets: staged.assets });
|
|
198
|
-
} finally {
|
|
199
|
-
staged.cleanup();
|
|
200
|
-
}
|
|
201
|
-
if (hosts.verdict !== 'PASS') return { verdict: 'FAIL', hosts };
|
|
344
|
+
async finalize(identity, receipt, _hostVerifier) {
|
|
202
345
|
if (!candidateReceipt || !publicationReceipt) {
|
|
203
346
|
throw new Error('final convergence requires candidate and publication receipt paths');
|
|
204
347
|
}
|
|
@@ -207,30 +350,64 @@ export function liveReleaseProvider({
|
|
|
207
350
|
'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
|
|
208
351
|
], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
|
|
209
352
|
if (publication.error || publication.status !== 0) {
|
|
210
|
-
return { verdict: 'FAIL',
|
|
353
|
+
return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
|
|
211
354
|
}
|
|
212
355
|
}
|
|
213
356
|
const seal = spawnSync(process.execPath, [
|
|
214
357
|
'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
|
|
215
358
|
], { cwd: root, encoding: 'utf8', timeout: 300_000 });
|
|
216
359
|
if (seal.error || seal.status !== 0) {
|
|
217
|
-
return { verdict: 'FAIL',
|
|
218
|
-
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
try { observed = JSON.parse(probe.stdout || ''); } catch {}
|
|
230
|
-
return {
|
|
231
|
-
verdict: probe.status === 0 && observed?.verdict === 'PASS' ? 'PASS' : 'FAIL',
|
|
360
|
+
return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
|
|
361
|
+
}
|
|
362
|
+
const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
|
|
363
|
+
const hosts = {
|
|
364
|
+
verdict: 'PASS',
|
|
365
|
+
claudeOnly: publication.installed?.claudeOnly,
|
|
366
|
+
codexOnly: publication.installed?.codexOnly,
|
|
367
|
+
dual: publication.installed?.dual,
|
|
368
|
+
};
|
|
369
|
+
const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
|
|
370
|
+
const result = {
|
|
371
|
+
verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
|
|
232
372
|
hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
|
|
233
373
|
};
|
|
374
|
+
if (result.verdict === 'PASS') {
|
|
375
|
+
const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
|
|
376
|
+
const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
|
|
377
|
+
schemaVersion: 1,
|
|
378
|
+
transactionId: receipt.transactionId,
|
|
379
|
+
version: identity.version,
|
|
380
|
+
tag: identity.tag,
|
|
381
|
+
candidateSha: identity.candidateSha,
|
|
382
|
+
verdict: 'PASS',
|
|
383
|
+
publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
|
|
384
|
+
hosts,
|
|
385
|
+
surface: observed,
|
|
386
|
+
}, null, 2)}\n`);
|
|
387
|
+
let remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
388
|
+
?.find((asset) => asset.name === 'current-release.json');
|
|
389
|
+
let currentReleaseBytes;
|
|
390
|
+
try {
|
|
391
|
+
currentReleaseBytes = selectCurrentReleaseBytes({
|
|
392
|
+
remoteBytes: remote ? assetBytes(remote) : null,
|
|
393
|
+
localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
|
|
394
|
+
generatedBytes: generatedCurrentRelease,
|
|
395
|
+
identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
|
|
399
|
+
}
|
|
400
|
+
if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
|
|
401
|
+
if (!remote) {
|
|
402
|
+
command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
|
|
403
|
+
remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
404
|
+
?.find((asset) => asset.name === 'current-release.json');
|
|
405
|
+
}
|
|
406
|
+
if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
|
|
407
|
+
return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
234
411
|
},
|
|
235
412
|
};
|
|
236
413
|
}
|