@the-open-engine/zeroshot 6.10.2 → 6.12.0
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/cli/commands/providers.js +13 -13
- package/cli/index.js +77 -35
- package/cli/lib/first-run.js +12 -11
- package/cli/lib/update-checker.js +517 -132
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +3 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/client.cjs +146 -0
- package/lib/cluster/client.d.ts +44 -0
- package/lib/cluster/client.mjs +141 -0
- package/lib/cluster/connection.cjs +382 -0
- package/lib/cluster/connection.d.ts +41 -0
- package/lib/cluster/connection.mjs +378 -0
- package/lib/cluster/errors.cjs +44 -0
- package/lib/cluster/errors.d.ts +23 -0
- package/lib/cluster/errors.mjs +32 -0
- package/lib/cluster/frames.cjs +49 -0
- package/lib/cluster/frames.d.ts +12 -0
- package/lib/cluster/frames.mjs +45 -0
- package/lib/cluster/generated/protocol-schema.cjs +5 -0
- package/lib/cluster/generated/protocol-schema.d.ts +1 -0
- package/lib/cluster/generated/protocol-schema.mjs +2 -0
- package/lib/cluster/generated/protocol.cjs +22 -0
- package/lib/cluster/generated/protocol.d.ts +781 -0
- package/lib/cluster/generated/protocol.mjs +19 -0
- package/lib/cluster/index.cjs +40 -0
- package/lib/cluster/index.d.ts +10 -0
- package/lib/cluster/index.mjs +6 -0
- package/lib/cluster/queue.cjs +71 -0
- package/lib/cluster/queue.d.ts +15 -0
- package/lib/cluster/queue.mjs +67 -0
- package/lib/cluster/socket.cjs +15 -0
- package/lib/cluster/socket.d.ts +11 -0
- package/lib/cluster/socket.mjs +12 -0
- package/lib/cluster/subscriptions.cjs +270 -0
- package/lib/cluster/subscriptions.d.ts +69 -0
- package/lib/cluster/subscriptions.mjs +264 -0
- package/lib/cluster/validators.cjs +46 -0
- package/lib/cluster/validators.d.ts +3 -0
- package/lib/cluster/validators.mjs +42 -0
- package/lib/settings.js +195 -23
- package/lib/setup-apply.js +45 -32
- package/lib/setup-undo.js +25 -16
- package/package.json +25 -3
- package/scripts/build-cluster.js +43 -0
- package/scripts/generate-cluster-types.js +203 -0
- package/scripts/release-dry-run.js +6 -6
- package/scripts/rust-distribution.js +933 -0
- package/src/agent/agent-hook-executor.js +14 -6
- package/src/agent/agent-lifecycle.js +25 -8
- package/src/agent/agent-task-executor.js +711 -139
- package/src/agent/output-reformatter.js +54 -41
- package/src/agent/pr-verification.js +11 -3
- package/src/agent/task-execution-handle.js +373 -0
- package/src/agent-cli-provider/adapters/opencode.ts +3 -0
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +5 -2
- package/src/cluster/client.ts +199 -0
- package/src/cluster/connection.ts +300 -0
- package/src/cluster/errors.ts +32 -0
- package/src/cluster/frames.ts +44 -0
- package/src/cluster/generated/protocol-schema.ts +2 -0
- package/src/cluster/generated/protocol.ts +183 -0
- package/src/cluster/index.ts +38 -0
- package/src/cluster/queue.ts +84 -0
- package/src/cluster/socket.ts +31 -0
- package/src/cluster/subscriptions.ts +256 -0
- package/src/cluster/validators.ts +56 -0
- package/src/cluster/ws.d.ts +7 -0
- package/src/isolation-manager.js +16 -0
- package/src/issue-providers/jira-provider.js +1 -1
- package/src/issue-providers/linear-provider.js +4 -4
- package/task-lib/runner.js +3 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const jsYaml = require('js-yaml');
|
|
10
|
+
const zlib = require('zlib');
|
|
11
|
+
|
|
12
|
+
const repositoryRoot = path.resolve(__dirname, '..');
|
|
13
|
+
const targetManifestPath = path.join(repositoryRoot, 'distribution', 'zeroshot-rust-targets.json');
|
|
14
|
+
const targets = Object.freeze(JSON.parse(fs.readFileSync(targetManifestPath, 'utf8')));
|
|
15
|
+
const VERSION_ERROR = 'RUST_VERSION_MISMATCH';
|
|
16
|
+
|
|
17
|
+
function isVersionCharacter(character) {
|
|
18
|
+
return (
|
|
19
|
+
(character >= '0' && character <= '9') ||
|
|
20
|
+
(character >= 'A' && character <= 'Z') ||
|
|
21
|
+
(character >= 'a' && character <= 'z') ||
|
|
22
|
+
character === '-'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeVersion(tag) {
|
|
27
|
+
const version = typeof tag === 'string' && tag.startsWith('v') ? tag.slice(1) : tag;
|
|
28
|
+
const prereleaseStart = typeof version === 'string' ? version.indexOf('-') : -1;
|
|
29
|
+
const core = prereleaseStart === -1 ? version : version.slice(0, prereleaseStart);
|
|
30
|
+
const prerelease = prereleaseStart === -1 ? '' : version.slice(prereleaseStart + 1);
|
|
31
|
+
const coreParts = typeof core === 'string' ? core.split('.') : [];
|
|
32
|
+
const validCore = coreParts.length === 3 && coreParts.every((part) => part && /^\d+$/.test(part));
|
|
33
|
+
const validPrerelease =
|
|
34
|
+
!prerelease ||
|
|
35
|
+
prerelease
|
|
36
|
+
.split('.')
|
|
37
|
+
.every((part) => part && [...part].every((character) => isVersionCharacter(character)));
|
|
38
|
+
if (!validCore || !validPrerelease) {
|
|
39
|
+
throw new Error(`invalid release tag ${JSON.stringify(tag)}; expected vX.Y.Z`);
|
|
40
|
+
}
|
|
41
|
+
return version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function archiveName(version, target) {
|
|
45
|
+
return `zeroshot-rust-v${normalizeVersion(version)}-${target}.tar.gz`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function targetForHost(platform, arch) {
|
|
49
|
+
const found = targets.find(
|
|
50
|
+
(candidate) => candidate.platform === platform && candidate.arch === arch
|
|
51
|
+
);
|
|
52
|
+
if (!found) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`UNSUPPORTED_ZEROSHOT_RUST_HOST: no prebuilt binary for ${platform}/${arch}; supported hosts: ${targets
|
|
55
|
+
.map((candidate) => `${candidate.platform}/${candidate.arch}`)
|
|
56
|
+
.join(', ')}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeOctal(buffer, offset, length, value) {
|
|
63
|
+
const encoded = value.toString(8).padStart(length - 1, '0') + '\0';
|
|
64
|
+
buffer.write(encoded, offset, length, 'ascii');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tarEntry(name, contents, mode = 0o755) {
|
|
68
|
+
if (Buffer.byteLength(name) > 100) throw new Error(`archive entry name is too long: ${name}`);
|
|
69
|
+
const header = Buffer.alloc(512);
|
|
70
|
+
header.write(name, 0, 100, 'utf8');
|
|
71
|
+
writeOctal(header, 100, 8, mode);
|
|
72
|
+
writeOctal(header, 108, 8, 0);
|
|
73
|
+
writeOctal(header, 116, 8, 0);
|
|
74
|
+
writeOctal(header, 124, 12, contents.length);
|
|
75
|
+
writeOctal(header, 136, 12, 0);
|
|
76
|
+
header.fill(0x20, 148, 156);
|
|
77
|
+
header[156] = '0'.charCodeAt(0);
|
|
78
|
+
header.write('ustar\0', 257, 6, 'ascii');
|
|
79
|
+
header.write('00', 263, 2, 'ascii');
|
|
80
|
+
writeOctal(
|
|
81
|
+
header,
|
|
82
|
+
148,
|
|
83
|
+
8,
|
|
84
|
+
[...header].reduce((sum, byte) => sum + byte, 0)
|
|
85
|
+
);
|
|
86
|
+
const padding = Buffer.alloc((512 - (contents.length % 512)) % 512);
|
|
87
|
+
return Buffer.concat([header, contents, padding]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createArchive(binary, executable) {
|
|
91
|
+
const tar = Buffer.concat([tarEntry(executable, binary), Buffer.alloc(1024)]);
|
|
92
|
+
return zlib.gzipSync(tar, { level: 9, mtime: 0 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractExecutable(archive, expectedName) {
|
|
96
|
+
const tar = zlib.gunzipSync(archive);
|
|
97
|
+
const name = tar.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
|
|
98
|
+
const sizeText = tar.subarray(124, 136).toString('ascii').replace(/\0.*$/, '').trim();
|
|
99
|
+
if (name !== expectedName || !/^[0-7]+$/.test(sizeText)) {
|
|
100
|
+
throw new Error(`ARCHIVE_INVALID: expected sole executable ${expectedName}`);
|
|
101
|
+
}
|
|
102
|
+
const size = Number.parseInt(sizeText, 8);
|
|
103
|
+
const end = 512 + size;
|
|
104
|
+
if (end > tar.length) throw new Error('ARCHIVE_INVALID: truncated executable');
|
|
105
|
+
const nextHeader = 512 + Math.ceil(size / 512) * 512;
|
|
106
|
+
if (!tar.subarray(nextHeader).every((byte) => byte === 0)) {
|
|
107
|
+
throw new Error('ARCHIVE_INVALID: archive contains unexpected entries');
|
|
108
|
+
}
|
|
109
|
+
return Buffer.from(tar.subarray(512, end));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sha256(contents) {
|
|
113
|
+
return crypto.createHash('sha256').update(contents).digest('hex');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseChecksumManifest(text) {
|
|
117
|
+
const checksums = new Map();
|
|
118
|
+
for (const line of text.split(/\r?\n/)) {
|
|
119
|
+
if (!line) continue;
|
|
120
|
+
const match = /^([0-9a-f]{64}) {2}([^/\\]+)$/.exec(line);
|
|
121
|
+
if (!match) throw new Error(`invalid SHA256SUMS line: ${line}`);
|
|
122
|
+
if (checksums.has(match[2])) throw new Error(`duplicate SHA256SUMS entry: ${match[2]}`);
|
|
123
|
+
checksums.set(match[2], match[1]);
|
|
124
|
+
}
|
|
125
|
+
return checksums;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function verifyChecksum(filename, contents, manifest) {
|
|
129
|
+
const checksums = manifest instanceof Map ? manifest : parseChecksumManifest(manifest);
|
|
130
|
+
const expected = checksums.get(filename);
|
|
131
|
+
if (!expected) throw new Error(`CHECKSUM_MISSING: SHA256SUMS has no entry for ${filename}`);
|
|
132
|
+
const actual = sha256(contents);
|
|
133
|
+
if (actual !== expected) {
|
|
134
|
+
throw new Error(`CHECKSUM_MISMATCH: ${filename} expected ${expected} but received ${actual}`);
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function packageTarget({ target, version, binaryPath, outputDirectory }) {
|
|
140
|
+
const declaration = targets.find((candidate) => candidate.target === target);
|
|
141
|
+
if (!declaration) throw new Error(`undeclared Rust release target: ${target}`);
|
|
142
|
+
const binary = fs.readFileSync(binaryPath);
|
|
143
|
+
const filename = archiveName(version, target);
|
|
144
|
+
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
path.join(outputDirectory, filename),
|
|
147
|
+
createArchive(binary, declaration.executable)
|
|
148
|
+
);
|
|
149
|
+
return filename;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createManifest({ version, directory }) {
|
|
153
|
+
const entries = targets.map(({ target }) => {
|
|
154
|
+
const filename = archiveName(version, target);
|
|
155
|
+
const contents = fs.readFileSync(path.join(directory, filename));
|
|
156
|
+
return `${sha256(contents)} ${filename}`;
|
|
157
|
+
});
|
|
158
|
+
const manifest = `${entries.join('\n')}\n`;
|
|
159
|
+
fs.writeFileSync(path.join(directory, 'SHA256SUMS'), manifest);
|
|
160
|
+
const parsed = parseChecksumManifest(manifest);
|
|
161
|
+
for (const { target } of targets) {
|
|
162
|
+
const filename = archiveName(version, target);
|
|
163
|
+
verifyChecksum(filename, fs.readFileSync(path.join(directory, filename)), parsed);
|
|
164
|
+
}
|
|
165
|
+
return manifest;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function runGh(args) {
|
|
169
|
+
return childProcess.execFileSync('gh', args, { encoding: 'utf8' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function publishAssets({ tag, directory, invokeGh = runGh }) {
|
|
173
|
+
const names = [...targets.map(({ target }) => archiveName(tag, target)), 'SHA256SUMS'];
|
|
174
|
+
const localAssets = new Map(
|
|
175
|
+
names.map((name) => [name, fs.readFileSync(path.join(directory, name))])
|
|
176
|
+
);
|
|
177
|
+
const release = JSON.parse(invokeGh(['release', 'view', tag, '--json', 'assets']));
|
|
178
|
+
const existingNames = release.assets.map(({ name }) => name);
|
|
179
|
+
if (new Set(existingNames).size !== existingNames.length) {
|
|
180
|
+
throw new Error('RELEASE_ASSET_CONFLICT: GitHub Release contains duplicate asset names');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const existingRequired = names.filter((name) => existingNames.includes(name));
|
|
184
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-rust-assets-'));
|
|
185
|
+
try {
|
|
186
|
+
for (const name of existingRequired) {
|
|
187
|
+
invokeGh(['release', 'download', tag, '--pattern', name, '--dir', temporary]);
|
|
188
|
+
const published = fs.readFileSync(path.join(temporary, name));
|
|
189
|
+
if (!published.equals(localAssets.get(name))) {
|
|
190
|
+
throw new Error(`RELEASE_ASSET_CONFLICT: existing ${name} differs from verified artifact`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const missing = names.filter((name) => !existingNames.includes(name));
|
|
198
|
+
for (const name of missing) {
|
|
199
|
+
invokeGh(['release', 'upload', tag, path.join(directory, name)]);
|
|
200
|
+
}
|
|
201
|
+
return { existing: existingRequired, uploaded: missing };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function cargoVersion(cargoToml) {
|
|
205
|
+
const packageSection = cargoToml.match(/\[package\]([\s\S]*?)(?:\n\[|$)/);
|
|
206
|
+
const version = packageSection && packageSection[1].match(/^version\s*=\s*"([^"]+)"\s*$/m);
|
|
207
|
+
if (!version) throw new Error('zeroshot-rust/Cargo.toml has no package version');
|
|
208
|
+
return version[1];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const STAGED_LOCK_DEPENDENCIES = Object.freeze(['windows-sys']);
|
|
212
|
+
|
|
213
|
+
function escapeRegExp(value) {
|
|
214
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function cargoLockPackages(cargoLock, packageName) {
|
|
218
|
+
const tables = [];
|
|
219
|
+
const marker = /^\[{1,2}[^\r\n]+\]{1,2}\r?$/gm;
|
|
220
|
+
for (let match = marker.exec(cargoLock); match; match = marker.exec(cargoLock)) {
|
|
221
|
+
tables.push({ header: match[0].trim(), start: match.index });
|
|
222
|
+
}
|
|
223
|
+
return tables.flatMap((table, index) => {
|
|
224
|
+
if (table.header !== '[[package]]') return [];
|
|
225
|
+
const text = cargoLock.slice(table.start, tables[index + 1]?.start ?? cargoLock.length);
|
|
226
|
+
const name = text.match(/^name = "([^"]+)"\r?$/m)?.[1];
|
|
227
|
+
const version = text.match(/^version = "([^"]+)"\r?$/m)?.[1];
|
|
228
|
+
if (name !== packageName || !version) return [];
|
|
229
|
+
return [
|
|
230
|
+
{
|
|
231
|
+
start: table.start,
|
|
232
|
+
text,
|
|
233
|
+
version,
|
|
234
|
+
source: text.match(/^source = "([^"]+)"\r?$/m)?.[1],
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function workspaceLockPackage(cargoLock) {
|
|
241
|
+
const candidates = cargoLockPackages(cargoLock, 'zeroshot-rust').filter(
|
|
242
|
+
(candidate) => candidate.source === undefined
|
|
243
|
+
);
|
|
244
|
+
if (candidates.length !== 1) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
'RUST_VERSION_STAGE_FAILED: Cargo.lock needs exactly one source-less zeroshot-rust package'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return candidates[0];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function workspaceDependencyRequirement(workspaceCargoToml, dependencyName) {
|
|
253
|
+
const workspaceDependencies = workspaceCargoToml.match(
|
|
254
|
+
/\[workspace\.dependencies\]([\s\S]*?)(?:\r?\n\[|$)/
|
|
255
|
+
);
|
|
256
|
+
const requirement =
|
|
257
|
+
workspaceDependencies &&
|
|
258
|
+
workspaceDependencies[1].match(
|
|
259
|
+
new RegExp(`^${escapeRegExp(dependencyName)}\\s*=\\s*"([^"]+)"\\s*$`, 'm')
|
|
260
|
+
);
|
|
261
|
+
if (!requirement || !/^(\^|=)?\d+\.\d+\.\d+$/.test(requirement[1])) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`RUST_VERSION_STAGE_FAILED: workspace dependency ${dependencyName} has an unsupported version requirement`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return requirement[1];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseCargoVersion(version) {
|
|
270
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
271
|
+
return match ? match.slice(1).map(Number) : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function compareCargoVersions(left, right) {
|
|
275
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
276
|
+
if (left[index] !== right[index]) return left[index] - right[index];
|
|
277
|
+
}
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function cargoRequirementMatches(requirement, version) {
|
|
282
|
+
const parsedVersion = parseCargoVersion(version);
|
|
283
|
+
const match = /^(\^|=)?(\d+)\.(\d+)\.(\d+)$/.exec(requirement);
|
|
284
|
+
if (!parsedVersion || !match) return false;
|
|
285
|
+
const lower = match.slice(2).map(Number);
|
|
286
|
+
if (match[1] === '=') return compareCargoVersions(parsedVersion, lower) === 0;
|
|
287
|
+
const upper =
|
|
288
|
+
lower[0] > 0
|
|
289
|
+
? [lower[0] + 1, 0, 0]
|
|
290
|
+
: lower[1] > 0
|
|
291
|
+
? [0, lower[1] + 1, 0]
|
|
292
|
+
: [0, 0, lower[2] + 1];
|
|
293
|
+
return (
|
|
294
|
+
compareCargoVersions(parsedVersion, lower) >= 0 &&
|
|
295
|
+
compareCargoVersions(parsedVersion, upper) < 0
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function stagedLockDependencies(cargoLock, workspaceCargoToml) {
|
|
300
|
+
return STAGED_LOCK_DEPENDENCIES.map((name) => {
|
|
301
|
+
const requirement = workspaceDependencyRequirement(workspaceCargoToml, name);
|
|
302
|
+
const candidates = cargoLockPackages(cargoLock, name);
|
|
303
|
+
const satisfying = candidates.filter((candidate) =>
|
|
304
|
+
cargoRequirementMatches(requirement, candidate.version)
|
|
305
|
+
);
|
|
306
|
+
if (satisfying.length !== 1) {
|
|
307
|
+
const sourceAmbiguous =
|
|
308
|
+
satisfying.length > 1 &&
|
|
309
|
+
new Set(satisfying.map((candidate) => candidate.version)).size === 1;
|
|
310
|
+
throw new Error(
|
|
311
|
+
sourceAmbiguous
|
|
312
|
+
? `RUST_VERSION_STAGE_FAILED: Cargo.lock ${name} ${satisfying[0].version} has ambiguous sources`
|
|
313
|
+
: `RUST_VERSION_STAGE_FAILED: Cargo.lock needs exactly one ${name} package satisfying ${requirement}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const selected = satisfying[0];
|
|
317
|
+
return {
|
|
318
|
+
name,
|
|
319
|
+
requirement,
|
|
320
|
+
version: selected.version,
|
|
321
|
+
reference: candidates.length > 1 ? `${name} ${selected.version}` : name,
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function stageCargoLock(cargoLock, version, workspaceCargoToml) {
|
|
327
|
+
const targetPackage = workspaceLockPackage(cargoLock);
|
|
328
|
+
let stagedPackage = targetPackage.text.replace(
|
|
329
|
+
/^(version = ")[^"]+(")$/m,
|
|
330
|
+
`$1${version}$2`
|
|
331
|
+
);
|
|
332
|
+
for (const dependency of stagedLockDependencies(cargoLock, workspaceCargoToml)) {
|
|
333
|
+
const dependencyPattern = new RegExp(
|
|
334
|
+
`^(\\s*")${escapeRegExp(dependency.name)}(?: [^"]+)?(",\\r?)$`,
|
|
335
|
+
'm'
|
|
336
|
+
);
|
|
337
|
+
if (!dependencyPattern.test(stagedPackage)) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`RUST_VERSION_STAGE_FAILED: Cargo.lock zeroshot-rust entry has no ${dependency.name} dependency`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
stagedPackage = stagedPackage.replace(
|
|
343
|
+
dependencyPattern,
|
|
344
|
+
`$1${dependency.reference}$2`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return (
|
|
348
|
+
cargoLock.slice(0, targetPackage.start) +
|
|
349
|
+
stagedPackage +
|
|
350
|
+
cargoLock.slice(targetPackage.start + targetPackage.text.length)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function verifyStagedCargoLock(cargoLock, version, workspaceCargoToml) {
|
|
355
|
+
const targetPackage = workspaceLockPackage(cargoLock);
|
|
356
|
+
if (targetPackage.version !== version) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`${VERSION_ERROR}: release tag version ${version} does not match Cargo.lock zeroshot-rust version ${targetPackage.version}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
for (const dependency of stagedLockDependencies(cargoLock, workspaceCargoToml)) {
|
|
362
|
+
const dependencyPattern = new RegExp(
|
|
363
|
+
`^\\s*"${escapeRegExp(dependency.reference)}",\\r?$`,
|
|
364
|
+
'm'
|
|
365
|
+
);
|
|
366
|
+
if (!dependencyPattern.test(targetPackage.text)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`${VERSION_ERROR}: Cargo.lock zeroshot-rust dependency ${dependency.name} is not coupled to ${dependency.version}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function stageVersion(
|
|
375
|
+
tag,
|
|
376
|
+
cargoManifestPath = path.join(repositoryRoot, 'zeroshot-rust', 'Cargo.toml'),
|
|
377
|
+
cargoLockPath = path.join(repositoryRoot, 'Cargo.lock'),
|
|
378
|
+
workspaceManifestPath = path.join(repositoryRoot, 'Cargo.toml')
|
|
379
|
+
) {
|
|
380
|
+
const version = normalizeVersion(tag);
|
|
381
|
+
const cargoToml = fs.readFileSync(cargoManifestPath, 'utf8');
|
|
382
|
+
const currentVersion = cargoVersion(cargoToml);
|
|
383
|
+
const stagedManifest = cargoToml.replace(
|
|
384
|
+
/(\[package\][\s\S]*?^version\s*=\s*")[^"]+(")/m,
|
|
385
|
+
`$1${version}$2`
|
|
386
|
+
);
|
|
387
|
+
if (cargoVersion(stagedManifest) !== version) {
|
|
388
|
+
throw new Error('RUST_VERSION_STAGE_FAILED: Cargo.toml package version was not updated');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const cargoLock = fs.readFileSync(cargoLockPath, 'utf8');
|
|
392
|
+
const workspaceCargoToml = fs.readFileSync(workspaceManifestPath, 'utf8');
|
|
393
|
+
const stagedLock = stageCargoLock(cargoLock, version, workspaceCargoToml);
|
|
394
|
+
verifyStagedCargoLock(stagedLock, version, workspaceCargoToml);
|
|
395
|
+
fs.writeFileSync(cargoManifestPath, stagedManifest);
|
|
396
|
+
fs.writeFileSync(cargoLockPath, stagedLock);
|
|
397
|
+
return { currentVersion, version };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function checkVersionCoupling(tag, cargoToml, cargoLock, workspaceCargoToml) {
|
|
401
|
+
const useRepositoryFiles = cargoToml === undefined;
|
|
402
|
+
const manifest =
|
|
403
|
+
cargoToml ??
|
|
404
|
+
fs.readFileSync(path.join(repositoryRoot, 'zeroshot-rust', 'Cargo.toml'), 'utf8');
|
|
405
|
+
const releaseVersion = normalizeVersion(tag);
|
|
406
|
+
const manifestVersion = cargoVersion(manifest);
|
|
407
|
+
if (releaseVersion !== manifestVersion) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`${VERSION_ERROR}: release tag version ${releaseVersion} does not match zeroshot-rust/Cargo.toml version ${manifestVersion}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const lock =
|
|
413
|
+
cargoLock ??
|
|
414
|
+
(useRepositoryFiles
|
|
415
|
+
? fs.readFileSync(path.join(repositoryRoot, 'Cargo.lock'), 'utf8')
|
|
416
|
+
: undefined);
|
|
417
|
+
if (lock !== undefined) {
|
|
418
|
+
const workspace =
|
|
419
|
+
workspaceCargoToml ??
|
|
420
|
+
fs.readFileSync(path.join(repositoryRoot, 'Cargo.toml'), 'utf8');
|
|
421
|
+
verifyStagedCargoLock(lock, releaseVersion, workspace);
|
|
422
|
+
}
|
|
423
|
+
return releaseVersion;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function failIntegrity(message) {
|
|
427
|
+
throw new Error(`RUST_DISTRIBUTION_INTEGRITY: ${message}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function exactJson(label, expected, actual) {
|
|
431
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
432
|
+
failIntegrity(
|
|
433
|
+
`${label} differs from the authoritative declaration: expected ${JSON.stringify(expected)}; got ${JSON.stringify(actual)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function findStep(job, name) {
|
|
439
|
+
const step = job.steps?.find((candidate) => candidate.name === name);
|
|
440
|
+
if (!step) failIntegrity(`job is missing enabled step ${name}`);
|
|
441
|
+
if (step.if === false || step.if === '${{ false }}') {
|
|
442
|
+
failIntegrity(`step ${name} is disabled`);
|
|
443
|
+
}
|
|
444
|
+
return step;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const RUST_DISTRIBUTION_INVOCATION =
|
|
448
|
+
/(?:(?:\bnode\s+)|(?:^|[\s(]))["']?(?:\.\/)?scripts\/rust-distribution\.js["']?(?=$|[\s)`;&|])/;
|
|
449
|
+
|
|
450
|
+
const SCRIPT_INSTALL_CONTRACTS = Object.freeze([
|
|
451
|
+
{
|
|
452
|
+
jobName: 'dry-run',
|
|
453
|
+
installName: 'Install pinned dependencies',
|
|
454
|
+
command: 'npm ci',
|
|
455
|
+
checkoutRef: '${{ github.sha }}',
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
jobName: 'release',
|
|
459
|
+
installName: 'Install pinned dependencies',
|
|
460
|
+
command: 'npm ci',
|
|
461
|
+
checkoutRef: '${{ github.event.workflow_run.head_sha }}',
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
jobName: 'rust-binaries',
|
|
465
|
+
installName: 'Install pinned script dependencies',
|
|
466
|
+
command: 'npm ci --ignore-scripts',
|
|
467
|
+
checkoutRef:
|
|
468
|
+
"${{ inputs.action == 'dry-run' && github.sha || inputs.action == 'recover-rust-distribution' && inputs.release_commit || github.event.workflow_run.head_sha }}",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
jobName: 'rust-manifest',
|
|
472
|
+
installName: 'Install pinned script dependencies',
|
|
473
|
+
command: 'npm ci --ignore-scripts',
|
|
474
|
+
checkoutRef:
|
|
475
|
+
"${{ inputs.action == 'dry-run' && github.sha || inputs.action == 'recover-rust-distribution' && inputs.release_commit || github.event.workflow_run.head_sha }}",
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
jobName: 'rust-publish',
|
|
479
|
+
installName: 'Install pinned script dependencies',
|
|
480
|
+
command: 'npm ci --ignore-scripts',
|
|
481
|
+
checkoutRef: '${{ env.RELEASE_TAG }}',
|
|
482
|
+
},
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
function invokesRustDistribution(step) {
|
|
486
|
+
return (
|
|
487
|
+
typeof step.run === 'string' && RUST_DISTRIBUTION_INVOCATION.test(step.run)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function checkScriptInstall(job, { jobName, installName, command, checkoutRef }) {
|
|
492
|
+
if (!job) failIntegrity(`release workflow has no ${jobName} job`);
|
|
493
|
+
const install = findStep(job, installName);
|
|
494
|
+
if (
|
|
495
|
+
install.if !== undefined ||
|
|
496
|
+
install['working-directory'] !== undefined ||
|
|
497
|
+
install.run?.trim() !== command
|
|
498
|
+
) {
|
|
499
|
+
failIntegrity(`${jobName} dependency install must execute at workspace root: ${command}`);
|
|
500
|
+
}
|
|
501
|
+
const checkout = job.steps.find((step) => step.uses?.startsWith('actions/checkout@'));
|
|
502
|
+
if (
|
|
503
|
+
!checkout ||
|
|
504
|
+
checkout.if !== undefined ||
|
|
505
|
+
checkout.with?.path !== undefined ||
|
|
506
|
+
(checkout.with?.repository !== undefined &&
|
|
507
|
+
checkout.with.repository !== '${{ github.repository }}') ||
|
|
508
|
+
checkout.with?.ref !== checkoutRef
|
|
509
|
+
) {
|
|
510
|
+
failIntegrity(
|
|
511
|
+
`${jobName} must checkout expected current repository source at workspace root`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const installIndex = job.steps.indexOf(install);
|
|
515
|
+
const checkoutIndex = job.steps.indexOf(checkout);
|
|
516
|
+
if (checkoutIndex >= installIndex) {
|
|
517
|
+
failIntegrity(`${jobName} must checkout source before dependency installation`);
|
|
518
|
+
}
|
|
519
|
+
const nodeSetup = job.steps.find((step) => step.uses?.startsWith('actions/setup-node@'));
|
|
520
|
+
const nodeSetupIndex = job.steps.indexOf(nodeSetup);
|
|
521
|
+
if (
|
|
522
|
+
!nodeSetup ||
|
|
523
|
+
nodeSetup.if !== undefined ||
|
|
524
|
+
nodeSetup.uses !== 'actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903' ||
|
|
525
|
+
String(nodeSetup.with?.['node-version']) !== '24' ||
|
|
526
|
+
nodeSetup.with?.cache !== 'npm' ||
|
|
527
|
+
nodeSetupIndex <= checkoutIndex ||
|
|
528
|
+
nodeSetupIndex >= installIndex
|
|
529
|
+
) {
|
|
530
|
+
failIntegrity(`${jobName} must enable pinned Node 24 npm cache before dependency installation`);
|
|
531
|
+
}
|
|
532
|
+
if (job.steps.slice(0, installIndex).some(invokesRustDistribution)) {
|
|
533
|
+
failIntegrity(
|
|
534
|
+
`${jobName} must install dependencies before every rust-distribution.js invocation`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function checkScriptInstalls(jobs) {
|
|
540
|
+
for (const contract of SCRIPT_INSTALL_CONTRACTS) {
|
|
541
|
+
checkScriptInstall(jobs[contract.jobName], contract);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function checkBuildJob(jobs) {
|
|
546
|
+
const job = jobs['rust-binaries'];
|
|
547
|
+
if (!job) failIntegrity('release workflow has no rust-binaries job');
|
|
548
|
+
exactJson(
|
|
549
|
+
'rust-binaries dependencies',
|
|
550
|
+
['dry-run', 'release-plan', 'rust-recovery-plan'],
|
|
551
|
+
[...(job.needs || [])].sort()
|
|
552
|
+
);
|
|
553
|
+
const expectedMatrix = targets.map(({ target, runner, executable, cCompiler }) => ({
|
|
554
|
+
target,
|
|
555
|
+
runner,
|
|
556
|
+
executable,
|
|
557
|
+
'c-compiler': cCompiler,
|
|
558
|
+
}));
|
|
559
|
+
exactJson('rust-binaries matrix rows', expectedMatrix, job.strategy?.matrix?.include);
|
|
560
|
+
|
|
561
|
+
const setup = findStep(job, 'Setup Rust 1.97.0 target');
|
|
562
|
+
if (
|
|
563
|
+
setup.uses !== 'dtolnay/rust-toolchain@stable' ||
|
|
564
|
+
setup.with?.toolchain !== '1.97.0' ||
|
|
565
|
+
setup.with?.targets !== '${{ matrix.target }}'
|
|
566
|
+
) {
|
|
567
|
+
failIntegrity('Rust target toolchain setup does not install the declared matrix target');
|
|
568
|
+
}
|
|
569
|
+
const unixC = findStep(job, 'Verify bundled SQLite C toolchain');
|
|
570
|
+
if (
|
|
571
|
+
unixC.if !== "runner.os != 'Windows'" ||
|
|
572
|
+
unixC.env?.C_COMPILER !== '${{ matrix.c-compiler }}' ||
|
|
573
|
+
unixC.run?.trim() !== 'command -v "$C_COMPILER"'
|
|
574
|
+
) {
|
|
575
|
+
failIntegrity('Unix bundled-SQLite C compiler setup does not use the matrix mapping');
|
|
576
|
+
}
|
|
577
|
+
const windowsC = findStep(job, 'Verify bundled SQLite MSVC toolchain');
|
|
578
|
+
if (
|
|
579
|
+
windowsC.if !== "runner.os == 'Windows'" ||
|
|
580
|
+
!windowsC.run?.includes('Microsoft.VisualStudio.Component.VC.Tools.x86.x64')
|
|
581
|
+
) {
|
|
582
|
+
failIntegrity('Windows bundled-SQLite MSVC setup is missing');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const stage = findStep(job, 'Stage planned Rust package version');
|
|
586
|
+
if (
|
|
587
|
+
stage.if !== undefined ||
|
|
588
|
+
stage.run?.trim() !== 'node scripts/rust-distribution.js stage-version --tag "$RELEASE_TAG"'
|
|
589
|
+
) {
|
|
590
|
+
failIntegrity('planned Rust version staging is missing before locked target builds');
|
|
591
|
+
}
|
|
592
|
+
const coupling = findStep(job, 'Verify Rust and release tag versions are coupled');
|
|
593
|
+
if (
|
|
594
|
+
coupling.if !== undefined ||
|
|
595
|
+
coupling.run?.trim() !== 'node scripts/rust-distribution.js check-version --tag "$RELEASE_TAG"'
|
|
596
|
+
) {
|
|
597
|
+
failIntegrity('staged Rust version coupling verification is missing');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const build = findStep(job, 'Build standalone Rust release binary');
|
|
601
|
+
const exactBuild =
|
|
602
|
+
'cargo build --release --locked -p zeroshot-rust --bin zeroshot-rust --target ${{ matrix.target }}';
|
|
603
|
+
if (
|
|
604
|
+
build.if !== undefined ||
|
|
605
|
+
build.run?.trim() !== exactBuild ||
|
|
606
|
+
job.steps.indexOf(stage) > job.steps.indexOf(build) ||
|
|
607
|
+
job.steps.indexOf(coupling) > job.steps.indexOf(build)
|
|
608
|
+
) {
|
|
609
|
+
failIntegrity(`rust-binaries build step must execute exactly: ${exactBuild}`);
|
|
610
|
+
}
|
|
611
|
+
const nativeSmoke = findStep(job, 'Run standalone Rust release binary');
|
|
612
|
+
if (
|
|
613
|
+
nativeSmoke.if !== undefined ||
|
|
614
|
+
nativeSmoke.run?.trim() !== 'node scripts/rust-distribution.js smoke --binary "$BINARY_PATH"'
|
|
615
|
+
) {
|
|
616
|
+
failIntegrity('native Rust executable smoke step must execute the built binary exactly');
|
|
617
|
+
}
|
|
618
|
+
findStep(job, 'Package target archive');
|
|
619
|
+
const archiveSmoke = findStep(job, 'Run executable extracted from target archive');
|
|
620
|
+
const exactArchiveSmoke = `node scripts/rust-distribution.js smoke-archive \\
|
|
621
|
+
--target "\${{ matrix.target }}" \\
|
|
622
|
+
--archive "rust-release/zeroshot-rust-\${RELEASE_TAG}-\${{ matrix.target }}.tar.gz"`;
|
|
623
|
+
if (archiveSmoke.if !== undefined || archiveSmoke.run?.trim() !== exactArchiveSmoke) {
|
|
624
|
+
failIntegrity('archive smoke step must execute the extracted target binary exactly');
|
|
625
|
+
}
|
|
626
|
+
const upload = findStep(job, 'Upload target archive');
|
|
627
|
+
if (
|
|
628
|
+
upload.if !== undefined ||
|
|
629
|
+
upload.uses !== 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' ||
|
|
630
|
+
upload.with?.name !== 'zeroshot-rust-${{ matrix.target }}' ||
|
|
631
|
+
upload.with?.path !== 'rust-release/*.tar.gz' ||
|
|
632
|
+
upload.with?.['if-no-files-found'] !== 'error'
|
|
633
|
+
) {
|
|
634
|
+
failIntegrity('rust-binaries per-target archive upload name/path is incomplete');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function checkManifestJob(jobs) {
|
|
639
|
+
const job = jobs['rust-manifest'];
|
|
640
|
+
if (!job) failIntegrity('release workflow has no rust-manifest job');
|
|
641
|
+
exactJson(
|
|
642
|
+
'rust-manifest dependencies',
|
|
643
|
+
['dry-run', 'release-plan', 'rust-binaries', 'rust-recovery-plan'],
|
|
644
|
+
[...(job.needs || [])].sort()
|
|
645
|
+
);
|
|
646
|
+
const manifest = findStep(job, 'Build and verify complete checksum manifest');
|
|
647
|
+
if (
|
|
648
|
+
manifest.run?.trim() !==
|
|
649
|
+
'node scripts/rust-distribution.js manifest --version "$RELEASE_TAG" --dir rust-release'
|
|
650
|
+
) {
|
|
651
|
+
failIntegrity('rust-manifest does not execute the deterministic manifest verifier');
|
|
652
|
+
}
|
|
653
|
+
const upload = findStep(job, 'Upload complete dry-run distribution');
|
|
654
|
+
if (
|
|
655
|
+
upload.if !== undefined ||
|
|
656
|
+
upload.uses !== 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' ||
|
|
657
|
+
upload.with?.name !== 'zeroshot-rust-${{ env.RELEASE_TAG }}' ||
|
|
658
|
+
upload.with?.path?.trim() !== 'rust-release/*.tar.gz\nrust-release/SHA256SUMS'
|
|
659
|
+
) {
|
|
660
|
+
failIntegrity('complete archive and SHA256SUMS upload is missing');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function checkPublicationJobs(document, jobs) {
|
|
665
|
+
const release = jobs.release;
|
|
666
|
+
exactJson(
|
|
667
|
+
'release dependencies',
|
|
668
|
+
['install-matrix', 'release-plan', 'rust-manifest'],
|
|
669
|
+
[...(release?.needs || [])].sort()
|
|
670
|
+
);
|
|
671
|
+
if (!release.if?.includes("vars.RELEASE_AUTOMATION_ENABLED == 'true'")) {
|
|
672
|
+
failIntegrity('release publication is not guarded by RELEASE_AUTOMATION_ENABLED');
|
|
673
|
+
}
|
|
674
|
+
const semantic = findStep(release, 'Run semantic-release');
|
|
675
|
+
if (!semantic.run?.split(/\r?\n/).some((line) => line.trim() === 'npx semantic-release')) {
|
|
676
|
+
failIntegrity('release job does not execute semantic-release');
|
|
677
|
+
}
|
|
678
|
+
for (const [jobName, job] of Object.entries(jobs)) {
|
|
679
|
+
if (jobName === 'release') continue;
|
|
680
|
+
if (
|
|
681
|
+
job.steps?.some((step) =>
|
|
682
|
+
step.run?.split(/\r?\n/).some((line) => line.trim() === 'npx semantic-release')
|
|
683
|
+
)
|
|
684
|
+
) {
|
|
685
|
+
failIntegrity(`semantic-release runs before artifacts in ${jobName}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (
|
|
690
|
+
!document.on?.workflow_dispatch?.inputs?.action?.options?.includes('recover-rust-distribution')
|
|
691
|
+
) {
|
|
692
|
+
failIntegrity('workflow_dispatch has no recover-rust-distribution action');
|
|
693
|
+
}
|
|
694
|
+
const recovery = jobs['rust-recovery-plan'];
|
|
695
|
+
const immutable = findStep(recovery, 'Verify immutable matching release tag');
|
|
696
|
+
const recoveryLines = immutable.run?.split(/\r?\n/).map((line) => line.trim()) || [];
|
|
697
|
+
if (
|
|
698
|
+
!recoveryLines.includes('tag_commit="$(git rev-parse "${RELEASE_TAG}^{commit}")"') ||
|
|
699
|
+
!recoveryLines.includes('if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/main; then')
|
|
700
|
+
) {
|
|
701
|
+
failIntegrity('Rust recovery does not execute immutable tag and main ancestry verification');
|
|
702
|
+
}
|
|
703
|
+
const publish = jobs['rust-publish'];
|
|
704
|
+
exactJson(
|
|
705
|
+
'rust-publish dependencies',
|
|
706
|
+
['release', 'rust-manifest', 'rust-recovery-plan'],
|
|
707
|
+
[...(publish?.needs || [])].sort()
|
|
708
|
+
);
|
|
709
|
+
if (!publish.if?.includes("inputs.action == 'recover-rust-distribution'")) {
|
|
710
|
+
failIntegrity('post-tag Rust publication is not recoverable');
|
|
711
|
+
}
|
|
712
|
+
const assets = findStep(publish, 'Verify existing assets and upload only missing names');
|
|
713
|
+
if (
|
|
714
|
+
assets.if !== undefined ||
|
|
715
|
+
assets.run?.trim() !==
|
|
716
|
+
'node scripts/rust-distribution.js publish-assets --tag "$RELEASE_TAG" --dir rust-release'
|
|
717
|
+
) {
|
|
718
|
+
failIntegrity('GitHub Release assets are not verified and uploaded without overwrite');
|
|
719
|
+
}
|
|
720
|
+
const npmPublish = findStep(publish, 'Idempotently publish standalone Rust shim package').run;
|
|
721
|
+
if (!npmPublish?.includes('npm view "$package" version') || !npmPublish.includes('npm publish')) {
|
|
722
|
+
failIntegrity('shim publication is not idempotently recoverable');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function checkShimTargets(shimTargets) {
|
|
727
|
+
const projected = targets
|
|
728
|
+
.map(({ platform, arch, target, executable }) => ({ platform, arch, target, executable }))
|
|
729
|
+
.sort((left, right) =>
|
|
730
|
+
`${left.platform}/${left.arch}`.localeCompare(`${right.platform}/${right.arch}`)
|
|
731
|
+
);
|
|
732
|
+
const actual = [...shimTargets].sort((left, right) =>
|
|
733
|
+
`${left.platform}/${left.arch}`.localeCompare(`${right.platform}/${right.arch}`)
|
|
734
|
+
);
|
|
735
|
+
exactJson('npm shim host mapping', projected, actual);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function hasValidSri(integrity) {
|
|
739
|
+
if (typeof integrity !== 'string') return false;
|
|
740
|
+
const match = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(integrity);
|
|
741
|
+
if (!match) return false;
|
|
742
|
+
const expectedBytes = { sha256: 32, sha384: 48, sha512: 64 }[match[1]];
|
|
743
|
+
const digest = Buffer.from(match[2], 'base64');
|
|
744
|
+
return digest.length === expectedBytes && digest.toString('base64') === match[2];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function checkScriptDependencies(packageManifest, packageLock) {
|
|
748
|
+
const directSpec = packageManifest.devDependencies?.['js-yaml'];
|
|
749
|
+
if (typeof directSpec !== 'string' || directSpec.length === 0) {
|
|
750
|
+
failIntegrity('rust-distribution.js requires a direct js-yaml devDependency');
|
|
751
|
+
}
|
|
752
|
+
const lockSpec = packageLock.packages?.['']?.devDependencies?.['js-yaml'];
|
|
753
|
+
if (lockSpec !== directSpec) {
|
|
754
|
+
failIntegrity('package-lock root js-yaml spec must match package.json');
|
|
755
|
+
}
|
|
756
|
+
const resolved = packageLock.packages?.['node_modules/js-yaml'];
|
|
757
|
+
if (
|
|
758
|
+
typeof resolved?.version !== 'string' ||
|
|
759
|
+
resolved.version.trim().length === 0 ||
|
|
760
|
+
typeof resolved.resolved !== 'string' ||
|
|
761
|
+
resolved.resolved.trim().length === 0 ||
|
|
762
|
+
!hasValidSri(resolved.integrity)
|
|
763
|
+
) {
|
|
764
|
+
failIntegrity('package-lock must contain an integrity-pinned resolved js-yaml package');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function checkRepository(
|
|
769
|
+
workflow = fs.readFileSync(
|
|
770
|
+
path.join(repositoryRoot, '.github', 'workflows', 'release.yml'),
|
|
771
|
+
'utf8'
|
|
772
|
+
),
|
|
773
|
+
shimTargets = JSON.parse(
|
|
774
|
+
fs.readFileSync(path.join(repositoryRoot, 'npm', 'zeroshot-rust', 'targets.json'), 'utf8')
|
|
775
|
+
),
|
|
776
|
+
packageManifest = JSON.parse(
|
|
777
|
+
fs.readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8')
|
|
778
|
+
),
|
|
779
|
+
packageLock = JSON.parse(
|
|
780
|
+
fs.readFileSync(path.join(repositoryRoot, 'package-lock.json'), 'utf8')
|
|
781
|
+
)
|
|
782
|
+
) {
|
|
783
|
+
let document;
|
|
784
|
+
try {
|
|
785
|
+
document = jsYaml.load(workflow);
|
|
786
|
+
} catch (error) {
|
|
787
|
+
failIntegrity(`release workflow is invalid YAML: ${error.message}`);
|
|
788
|
+
}
|
|
789
|
+
if (!document?.jobs) failIntegrity('release workflow has no jobs');
|
|
790
|
+
checkScriptInstalls(document.jobs);
|
|
791
|
+
checkBuildJob(document.jobs);
|
|
792
|
+
checkManifestJob(document.jobs);
|
|
793
|
+
checkPublicationJobs(document, document.jobs);
|
|
794
|
+
checkShimTargets(shimTargets);
|
|
795
|
+
checkScriptDependencies(packageManifest, packageLock);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function argument(name) {
|
|
800
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
801
|
+
if (index === -1 || !process.argv[index + 1]) throw new Error(`missing --${name}`);
|
|
802
|
+
return process.argv[index + 1];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function run() {
|
|
806
|
+
const command = process.argv[2];
|
|
807
|
+
if (command === 'package') {
|
|
808
|
+
const filename = packageTarget({
|
|
809
|
+
target: argument('target'),
|
|
810
|
+
version: argument('version'),
|
|
811
|
+
binaryPath: argument('binary'),
|
|
812
|
+
outputDirectory: argument('out'),
|
|
813
|
+
});
|
|
814
|
+
process.stdout.write(`${filename}\n`);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (command === 'manifest') {
|
|
818
|
+
createManifest({ version: argument('version'), directory: argument('dir') });
|
|
819
|
+
process.stdout.write(`verified ${targets.length} archives and SHA256SUMS\n`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (command === 'dry-run') {
|
|
823
|
+
const version = argument('version');
|
|
824
|
+
const binaryPath = argument('binary');
|
|
825
|
+
const outputDirectory = argument('out');
|
|
826
|
+
for (const { target } of targets)
|
|
827
|
+
packageTarget({ target, version, binaryPath, outputDirectory });
|
|
828
|
+
createManifest({ version, directory: outputDirectory });
|
|
829
|
+
process.stdout.write(`dry-run produced and verified ${targets.length} archives\n`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (command === 'stage-version') {
|
|
833
|
+
const staged = stageVersion(argument('tag'));
|
|
834
|
+
process.stdout.write(
|
|
835
|
+
`staged Rust package version ${staged.currentVersion} -> ${staged.version}\n`
|
|
836
|
+
);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (command === 'check-version') {
|
|
840
|
+
const version = checkVersionCoupling(argument('tag'));
|
|
841
|
+
process.stdout.write(`Rust package version matches release tag: ${version}\n`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (command === 'print-version') {
|
|
845
|
+
process.stdout.write(
|
|
846
|
+
`${cargoVersion(fs.readFileSync(path.join(repositoryRoot, 'zeroshot-rust', 'Cargo.toml'), 'utf8'))}\n`
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (command === 'smoke') {
|
|
851
|
+
const binaryPath = path.resolve(argument('binary'));
|
|
852
|
+
const result = childProcess.spawnSync(binaryPath, [], { stdio: 'inherit' });
|
|
853
|
+
if (result.error) throw result.error;
|
|
854
|
+
if (result.signal || result.status !== 0) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`RUST_BINARY_SMOKE_FAILED: status=${result.status} signal=${result.signal || 'none'}`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
process.stdout.write(`Rust release executable exited 0: ${binaryPath}\n`);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (command === 'smoke-archive') {
|
|
863
|
+
const target = argument('target');
|
|
864
|
+
const declaration = targets.find((candidate) => candidate.target === target);
|
|
865
|
+
if (!declaration) throw new Error(`undeclared Rust release target: ${target}`);
|
|
866
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-rust-smoke-'));
|
|
867
|
+
const binaryPath = path.join(directory, declaration.executable);
|
|
868
|
+
try {
|
|
869
|
+
const executable = extractExecutable(
|
|
870
|
+
fs.readFileSync(argument('archive')),
|
|
871
|
+
declaration.executable
|
|
872
|
+
);
|
|
873
|
+
fs.writeFileSync(binaryPath, executable, { mode: 0o755 });
|
|
874
|
+
const result = childProcess.spawnSync(binaryPath, [], { stdio: 'inherit' });
|
|
875
|
+
if (result.error) throw result.error;
|
|
876
|
+
if (result.signal || result.status !== 0) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`RUST_ARCHIVE_SMOKE_FAILED: status=${result.status} signal=${result.signal || 'none'}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
} finally {
|
|
882
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
883
|
+
}
|
|
884
|
+
process.stdout.write(`Rust release archive executable exited 0: ${target}\n`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (command === 'publish-assets') {
|
|
888
|
+
const result = publishAssets({ tag: argument('tag'), directory: argument('dir') });
|
|
889
|
+
process.stdout.write(
|
|
890
|
+
`verified ${result.existing.length} existing assets and uploaded ${result.uploaded.length} missing assets\n`
|
|
891
|
+
);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (command === 'check-repository') {
|
|
895
|
+
checkRepository();
|
|
896
|
+
process.stdout.write(
|
|
897
|
+
`Rust distribution workflow declares ${targets.length} complete targets\n`
|
|
898
|
+
);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
throw new Error(
|
|
902
|
+
'usage: rust-distribution.js <package|manifest|dry-run|stage-version|check-version|check-repository|print-version|smoke|smoke-archive|publish-assets>'
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (require.main === module) {
|
|
907
|
+
try {
|
|
908
|
+
run();
|
|
909
|
+
} catch (error) {
|
|
910
|
+
process.stderr.write(`${error.message}\n`);
|
|
911
|
+
process.exitCode = 1;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
module.exports = {
|
|
916
|
+
VERSION_ERROR,
|
|
917
|
+
archiveName,
|
|
918
|
+
checkRepository,
|
|
919
|
+
checkVersionCoupling,
|
|
920
|
+
createArchive,
|
|
921
|
+
createManifest,
|
|
922
|
+
extractExecutable,
|
|
923
|
+
cargoVersion,
|
|
924
|
+
publishAssets,
|
|
925
|
+
normalizeVersion,
|
|
926
|
+
packageTarget,
|
|
927
|
+
parseChecksumManifest,
|
|
928
|
+
sha256,
|
|
929
|
+
targetForHost,
|
|
930
|
+
stageVersion,
|
|
931
|
+
targets,
|
|
932
|
+
verifyChecksum,
|
|
933
|
+
};
|