@empire-builder-kit/nx 1.0.0-rc.7 → 1.0.0-rc.9
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/package.json +3 -3
- package/src/generators/preset/workspace-files-ci-acceptance.d.ts +1 -0
- package/src/generators/preset/workspace-files-ci-acceptance.js +648 -0
- package/src/generators/preset/workspace-files-ci-acceptance.js.map +1 -0
- package/src/generators/preset/workspace-files.js +111 -4
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/supply-chain/blueprint-trust-git.js +72 -21
- package/src/supply-chain/blueprint-trust-git.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empire-builder-kit/nx",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.9",
|
|
4
4
|
"description": "Nx generators, executors, and Blueprint contracts for Empire Builder Kit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aws",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"provenance": false
|
|
127
127
|
},
|
|
128
128
|
"dependencies": {
|
|
129
|
-
"@empire-builder-kit/runtime": "1.0.0-rc.
|
|
129
|
+
"@empire-builder-kit/runtime": "1.0.0-rc.9",
|
|
130
130
|
"@aws-sdk/client-cloudwatch-logs": "^3.700.0",
|
|
131
131
|
"@aws-sdk/client-iam": "^3.700.0",
|
|
132
132
|
"@nx/devkit": "23.1.0",
|
|
@@ -154,5 +154,5 @@
|
|
|
154
154
|
"engines": {
|
|
155
155
|
"node": ">=22.13.0"
|
|
156
156
|
},
|
|
157
|
-
"gitHead": "
|
|
157
|
+
"gitHead": "217caf65da4115ac12ca232593a6f1479183a0e1"
|
|
158
158
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderWorkspaceCiAcceptanceFiles(): Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderWorkspaceCiAcceptanceFiles = renderWorkspaceCiAcceptanceFiles;
|
|
4
|
+
function renderWorkspaceCiAcceptanceFiles() {
|
|
5
|
+
return {
|
|
6
|
+
'tools/ci/workspace-acceptance.mjs': `import {
|
|
7
|
+
appendFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { inflateRawSync } from 'node:zlib';
|
|
20
|
+
|
|
21
|
+
const WORKFLOW = '.github/workflows/ci.yml';
|
|
22
|
+
const KIND = 'ebk-workspace-protected-promotion-acceptance';
|
|
23
|
+
const MAX_ARCHIVE_BYTES = 1024 * 1024;
|
|
24
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
25
|
+
const INPUTS = Object.freeze([
|
|
26
|
+
WORKFLOW,
|
|
27
|
+
'.node-version',
|
|
28
|
+
'ebk.repository-policy.json',
|
|
29
|
+
'package.json',
|
|
30
|
+
'pnpm-lock.yaml',
|
|
31
|
+
'supply-chain/blueprint-trust-policy.json',
|
|
32
|
+
'supply-chain/blueprint-trust-snapshot.json',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function sha256(value) {
|
|
36
|
+
return createHash('sha256').update(value).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function artifactDigest(value) {
|
|
40
|
+
if (typeof value !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(value)) {
|
|
41
|
+
throw new Error('Workspace acceptance artifact digest is invalid.');
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function artifactSize(value) {
|
|
47
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_ARCHIVE_BYTES) {
|
|
48
|
+
throw new Error('Workspace acceptance artifact size is invalid.');
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function artifactId(value) {
|
|
54
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
55
|
+
throw new Error('Workspace acceptance artifact identity is invalid.');
|
|
56
|
+
}
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function file(root, relative, limit = 16 * 1024 * 1024) {
|
|
61
|
+
const absolute = path.join(root, relative);
|
|
62
|
+
const entry = lstatSync(absolute);
|
|
63
|
+
if (
|
|
64
|
+
entry.isSymbolicLink() ||
|
|
65
|
+
!entry.isFile() ||
|
|
66
|
+
entry.size < 1 ||
|
|
67
|
+
entry.size > limit
|
|
68
|
+
) {
|
|
69
|
+
throw new Error(\`Workspace acceptance input is invalid: \${relative}\`);
|
|
70
|
+
}
|
|
71
|
+
return readFileSync(absolute);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function git(root, args) {
|
|
75
|
+
const result = spawnSync('git', ['-C', root, ...args], {
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
shell: false,
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
});
|
|
80
|
+
if (result.error || result.status !== 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
\`Workspace acceptance git failed: \${result.stderr || result.error?.message}\`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return result.stdout.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rootIdentity(requested) {
|
|
89
|
+
const root = realpathSync(requested);
|
|
90
|
+
if (root !== path.resolve(requested) || !lstatSync(root).isDirectory()) {
|
|
91
|
+
throw new Error('Workspace acceptance root is not canonical.');
|
|
92
|
+
}
|
|
93
|
+
const commit = git(root, ['rev-parse', 'HEAD']);
|
|
94
|
+
const tree = git(root, ['rev-parse', 'HEAD^{tree}']);
|
|
95
|
+
if (
|
|
96
|
+
!/^[a-f0-9]{40}$/u.test(commit) ||
|
|
97
|
+
!/^[a-f0-9]{40}$/u.test(tree) ||
|
|
98
|
+
git(root, ['status', '--porcelain=v1', '--untracked-files=no'])
|
|
99
|
+
) {
|
|
100
|
+
throw new Error('Workspace acceptance source is invalid or dirty.');
|
|
101
|
+
}
|
|
102
|
+
return { root, source: { commit, tree } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function digests(root) {
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
INPUTS.map((relative) => [relative, sha256(file(root, relative))]),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function integer(value, label) {
|
|
112
|
+
if (typeof value !== 'string' || !/^[1-9]\\d{0,19}$/u.test(value)) {
|
|
113
|
+
throw new Error(\`Workspace acceptance \${label} is invalid.\`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function repository(value) {
|
|
119
|
+
if (
|
|
120
|
+
typeof value !== 'string' ||
|
|
121
|
+
value.length > 200 ||
|
|
122
|
+
!/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/u.test(value)
|
|
123
|
+
) {
|
|
124
|
+
throw new Error('Workspace acceptance repository is invalid.');
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// GitHub resolves pull_request workflows through an ephemeral merge ref. The
|
|
130
|
+
// workflow bytes and exact source tree are bound independently in the sealed
|
|
131
|
+
// manifest, while this check binds the observable GitHub route.
|
|
132
|
+
function isPromotionWorkflowRef(value, expectedRepository) {
|
|
133
|
+
const prefix = \`\${expectedRepository}/\${WORKFLOW}@refs/pull/\`;
|
|
134
|
+
return (
|
|
135
|
+
typeof value === 'string' &&
|
|
136
|
+
value.startsWith(prefix) &&
|
|
137
|
+
/^[1-9]\\d{0,9}\\/merge$/u.test(value.slice(prefix.length))
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function canonical(value) {
|
|
142
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
143
|
+
if (value !== null && typeof value === 'object') {
|
|
144
|
+
return Object.fromEntries(
|
|
145
|
+
Object.keys(value)
|
|
146
|
+
.sort()
|
|
147
|
+
.map((key) => [key, canonical(value[key])]),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function sealWorkspaceAcceptance({
|
|
154
|
+
environment,
|
|
155
|
+
output,
|
|
156
|
+
root: requested,
|
|
157
|
+
}) {
|
|
158
|
+
const { root, source } = rootIdentity(requested);
|
|
159
|
+
const expectedRepository = repository(environment.GITHUB_REPOSITORY);
|
|
160
|
+
const runId = integer(environment.GITHUB_RUN_ID, 'run id');
|
|
161
|
+
const runAttempt = integer(environment.GITHUB_RUN_ATTEMPT, 'run attempt');
|
|
162
|
+
if (
|
|
163
|
+
environment.GITHUB_EVENT_NAME !== 'pull_request' ||
|
|
164
|
+
environment.EBK_HEAD_REPOSITORY !== expectedRepository ||
|
|
165
|
+
environment.GITHUB_BASE_REF !== 'main' ||
|
|
166
|
+
environment.GITHUB_HEAD_REF !== 'develop' ||
|
|
167
|
+
!isPromotionWorkflowRef(
|
|
168
|
+
environment.GITHUB_WORKFLOW_REF,
|
|
169
|
+
expectedRepository,
|
|
170
|
+
) ||
|
|
171
|
+
environment.EBK_CI_CANDIDATE_SHA !== source.commit
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
'Workspace acceptance is not the protected develop-to-main route.',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (existsSync(output)) {
|
|
178
|
+
throw new Error('Workspace acceptance output already exists.');
|
|
179
|
+
}
|
|
180
|
+
const manifest = {
|
|
181
|
+
base: 'main',
|
|
182
|
+
eventName: 'pull_request',
|
|
183
|
+
head: 'develop',
|
|
184
|
+
headRepository: expectedRepository,
|
|
185
|
+
inputs: digests(root),
|
|
186
|
+
kind: KIND,
|
|
187
|
+
repository: expectedRepository,
|
|
188
|
+
route: 'protected-develop-promotion',
|
|
189
|
+
runAttempt,
|
|
190
|
+
runId,
|
|
191
|
+
schemaVersion: 1,
|
|
192
|
+
source,
|
|
193
|
+
workflow: WORKFLOW,
|
|
194
|
+
};
|
|
195
|
+
mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 });
|
|
196
|
+
writeFileSync(output, \`\${JSON.stringify(manifest, null, 2)}\\n\`, {
|
|
197
|
+
encoding: 'utf8',
|
|
198
|
+
mode: 0o600,
|
|
199
|
+
});
|
|
200
|
+
return manifest;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function verifyWorkspaceAcceptance({
|
|
204
|
+
headCommit,
|
|
205
|
+
manifestPath,
|
|
206
|
+
repository: requestedRepository,
|
|
207
|
+
root: requested,
|
|
208
|
+
runAttempt,
|
|
209
|
+
runId,
|
|
210
|
+
}) {
|
|
211
|
+
const { root, source } = rootIdentity(requested);
|
|
212
|
+
const expectedRepository = repository(requestedRepository);
|
|
213
|
+
runId = integer(runId, 'run id');
|
|
214
|
+
runAttempt = integer(runAttempt, 'run attempt');
|
|
215
|
+
if (!/^[a-f0-9]{40}$/u.test(headCommit)) {
|
|
216
|
+
throw new Error('Workspace acceptance head commit is invalid.');
|
|
217
|
+
}
|
|
218
|
+
const headTree = git(root, ['rev-parse', \`\${headCommit}^{tree}\`]);
|
|
219
|
+
if (headTree !== source.tree) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Workspace acceptance head tree differs from the merged tree.',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const manifest = JSON.parse(
|
|
225
|
+
file(
|
|
226
|
+
path.dirname(manifestPath),
|
|
227
|
+
path.basename(manifestPath),
|
|
228
|
+
256 * 1024,
|
|
229
|
+
).toString('utf8'),
|
|
230
|
+
);
|
|
231
|
+
const expected = {
|
|
232
|
+
base: 'main',
|
|
233
|
+
eventName: 'pull_request',
|
|
234
|
+
head: 'develop',
|
|
235
|
+
headRepository: expectedRepository,
|
|
236
|
+
inputs: digests(root),
|
|
237
|
+
kind: KIND,
|
|
238
|
+
repository: expectedRepository,
|
|
239
|
+
route: 'protected-develop-promotion',
|
|
240
|
+
runAttempt,
|
|
241
|
+
runId,
|
|
242
|
+
schemaVersion: 1,
|
|
243
|
+
source: { commit: headCommit, tree: headTree },
|
|
244
|
+
workflow: WORKFLOW,
|
|
245
|
+
};
|
|
246
|
+
if (
|
|
247
|
+
JSON.stringify(canonical(manifest)) !== JSON.stringify(canonical(expected))
|
|
248
|
+
) {
|
|
249
|
+
throw new Error('Workspace acceptance differs from the merged tree.');
|
|
250
|
+
}
|
|
251
|
+
return manifest;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function json(url, token, fetchImpl) {
|
|
255
|
+
const response = await fetchImpl(url, {
|
|
256
|
+
headers: {
|
|
257
|
+
Accept: 'application/vnd.github+json',
|
|
258
|
+
Authorization: \`Bearer \${token}\`,
|
|
259
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
const text = await response.text();
|
|
263
|
+
if (!response.ok || text.length > 2 * 1024 * 1024) {
|
|
264
|
+
throw new Error(\`GitHub acceptance lookup failed (\${response.status}).\`);
|
|
265
|
+
}
|
|
266
|
+
return JSON.parse(text);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function boundedResponseBytes(response, limit) {
|
|
270
|
+
if (!response.ok || !response.body) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
\`Workspace acceptance artifact download failed (\${response.status}).\`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const reader = response.body.getReader();
|
|
276
|
+
const chunks = [];
|
|
277
|
+
let total = 0;
|
|
278
|
+
while (true) {
|
|
279
|
+
const { done, value } = await reader.read();
|
|
280
|
+
if (done) break;
|
|
281
|
+
if (!(value instanceof Uint8Array)) {
|
|
282
|
+
throw new Error('Workspace acceptance artifact stream is invalid.');
|
|
283
|
+
}
|
|
284
|
+
total += value.byteLength;
|
|
285
|
+
if (total > limit) {
|
|
286
|
+
await reader.cancel();
|
|
287
|
+
throw new Error('Workspace acceptance artifact exceeds its byte bound.');
|
|
288
|
+
}
|
|
289
|
+
chunks.push(Buffer.from(value));
|
|
290
|
+
}
|
|
291
|
+
if (total < 1) {
|
|
292
|
+
throw new Error('Workspace acceptance artifact is empty.');
|
|
293
|
+
}
|
|
294
|
+
return Buffer.concat(chunks, total);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function crc32(value) {
|
|
298
|
+
let crc = 0xffffffff;
|
|
299
|
+
for (const byte of value) {
|
|
300
|
+
crc ^= byte;
|
|
301
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
302
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The acceptance upload contains exactly one manifest. Parse that bounded ZIP
|
|
309
|
+
// directly so no unverified archive bytes reach a filesystem extractor.
|
|
310
|
+
function acceptanceManifestFromArchive(archive) {
|
|
311
|
+
const minimumEocd = 22;
|
|
312
|
+
const minimumOffset = Math.max(0, archive.length - 65_557);
|
|
313
|
+
let eocd = -1;
|
|
314
|
+
for (
|
|
315
|
+
let offset = archive.length - minimumEocd;
|
|
316
|
+
offset >= minimumOffset;
|
|
317
|
+
offset -= 1
|
|
318
|
+
) {
|
|
319
|
+
if (
|
|
320
|
+
archive.readUInt32LE(offset) === 0x06054b50 &&
|
|
321
|
+
offset + minimumEocd + archive.readUInt16LE(offset + 20) ===
|
|
322
|
+
archive.length
|
|
323
|
+
) {
|
|
324
|
+
eocd = offset;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (
|
|
329
|
+
eocd < 0 ||
|
|
330
|
+
archive.readUInt16LE(eocd + 4) !== 0 ||
|
|
331
|
+
archive.readUInt16LE(eocd + 6) !== 0 ||
|
|
332
|
+
archive.readUInt16LE(eocd + 8) !== 1 ||
|
|
333
|
+
archive.readUInt16LE(eocd + 10) !== 1
|
|
334
|
+
) {
|
|
335
|
+
throw new Error('Workspace acceptance archive shape is invalid.');
|
|
336
|
+
}
|
|
337
|
+
const centralSize = archive.readUInt32LE(eocd + 12);
|
|
338
|
+
const centralOffset = archive.readUInt32LE(eocd + 16);
|
|
339
|
+
if (
|
|
340
|
+
centralOffset + centralSize !== eocd ||
|
|
341
|
+
centralSize < 46 ||
|
|
342
|
+
archive.readUInt32LE(centralOffset) !== 0x02014b50
|
|
343
|
+
) {
|
|
344
|
+
throw new Error('Workspace acceptance central directory is invalid.');
|
|
345
|
+
}
|
|
346
|
+
const flags = archive.readUInt16LE(centralOffset + 8);
|
|
347
|
+
const method = archive.readUInt16LE(centralOffset + 10);
|
|
348
|
+
const expectedCrc = archive.readUInt32LE(centralOffset + 16);
|
|
349
|
+
const compressedSize = archive.readUInt32LE(centralOffset + 20);
|
|
350
|
+
const manifestSize = archive.readUInt32LE(centralOffset + 24);
|
|
351
|
+
const nameSize = archive.readUInt16LE(centralOffset + 28);
|
|
352
|
+
const extraSize = archive.readUInt16LE(centralOffset + 30);
|
|
353
|
+
const commentSize = archive.readUInt16LE(centralOffset + 32);
|
|
354
|
+
const disk = archive.readUInt16LE(centralOffset + 34);
|
|
355
|
+
const madeBy = archive.readUInt16LE(centralOffset + 4);
|
|
356
|
+
const externalAttributes = archive.readUInt32LE(centralOffset + 38);
|
|
357
|
+
const localOffset = archive.readUInt32LE(centralOffset + 42);
|
|
358
|
+
const centralEnd = centralOffset + 46 + nameSize + extraSize + commentSize;
|
|
359
|
+
const name = archive
|
|
360
|
+
.subarray(centralOffset + 46, centralOffset + 46 + nameSize)
|
|
361
|
+
.toString('utf8');
|
|
362
|
+
const unixMode = madeBy >>> 8 === 3 ? externalAttributes >>> 16 : 0;
|
|
363
|
+
if (
|
|
364
|
+
centralEnd !== eocd ||
|
|
365
|
+
name !== 'manifest.json' ||
|
|
366
|
+
disk !== 0 ||
|
|
367
|
+
(flags & ~0x0808) !== 0 ||
|
|
368
|
+
(method !== 0 && method !== 8) ||
|
|
369
|
+
compressedSize < 1 ||
|
|
370
|
+
manifestSize < 1 ||
|
|
371
|
+
manifestSize > MAX_MANIFEST_BYTES ||
|
|
372
|
+
(unixMode !== 0 && (unixMode & 0o170000) !== 0o100000) ||
|
|
373
|
+
localOffset + 30 > centralOffset ||
|
|
374
|
+
archive.readUInt32LE(localOffset) !== 0x04034b50 ||
|
|
375
|
+
archive.readUInt16LE(localOffset + 6) !== flags ||
|
|
376
|
+
archive.readUInt16LE(localOffset + 8) !== method
|
|
377
|
+
) {
|
|
378
|
+
throw new Error('Workspace acceptance archive entry is invalid.');
|
|
379
|
+
}
|
|
380
|
+
const localNameSize = archive.readUInt16LE(localOffset + 26);
|
|
381
|
+
const localExtraSize = archive.readUInt16LE(localOffset + 28);
|
|
382
|
+
const localNameStart = localOffset + 30;
|
|
383
|
+
const localNameEnd = localNameStart + localNameSize;
|
|
384
|
+
const dataStart = localNameEnd + localExtraSize;
|
|
385
|
+
const dataEnd = dataStart + compressedSize;
|
|
386
|
+
if (
|
|
387
|
+
localNameSize !== nameSize ||
|
|
388
|
+
archive.subarray(localNameStart, localNameEnd).toString('utf8') !== name ||
|
|
389
|
+
dataEnd > centralOffset
|
|
390
|
+
) {
|
|
391
|
+
throw new Error('Workspace acceptance local archive entry is invalid.');
|
|
392
|
+
}
|
|
393
|
+
const compressed = archive.subarray(dataStart, dataEnd);
|
|
394
|
+
const manifest =
|
|
395
|
+
method === 0
|
|
396
|
+
? Buffer.from(compressed)
|
|
397
|
+
: inflateRawSync(compressed, {
|
|
398
|
+
maxOutputLength: MAX_MANIFEST_BYTES,
|
|
399
|
+
});
|
|
400
|
+
if (manifest.length !== manifestSize || crc32(manifest) !== expectedCrc) {
|
|
401
|
+
throw new Error('Workspace acceptance manifest archive authority differs.');
|
|
402
|
+
}
|
|
403
|
+
return manifest;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function downloadWorkspaceAcceptance({
|
|
407
|
+
artifactDigest: requestedDigest,
|
|
408
|
+
artifactId: requestedId,
|
|
409
|
+
artifactSize: requestedSize,
|
|
410
|
+
environment,
|
|
411
|
+
fetchImpl = fetch,
|
|
412
|
+
output,
|
|
413
|
+
}) {
|
|
414
|
+
const expectedRepository = repository(environment.GITHUB_REPOSITORY);
|
|
415
|
+
const token = environment.GH_TOKEN;
|
|
416
|
+
const id = integer(requestedId, 'artifact id');
|
|
417
|
+
const digest = artifactDigest(requestedDigest);
|
|
418
|
+
const size = Number(integer(requestedSize, 'artifact size'));
|
|
419
|
+
artifactSize(size);
|
|
420
|
+
if (typeof token !== 'string' || token.length < 1) {
|
|
421
|
+
throw new Error('Workspace acceptance download token is missing.');
|
|
422
|
+
}
|
|
423
|
+
const api = environment.GITHUB_API_URL ?? 'https://api.github.com';
|
|
424
|
+
const response = await fetchImpl(
|
|
425
|
+
\`\${api}/repos/\${expectedRepository}/actions/artifacts/\${id}/zip\`,
|
|
426
|
+
{
|
|
427
|
+
headers: {
|
|
428
|
+
Accept: 'application/vnd.github+json',
|
|
429
|
+
Authorization: \`Bearer \${token}\`,
|
|
430
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
431
|
+
},
|
|
432
|
+
redirect: 'follow',
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
const archive = await boundedResponseBytes(response, MAX_ARCHIVE_BYTES);
|
|
436
|
+
if (archive.length !== size || \`sha256:\${sha256(archive)}\` !== digest) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
'Workspace acceptance archive differs from GitHub authority.',
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const manifest = acceptanceManifestFromArchive(archive);
|
|
442
|
+
if (existsSync(output)) {
|
|
443
|
+
throw new Error('Workspace acceptance restore output already exists.');
|
|
444
|
+
}
|
|
445
|
+
mkdirSync(output, { recursive: true, mode: 0o700 });
|
|
446
|
+
writeFileSync(path.join(output, 'manifest.json'), manifest, {
|
|
447
|
+
mode: 0o600,
|
|
448
|
+
});
|
|
449
|
+
return { digest, id, size };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Declining reuse is always safe: the complete verification lane runs. Reuse
|
|
453
|
+
// is claimed only when one protected promotion, the identical reviewed tree,
|
|
454
|
+
// one successful workflow run, and one retained artifact all agree.
|
|
455
|
+
export async function resolveWorkspaceAcceptance({
|
|
456
|
+
environment,
|
|
457
|
+
fetchImpl = fetch,
|
|
458
|
+
root: requested,
|
|
459
|
+
}) {
|
|
460
|
+
const { root, source } = rootIdentity(requested);
|
|
461
|
+
const expectedRepository = repository(environment.GITHUB_REPOSITORY);
|
|
462
|
+
const token = environment.GH_TOKEN;
|
|
463
|
+
if (
|
|
464
|
+
typeof token !== 'string' ||
|
|
465
|
+
token.length < 1 ||
|
|
466
|
+
environment.GITHUB_EVENT_NAME !== 'push' ||
|
|
467
|
+
environment.GITHUB_REF !== 'refs/heads/main' ||
|
|
468
|
+
environment.GITHUB_SHA !== source.commit
|
|
469
|
+
) {
|
|
470
|
+
throw new Error('Workspace merge lookup lacks protected-main authority.');
|
|
471
|
+
}
|
|
472
|
+
const api = environment.GITHUB_API_URL ?? 'https://api.github.com';
|
|
473
|
+
try {
|
|
474
|
+
const pulls = await json(
|
|
475
|
+
\`\${api}/repos/\${expectedRepository}/commits/\${source.commit}/pulls\`,
|
|
476
|
+
token,
|
|
477
|
+
fetchImpl,
|
|
478
|
+
);
|
|
479
|
+
const eligible = pulls.filter(
|
|
480
|
+
(pull) =>
|
|
481
|
+
pull?.merged_at &&
|
|
482
|
+
pull?.merge_commit_sha === source.commit &&
|
|
483
|
+
pull?.base?.ref === 'main' &&
|
|
484
|
+
pull?.head?.ref === 'develop' &&
|
|
485
|
+
pull?.head?.repo?.full_name === expectedRepository,
|
|
486
|
+
);
|
|
487
|
+
if (eligible.length !== 1) {
|
|
488
|
+
return {
|
|
489
|
+
reason: \`eligible protected promotions: \${eligible.length}\`,
|
|
490
|
+
reused: false,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const pull = eligible[0];
|
|
494
|
+
const headCommit = pull.head?.sha;
|
|
495
|
+
if (typeof headCommit !== 'string' || !/^[a-f0-9]{40}$/u.test(headCommit)) {
|
|
496
|
+
return { reason: 'promotion head commit is not exact', reused: false };
|
|
497
|
+
}
|
|
498
|
+
if (git(root, ['rev-parse', \`\${headCommit}^{tree}\`]) !== source.tree) {
|
|
499
|
+
return {
|
|
500
|
+
reason: 'merged tree differs from the reviewed promotion tree',
|
|
501
|
+
reused: false,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const query = new URL(
|
|
505
|
+
\`\${api}/repos/\${expectedRepository}/actions/workflows/ci.yml/runs\`,
|
|
506
|
+
);
|
|
507
|
+
query.searchParams.set('event', 'pull_request');
|
|
508
|
+
query.searchParams.set('head_sha', headCommit);
|
|
509
|
+
query.searchParams.set('status', 'success');
|
|
510
|
+
query.searchParams.set('per_page', '100');
|
|
511
|
+
const observed = await json(query.href, token, fetchImpl);
|
|
512
|
+
const runs = observed.workflow_runs.filter(
|
|
513
|
+
(run) =>
|
|
514
|
+
run?.conclusion === 'success' &&
|
|
515
|
+
run?.event === 'pull_request' &&
|
|
516
|
+
run?.head_sha === headCommit &&
|
|
517
|
+
run?.head_branch === 'develop' &&
|
|
518
|
+
run?.head_repository?.full_name === expectedRepository &&
|
|
519
|
+
run?.path === WORKFLOW &&
|
|
520
|
+
run?.pull_requests?.some(({ number }) => number === pull.number) &&
|
|
521
|
+
Number.isSafeInteger(run.id) &&
|
|
522
|
+
Number.isSafeInteger(run.run_attempt),
|
|
523
|
+
);
|
|
524
|
+
runs.sort(
|
|
525
|
+
(left, right) =>
|
|
526
|
+
right.run_attempt - left.run_attempt || right.id - left.id,
|
|
527
|
+
);
|
|
528
|
+
for (const run of runs) {
|
|
529
|
+
const artifactName = \`workspace-acceptance-\${headCommit}-\${run.id}-\${run.run_attempt}\`;
|
|
530
|
+
const retained = await json(
|
|
531
|
+
\`\${api}/repos/\${expectedRepository}/actions/runs/\${run.id}/artifacts?per_page=100\`,
|
|
532
|
+
token,
|
|
533
|
+
fetchImpl,
|
|
534
|
+
);
|
|
535
|
+
const artifacts = Array.isArray(retained?.artifacts)
|
|
536
|
+
? retained.artifacts.filter(
|
|
537
|
+
(artifact) =>
|
|
538
|
+
artifact?.name === artifactName && artifact?.expired === false,
|
|
539
|
+
)
|
|
540
|
+
: [];
|
|
541
|
+
if (artifacts.length === 1) {
|
|
542
|
+
const [artifact] = artifacts;
|
|
543
|
+
const id = artifactId(artifact.id);
|
|
544
|
+
const digest = artifactDigest(artifact.digest);
|
|
545
|
+
const size = String(artifactSize(artifact.size_in_bytes));
|
|
546
|
+
if (
|
|
547
|
+
artifact.archive_download_url !==
|
|
548
|
+
\`\${api}/repos/\${expectedRepository}/actions/artifacts/\${id}/zip\` ||
|
|
549
|
+
artifact.workflow_run?.id !== run.id ||
|
|
550
|
+
artifact.workflow_run?.head_sha !== headCommit
|
|
551
|
+
) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
artifactDigest: digest,
|
|
556
|
+
artifactId: id,
|
|
557
|
+
artifactName,
|
|
558
|
+
artifactSize: size,
|
|
559
|
+
headCommit,
|
|
560
|
+
reused: true,
|
|
561
|
+
runAttempt: String(run.run_attempt),
|
|
562
|
+
runId: String(run.id),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
reason: 'no successful promotion run retains the acceptance artifact',
|
|
568
|
+
reused: false,
|
|
569
|
+
};
|
|
570
|
+
} catch (error) {
|
|
571
|
+
return {
|
|
572
|
+
reason: \`acceptance resolution failed: \${error?.message ?? error}\`,
|
|
573
|
+
reused: false,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function main(argv) {
|
|
579
|
+
const [operation, ...args] = argv;
|
|
580
|
+
if (operation === 'seal' && args.length === 1) {
|
|
581
|
+
sealWorkspaceAcceptance({
|
|
582
|
+
environment: process.env,
|
|
583
|
+
output: path.resolve(args[0]),
|
|
584
|
+
root: process.cwd(),
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (operation === 'verify' && args.length === 4) {
|
|
589
|
+
verifyWorkspaceAcceptance({
|
|
590
|
+
headCommit: args[1],
|
|
591
|
+
manifestPath: path.resolve(args[0]),
|
|
592
|
+
repository: process.env.GITHUB_REPOSITORY,
|
|
593
|
+
root: process.cwd(),
|
|
594
|
+
runAttempt: args[3],
|
|
595
|
+
runId: args[2],
|
|
596
|
+
});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (operation === 'download' && args.length === 4) {
|
|
600
|
+
await downloadWorkspaceAcceptance({
|
|
601
|
+
artifactDigest: args[2],
|
|
602
|
+
artifactId: args[1],
|
|
603
|
+
artifactSize: args[3],
|
|
604
|
+
environment: process.env,
|
|
605
|
+
output: path.resolve(args[0]),
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (operation === 'resolve' && args.length === 0) {
|
|
610
|
+
const result = await resolveWorkspaceAcceptance({
|
|
611
|
+
environment: process.env,
|
|
612
|
+
root: process.cwd(),
|
|
613
|
+
});
|
|
614
|
+
const output = process.env.GITHUB_OUTPUT;
|
|
615
|
+
if (typeof output !== 'string' || !path.isAbsolute(output)) {
|
|
616
|
+
throw new Error('Workspace acceptance output authority is missing.');
|
|
617
|
+
}
|
|
618
|
+
if (result.reused !== true && result.reason) {
|
|
619
|
+
console.warn(
|
|
620
|
+
\`Workspace acceptance reuse declined (\${result.reason}); the complete verification lane runs instead.\`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
for (const [name, value] of Object.entries({
|
|
624
|
+
artifact_digest: result.artifactDigest ?? '',
|
|
625
|
+
artifact_id: result.artifactId ?? '',
|
|
626
|
+
artifact_name: result.artifactName ?? '',
|
|
627
|
+
artifact_size: result.artifactSize ?? '',
|
|
628
|
+
head_commit: result.headCommit ?? '',
|
|
629
|
+
reused: String(result.reused),
|
|
630
|
+
run_attempt: result.runAttempt ?? '',
|
|
631
|
+
run_id: result.runId ?? '',
|
|
632
|
+
})) {
|
|
633
|
+
appendFileSync(output, \`\${name}=\${value}\\n\`, 'utf8');
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
throw new Error(
|
|
638
|
+
'Usage: workspace-acceptance.mjs seal <manifest> | download <output> <artifact-id> <artifact-digest> <artifact-size> | verify <manifest> <head> <run> <attempt> | resolve',
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
643
|
+
await main(process.argv.slice(2));
|
|
644
|
+
}
|
|
645
|
+
`,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
//# sourceMappingURL=workspace-files-ci-acceptance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-files-ci-acceptance.js","sourceRoot":"","sources":["../../../../../../packages/nx/src/generators/preset/workspace-files-ci-acceptance.ts"],"names":[],"mappings":";;AAAA,4EAqoBC;AAroBD,SAAgB,gCAAgC;IAG9C,OAAO;QACL,mCAAmC,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+nBxC;KACE,CAAC;AACJ,CAAC"}
|