@xemahq/xema 0.40.3 → 0.41.1
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/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +289 -19
- package/dist/edition/verify-image-evidence.bundle.provenance.json +71 -47
- package/dist/runtime/edition/verb-map.d.ts +3 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +4 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +5 -0
- package/edition/catalogue-evidence.mjs +225 -0
- package/edition/catalogue-fetch.mjs +40 -7
- package/edition/customer-edition-verbs.mjs +477 -0
- package/edition/customer-flags.mjs +2 -0
- package/edition/develop-coverage-readers.mjs +212 -0
- package/edition/develop-coverage-run.mjs +48 -0
- package/edition/develop-coverage-verdict.mjs +153 -0
- package/edition/develop-coverage.mjs +120 -0
- package/edition/develop-edition-resolve.mjs +18 -7
- package/edition/develop-edition.mjs +47 -220
- package/edition/develop-freeze.mjs +50 -13
- package/edition/develop-image-probe.mjs +7 -7
- package/edition/dispatched-runtime-components.mjs +90 -1
- package/edition/edition-artifact.mjs +31 -14
- package/edition/edition-constants.mjs +9 -0
- package/edition/edition-contract.json +2 -1
- package/edition/{develop-seal.mjs → edition-image-seal.mjs} +36 -17
- package/edition/edition-workspace.mjs +4 -3
- package/edition/evidence-trust-pin.mjs +28 -8
- package/edition/evidence-trust-policy.mjs +51 -7
- package/edition/image-evidence.mjs +2 -2
- package/edition/lib.mjs +20 -11
- package/edition/oci-image-source-evidence.mjs +3 -0
- package/edition/oras-process.mjs +3 -3
- package/edition/oras.mjs +47 -5
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-source-evidence.mjs +11 -2
- package/edition/release-edition.mjs +54 -0
- package/edition/repin-evidence-trust.mjs +5 -3
- package/edition/resolve-images.mjs +40 -5
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/trust-policy-arming.mjs +14 -1
- package/edition/verify-catalogue-evidence.mjs +245 -0
- package/edition/verify-image-evidence.mjs +15 -1
- package/package.json +2 -2
- package/edition/develop-customer-edition.mjs +0 -149
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// --sources-root, --cache-dir (no --deploy-root, no --kernel: the kernel is the catalogue's, from npm).
|
|
19
19
|
// target <id> --pins <file> --registry <root> --engine-image <repository>@sha256:<digest>
|
|
20
20
|
// print <root>/<develop segment>/xema-edition-<id>:<tag>, the tag being sha256 of the canonical
|
|
21
|
-
// { cli, engine, pins, sourceImages } (
|
|
21
|
+
// { cli, engine, pins, sourceImages } (snapshotEditionTag): every input of the bytes the artifact records,
|
|
22
22
|
// so the same pins with another engine, source-image map or CLI version are another artifact. The root is always the caller's
|
|
23
23
|
// registry root (e.g. registry.example.com/acme); the develop namespace segment and the repository
|
|
24
24
|
// prefix come from edition-contract.json, and no root is implied.
|
|
@@ -38,6 +38,12 @@
|
|
|
38
38
|
// the same for a customer artifact; its descriptor's catalogue provenance pin must also equal the
|
|
39
39
|
// pinned develop catalogue digest, and its own image rows must be the rows the --source-image map composes. It fetches that catalogue (as `resolve --definition` does) to take the
|
|
40
40
|
// kernel from it. Takes --data-root, --cache-dir.
|
|
41
|
+
// coverage <id> --pins <file> --cosign <absolute-path> [--json]
|
|
42
|
+
// READ-ONLY: for every image subject of the snapshot, whether its develop image is producer-signed
|
|
43
|
+
// in the form T72 resolve accepts (attested | signature-verified | no-row | unsigned |
|
|
44
|
+
// annotations-missing | annotation-mismatch | not-built). Prints a table grouped by producer repo
|
|
45
|
+
// and `covered N/M` (--json: the same as data). Exit 0 only when every subject is covered, 1 when
|
|
46
|
+
// not, 2 on an operational error. Takes the roots `resolve` takes; writes nothing.
|
|
41
47
|
// check-overrides --overrides <json>
|
|
42
48
|
// refuse a pins override that would select the tooling this run executes
|
|
43
49
|
// (the edition tooling's own repository); the workflow runs it before any other checkout.
|
|
@@ -51,27 +57,21 @@
|
|
|
51
57
|
// Tokens come from the environment (EVIDENCE_REPOSITORIES_TOKEN, XEMA_GHCR_PULL_TOKEN),
|
|
52
58
|
// never argv. Nothing here writes git or publishes a release edition.
|
|
53
59
|
|
|
54
|
-
import { writeFileSync } from 'node:fs';
|
|
55
60
|
import { resolve } from 'node:path';
|
|
56
61
|
|
|
57
|
-
import { fetchCatalogue } from './catalogue-fetch.mjs';
|
|
58
|
-
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
59
62
|
import { writeCatalogue } from './catalogue.mjs';
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
63
|
+
import { CUSTOMER_ROOTS, headResolver, parseEditionVerbArgs, runCustomerVerb, writeOnce } from './customer-edition-verbs.mjs';
|
|
64
|
+
import { DEFINITION_FLAG } from './customer-flags.mjs';
|
|
65
|
+
import { runDevelopCoverage } from './develop-coverage-run.mjs';
|
|
66
|
+
import { CoverageExit } from './develop-coverage.mjs';
|
|
62
67
|
import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
63
|
-
import { assertNoToolingOverride,
|
|
64
|
-
import {
|
|
65
|
-
import { LOCAL_WORKFLOW, assertEngineImage, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
66
|
-
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
68
|
+
import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
|
|
69
|
+
import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
67
70
|
import { EditionChannel } from './edition-constants.mjs';
|
|
68
71
|
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
69
72
|
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
70
|
-
import {
|
|
71
|
-
import { adoptCatalogueKernel } from './kernel-package.mjs';
|
|
72
|
-
import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
73
|
+
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
73
74
|
import { snapshotPinSource } from './pin-source.mjs';
|
|
74
|
-
import { assertSourceImagesCoverDefinition, parseSourceImages } from './resolve-customer-edition.mjs';
|
|
75
75
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
76
76
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
77
77
|
|
|
@@ -81,12 +81,9 @@ import { readStrictJsonFile } from './strict-json.mjs';
|
|
|
81
81
|
// takes only the data root in either form: it judges its --overrides JSON against the pin roles the data
|
|
82
82
|
// root declares, so it reads nothing else and refuses every other root flag.
|
|
83
83
|
const VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.Cache],
|
|
88
|
-
'freeze-sources': [WorkspaceRoot.Data],
|
|
89
|
-
});
|
|
84
|
+
// `coverage` is the P2b gate: 1 means "not covered", so an operational error is a distinct 2.
|
|
85
|
+
const COVERAGE_COMMAND = 'coverage';
|
|
86
|
+
const COVERAGE_OPERATIONAL_EXIT = CoverageExit.Operational;
|
|
90
87
|
const DATA_ROOT_ONLY_COMMANDS = Object.freeze(['check-overrides']);
|
|
91
88
|
|
|
92
89
|
if (isEntryModule(import.meta.url)) {
|
|
@@ -95,103 +92,15 @@ if (isEntryModule(import.meta.url)) {
|
|
|
95
92
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data], { refuseOthers: true });
|
|
96
93
|
} else {
|
|
97
94
|
const customerForm = process.argv.includes(DEFINITION_FLAG);
|
|
98
|
-
configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, {
|
|
95
|
+
configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, {
|
|
96
|
+
refuseOthers: customerForm && CUSTOMER_ROOTS[command] !== undefined,
|
|
97
|
+
...(command === COVERAGE_COMMAND ? { failureExitCode: COVERAGE_OPERATIONAL_EXIT } : {}),
|
|
98
|
+
});
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const CREDENTIAL_FLAGS = Object.freeze([CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
|
|
105
|
-
const REPEATABLE = Object.freeze([SOURCE_IMAGE_FLAG]);
|
|
106
|
-
|
|
107
|
-
const COMMANDS = Object.freeze({
|
|
108
|
-
resolve: {
|
|
109
|
-
positional: true,
|
|
110
|
-
values: ['--pins', '--out', '--registry', '--engine-image'],
|
|
111
|
-
switches: ['--local'],
|
|
112
|
-
required: ['--pins', '--out', '--registry', '--engine-image'],
|
|
113
|
-
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [] },
|
|
114
|
-
},
|
|
115
|
-
target: {
|
|
116
|
-
positional: true,
|
|
117
|
-
values: ['--pins', '--registry', '--engine-image'],
|
|
118
|
-
switches: [],
|
|
119
|
-
required: ['--pins', '--registry', '--engine-image'],
|
|
120
|
-
customer: { values: [DEFINITION_FLAG, SOURCE_IMAGE_FLAG], required: ['--pins', '--registry', '--engine-image'], refused: [] },
|
|
121
|
-
},
|
|
122
|
-
'freeze-graph': {
|
|
123
|
-
positional: false,
|
|
124
|
-
values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
|
|
125
|
-
switches: [],
|
|
126
|
-
required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
|
|
127
|
-
},
|
|
128
|
-
'freeze-sources': {
|
|
129
|
-
positional: true,
|
|
130
|
-
values: ['--pins', '--overrides', '--out'],
|
|
131
|
-
switches: [],
|
|
132
|
-
required: ['--pins', '--out'],
|
|
133
|
-
customer: { values: [DEFINITION_FLAG], required: ['--out'], refused: ['--pins', '--overrides'] },
|
|
134
|
-
},
|
|
135
|
-
'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
|
|
136
|
-
'check-existing': {
|
|
137
|
-
positional: true,
|
|
138
|
-
values: ['--pins', '--artifact', '--engine-image'],
|
|
139
|
-
switches: [],
|
|
140
|
-
required: ['--pins', '--artifact', '--engine-image'],
|
|
141
|
-
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG], required: ['--pins', '--artifact', '--engine-image'], refused: [] },
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
/** Parse `<command> [<id>] --flag value ...` against the closed vocabulary of the command. */
|
|
146
|
-
export function parseDevelopEditionArgs(argv) {
|
|
147
|
-
const [command, ...rest] = argv;
|
|
148
|
-
const spec = COMMANDS[command];
|
|
149
|
-
if (spec === undefined) {
|
|
150
|
-
throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(COMMANDS).join(' | ')}`);
|
|
151
|
-
}
|
|
152
|
-
const customerValues = spec.customer?.values ?? [];
|
|
153
|
-
const options = {};
|
|
154
|
-
const positional = [];
|
|
155
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
156
|
-
const arg = rest[i];
|
|
157
|
-
if (spec.switches.includes(arg)) {
|
|
158
|
-
options[arg] = true;
|
|
159
|
-
} else if (spec.values.includes(arg) || customerValues.includes(arg)) {
|
|
160
|
-
const value = rest[i + 1];
|
|
161
|
-
if (value === undefined || (value.startsWith('--') && value.length > 2)) {
|
|
162
|
-
throw new Error(`${arg} requires a value`);
|
|
163
|
-
}
|
|
164
|
-
if (REPEATABLE.includes(arg)) {
|
|
165
|
-
options[arg] = [...(options[arg] ?? []), value];
|
|
166
|
-
} else if (options[arg] !== undefined) {
|
|
167
|
-
throw new Error(`${arg} given twice`);
|
|
168
|
-
} else {
|
|
169
|
-
options[arg] = value;
|
|
170
|
-
}
|
|
171
|
-
i += 1;
|
|
172
|
-
} else if (arg.startsWith('--')) {
|
|
173
|
-
throw new Error(`unknown argument ${arg} for ${command}`);
|
|
174
|
-
} else {
|
|
175
|
-
positional.push(arg);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const customer = spec.customer !== undefined && customerValues.some((flag) => options[flag] !== undefined);
|
|
179
|
-
if (customer) {
|
|
180
|
-
if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
|
|
181
|
-
for (const refused of spec.customer.refused) {
|
|
182
|
-
if (options[refused] !== undefined) {throw new Error(`a customer ${command} takes no ${refused}`);}
|
|
183
|
-
}
|
|
184
|
-
if (positional.length !== 0) {throw new Error(`a customer ${command} takes no <distribution-id>: the id is the definition's`);}
|
|
185
|
-
} else if (spec.positional ? positional.length !== 1 : positional.length !== 0) {
|
|
186
|
-
throw new Error(`${command} takes ${spec.positional ? 'exactly one <distribution-id>' : 'no positional argument'}`);
|
|
187
|
-
}
|
|
188
|
-
for (const required of customer ? spec.customer.required : spec.required) {
|
|
189
|
-
if (options[required] === undefined) {throw new Error(`${command} requires ${required}`);}
|
|
190
|
-
}
|
|
191
|
-
if (spec.positional && !customer) {assertCanonicalDistributionId(positional[0]);}
|
|
192
|
-
if (options['--engine-image'] !== undefined) {assertEngineImage(options['--engine-image']);}
|
|
193
|
-
return { command, id: positional[0], options, customer };
|
|
194
|
-
}
|
|
102
|
+
/** Parse `<command> [<id>] --flag value ...` against the closed vocabulary of the develop command. */
|
|
103
|
+
export const parseDevelopEditionArgs = (argv) => parseEditionVerbArgs(EditionChannel.Develop, argv);
|
|
195
104
|
|
|
196
105
|
function parseOverrides(text) {
|
|
197
106
|
if (text === undefined || text.trim() === '') {return {};}
|
|
@@ -202,102 +111,16 @@ function parseOverrides(text) {
|
|
|
202
111
|
return parsed;
|
|
203
112
|
}
|
|
204
113
|
|
|
205
|
-
function writeOnce(path, value) {
|
|
206
|
-
writeFileSync(path, serializeJson(value), { flag: 'wx' });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function headResolver(env) {
|
|
210
|
-
const workspace = editionWorkspaceConfig();
|
|
211
|
-
return { resolveHead: (source) => lsRemoteDevelopHead(source, { workspace, env }), holdSource: (source) => holdSource(source, workspace, env) };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/** The pinned develop catalogue of a customer definition: fetched by its digest, as every customer develop verb does. */
|
|
215
|
-
function fetchPinnedDevelopCatalogue(options, env) {
|
|
216
|
-
return fetchCatalogue({
|
|
217
|
-
ref: readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop }),
|
|
218
|
-
channel: EditionChannel.Develop,
|
|
219
|
-
username: options[CATALOGUE_USERNAME_FLAG],
|
|
220
|
-
tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
|
|
221
|
-
env,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** A customer develop resolve: the artifact (descriptor and four locks, no catalogue: it is not an offering) goes to --out. */
|
|
226
|
-
async function resolveCustomer(options, env) {
|
|
227
|
-
const { catalogue, digest } = await fetchPinnedDevelopCatalogue(options, env);
|
|
228
|
-
const rawDefinition = readStrictJsonFile(options[DEFINITION_FLAG]);
|
|
229
|
-
const sourceImages = parseSourceImages(options[SOURCE_IMAGE_FLAG] ?? []);
|
|
230
|
-
// The catalogue's kernel is adopted inside the resolve, before any kernel schema is loaded; the evidence trust
|
|
231
|
-
// state loads kernel schemas, so it is read after.
|
|
232
|
-
const { id, pins, ...resolved } = await resolveCustomerDevelopEdition({
|
|
233
|
-
rawDefinition,
|
|
234
|
-
catalogue,
|
|
235
|
-
catalogueDigest: digest,
|
|
236
|
-
pins: readStrictJsonFile(options['--pins']),
|
|
237
|
-
sourceImages,
|
|
238
|
-
env,
|
|
239
|
-
});
|
|
240
|
-
const evidenceTrustPolicy = await descriptorEvidenceTrust({ channel: EditionChannel.Develop });
|
|
241
|
-
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage: options['--engine-image'], sourceImages });
|
|
242
|
-
writeEditionArtifact({
|
|
243
|
-
outDir: resolve(options['--out']),
|
|
244
|
-
channel: EditionChannel.Develop,
|
|
245
|
-
distributionId: id,
|
|
246
|
-
pins,
|
|
247
|
-
workflow: options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env),
|
|
248
|
-
evidenceTrustPolicy,
|
|
249
|
-
engineImage: options['--engine-image'],
|
|
250
|
-
distributionLock: resolved.distributionLock,
|
|
251
|
-
imageLock: resolved.imageLock,
|
|
252
|
-
authorizationLock: resolved.authorizationLock,
|
|
253
|
-
chartSetLock: resolved.chartSetLock,
|
|
254
|
-
});
|
|
255
|
-
console.log(target);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** A customer check-existing: the pinned catalogue is fetched to take its kernel; the digest fetched is the digest the artifact is compared with. */
|
|
259
|
-
export async function checkExistingCustomer(options, env, { adopt = adoptCatalogueKernel } = {}) {
|
|
260
|
-
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
261
|
-
const sourceImages = customerSourceImages(options, frame.rawDefinition);
|
|
262
|
-
const { catalogue, digest } = await fetchPinnedDevelopCatalogue(options, env);
|
|
263
|
-
if (digest !== frame.catalogueDigest) {
|
|
264
|
-
throw new Error(`the fetched catalogue is ${digest}, not the pinned ${frame.catalogueDigest}`);
|
|
265
|
-
}
|
|
266
|
-
await adopt(catalogue);
|
|
267
|
-
assertExistingArtifactMatches({
|
|
268
|
-
artifactDir: resolve(options['--artifact']),
|
|
269
|
-
distributionId: frame.id,
|
|
270
|
-
channel: EditionChannel.Develop,
|
|
271
|
-
evidenceTrustPolicy: await descriptorEvidenceTrust({ channel: EditionChannel.Develop }),
|
|
272
|
-
pins: frame.pins,
|
|
273
|
-
engineImage: options['--engine-image'],
|
|
274
|
-
catalogueDigest: frame.catalogueDigest,
|
|
275
|
-
});
|
|
276
|
-
assertOwnImageRowsMatch({ artifactDir: resolve(options['--artifact']), sourceImages });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** The parsed --source-image map of a customer verb, covering exactly the definition's declared sources. */
|
|
280
|
-
function customerSourceImages(options, rawDefinition) {
|
|
281
|
-
const sourceImages = parseSourceImages(options[SOURCE_IMAGE_FLAG] ?? []);
|
|
282
|
-
assertSourceImagesCoverDefinition(rawDefinition, sourceImages);
|
|
283
|
-
return sourceImages;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
114
|
async function run(argv, env) {
|
|
287
115
|
const { command, id, options, customer } = parseDevelopEditionArgs(argv);
|
|
288
116
|
switch (command) {
|
|
289
117
|
case 'target': {
|
|
290
|
-
if (customer) {
|
|
291
|
-
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
292
|
-
const sourceImages = customerSourceImages(options, frame.rawDefinition);
|
|
293
|
-
console.log(developEditionTarget(frame.id, frame.pins, { registry: options['--registry'], engineImage: options['--engine-image'], sourceImages }));
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
118
|
+
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
296
119
|
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'] }));
|
|
297
120
|
return;
|
|
298
121
|
}
|
|
299
122
|
case 'resolve': {
|
|
300
|
-
if (customer) {return
|
|
123
|
+
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
301
124
|
const pins = readStrictJsonFile(options['--pins']);
|
|
302
125
|
const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
|
|
303
126
|
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage: options['--engine-image'] });
|
|
@@ -318,8 +141,16 @@ async function run(argv, env) {
|
|
|
318
141
|
console.log(target);
|
|
319
142
|
return;
|
|
320
143
|
}
|
|
144
|
+
case 'coverage':
|
|
145
|
+
return runDevelopCoverage({
|
|
146
|
+
id,
|
|
147
|
+
pins: readStrictJsonFile(options['--pins']),
|
|
148
|
+
cosignBinary: options['--cosign'],
|
|
149
|
+
json: options['--json'] === true,
|
|
150
|
+
env,
|
|
151
|
+
});
|
|
321
152
|
case 'check-existing': {
|
|
322
|
-
if (customer) {return
|
|
153
|
+
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
323
154
|
assertExistingArtifactMatches({
|
|
324
155
|
artifactDir: resolve(options['--artifact']),
|
|
325
156
|
distributionId: id,
|
|
@@ -342,29 +173,19 @@ async function run(argv, env) {
|
|
|
342
173
|
deploySha: options['--deploy-sha'],
|
|
343
174
|
editionsRootUrl: options['--editions-root-url'],
|
|
344
175
|
overrides: parseOverrides(options['--overrides']),
|
|
345
|
-
...headResolver(env),
|
|
176
|
+
...headResolver(EditionChannel.Develop, env),
|
|
346
177
|
});
|
|
347
178
|
writeOnce(options['--out'], pins);
|
|
348
179
|
return;
|
|
349
180
|
}
|
|
350
181
|
case 'freeze-sources': {
|
|
351
|
-
if (customer) {
|
|
352
|
-
writeOnce(
|
|
353
|
-
options['--out'],
|
|
354
|
-
freezeCustomerSources({
|
|
355
|
-
definition: readStrictJsonFile(options[DEFINITION_FLAG]),
|
|
356
|
-
...headResolver(env),
|
|
357
|
-
sourceRepositoryUrlPrefix: editionWorkspaceConfig().sourceRepositoryUrlPrefix,
|
|
358
|
-
}),
|
|
359
|
-
);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
182
|
+
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
362
183
|
const pins = readStrictJsonFile(options['--pins']);
|
|
363
184
|
const { parseDistribution } = await loadKernelDistributionSchemas();
|
|
364
185
|
const declaredSources = composeEdition(parseDistribution, id, snapshotPinSource(pins)).declaredSources;
|
|
365
186
|
writeOnce(
|
|
366
187
|
options['--out'],
|
|
367
|
-
freezeSources({ pins, declaredSources, overrides: parseOverrides(options['--overrides']), ...headResolver(env) }),
|
|
188
|
+
freezeSources({ pins, declaredSources, overrides: parseOverrides(options['--overrides']), ...headResolver(EditionChannel.Develop, env) }),
|
|
368
189
|
);
|
|
369
190
|
return;
|
|
370
191
|
}
|
|
@@ -374,8 +195,14 @@ async function run(argv, env) {
|
|
|
374
195
|
}
|
|
375
196
|
|
|
376
197
|
if (isEntryModule(import.meta.url)) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
198
|
+
const argv = process.argv.slice(2);
|
|
199
|
+
run(argv, process.env).then(
|
|
200
|
+
(code) => {
|
|
201
|
+
if (typeof code === 'number') {process.exitCode = code;}
|
|
202
|
+
},
|
|
203
|
+
(error) => {
|
|
204
|
+
console.error(`[develop-edition] ${error.message}`);
|
|
205
|
+
process.exit(argv[0] === COVERAGE_COMMAND ? COVERAGE_OPERATIONAL_EXIT : 1);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
381
208
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
17
|
|
|
18
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
18
19
|
import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
|
|
19
20
|
import { canonicalPublicHttpsRepositoryUrl } from './lib.mjs';
|
|
20
21
|
import {
|
|
@@ -29,35 +30,71 @@ import {
|
|
|
29
30
|
sourcePinKey,
|
|
30
31
|
} from './pin-source.mjs';
|
|
31
32
|
|
|
32
|
-
const DEVELOP_REF = 'refs/heads/develop';
|
|
33
33
|
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
34
34
|
|
|
35
|
+
/** The ref namespaces a freeze may resolve a ref in. */
|
|
36
|
+
export const RefKind = Object.freeze({ Branch: 'branch', Tag: 'tag' });
|
|
37
|
+
|
|
38
|
+
const REF_PREFIX = Object.freeze({ [RefKind.Branch]: 'refs/heads/', [RefKind.Tag]: 'refs/tags/' });
|
|
39
|
+
const PEELED_SUFFIX = '^{}';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What each channel freezes a source at. A develop edition freezes the head of the `develop` BRANCH of every
|
|
43
|
+
* repository; a release edition freezes each source at the ref its definition declares, a branch or a tag.
|
|
44
|
+
*/
|
|
45
|
+
const FREEZE_REF_KINDS = Object.freeze({
|
|
46
|
+
[EditionChannel.Develop]: Object.freeze([RefKind.Branch]),
|
|
47
|
+
[EditionChannel.Release]: Object.freeze([RefKind.Branch, RefKind.Tag]),
|
|
48
|
+
});
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
51
|
+
* The commit `ref` names in one source `{ name, url }`, by `git ls-remote <url> <refs/heads/ref | refs/tags/ref |
|
|
52
|
+
* refs/tags/ref^{}>` (only the namespaces the channel freezes at). A tag resolves to its peeled commit when it is annotated.
|
|
53
|
+
* Throws naming the repository when the ref is absent, or when it is both a branch and a tag. The source is held and its
|
|
54
|
+
* git environment built by `withSourceGitEnv`: the workspace credential reaches only a repository under `sourceRepositoryUrlPrefix`.
|
|
39
55
|
*/
|
|
40
|
-
export function
|
|
56
|
+
export function lsRemoteRef(source, ref, { channel, workspace, env = process.env, runGit = execFileSync }) {
|
|
57
|
+
const kinds = FREEZE_REF_KINDS[channel];
|
|
58
|
+
if (kinds === undefined) {throw new Error(`unknown edition channel ${JSON.stringify(channel)}`);}
|
|
59
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
60
|
+
throw new Error(`source "${source.name}" declares no ref; a ${channel} edition cannot be frozen without one`);
|
|
61
|
+
}
|
|
62
|
+
const names = kinds.flatMap((kind) => (kind === RefKind.Tag ? [`${REF_PREFIX[kind]}${ref}`, `${REF_PREFIX[kind]}${ref}${PEELED_SUFFIX}`] : [`${REF_PREFIX[kind]}${ref}`]));
|
|
41
63
|
const output = withSourceGitEnv(source, { workspace, env }, (gitEnv) =>
|
|
42
|
-
runGit('git', ['ls-remote', source.url,
|
|
64
|
+
runGit('git', ['ls-remote', source.url, ...names], {
|
|
43
65
|
env: gitEnv,
|
|
44
66
|
encoding: 'utf8',
|
|
45
67
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
68
|
}),
|
|
47
69
|
).trim();
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
const served = new Map();
|
|
71
|
+
for (const line of output.split('\n')) {
|
|
72
|
+
const [sha, name] = line.trim().split(/\s+/);
|
|
73
|
+
if (names.includes(name)) {
|
|
74
|
+
if (!SHA_PATTERN.test(sha ?? '')) {throw new Error(`${source.url}: git ls-remote answered ${JSON.stringify(line)} for ${name}`);}
|
|
75
|
+
served.set(name, sha);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const branch = served.get(`${REF_PREFIX[RefKind.Branch]}${ref}`);
|
|
79
|
+
const tagName = `${REF_PREFIX[RefKind.Tag]}${ref}`;
|
|
80
|
+
const tag = served.get(`${tagName}${PEELED_SUFFIX}`) ?? served.get(tagName);
|
|
81
|
+
if (branch !== undefined && tag !== undefined) {
|
|
82
|
+
throw new Error(`${source.url}: ${JSON.stringify(ref)} is both a branch and a tag; a ${channel} edition cannot be frozen at an ambiguous ref`);
|
|
83
|
+
}
|
|
84
|
+
const commit = branch ?? tag;
|
|
85
|
+
if (commit === undefined) {
|
|
86
|
+
throw new Error(`${source.url} has no ${kinds.map((kind) => `${REF_PREFIX[kind]}${ref}`).join(' or ')}; a ${channel} edition cannot be frozen without it`);
|
|
51
87
|
}
|
|
52
|
-
return
|
|
88
|
+
return commit;
|
|
53
89
|
}
|
|
54
90
|
|
|
55
|
-
/** A resolver that asks `resolveHead` at most once per distinct URL; it takes the source `{ name, url }`. */
|
|
91
|
+
/** A resolver that asks `resolveHead` at most once per distinct URL and ref; it takes the source `{ name, url, ref? }`. */
|
|
56
92
|
function memoizedHeads(resolveHead) {
|
|
57
93
|
const heads = new Map();
|
|
58
94
|
return (source) => {
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
const key = JSON.stringify([source.url, source.ref]);
|
|
96
|
+
if (!heads.has(key)) {heads.set(key, resolveHead(source));}
|
|
97
|
+
return heads.get(key);
|
|
61
98
|
};
|
|
62
99
|
}
|
|
63
100
|
|
|
@@ -18,20 +18,20 @@ import { registryClientsFor } from './verify-image-platforms.mjs';
|
|
|
18
18
|
|
|
19
19
|
const DEFAULT_PROBE_CONCURRENCY = 6;
|
|
20
20
|
|
|
21
|
-
/** One subject's line in the "not built yet" refusal. */
|
|
22
|
-
export function notBuiltYetMessage(subject) {
|
|
21
|
+
/** One subject's line in the "not built yet" refusal of `channel` (develop unless named). */
|
|
22
|
+
export function notBuiltYetMessage(subject, channel = 'develop') {
|
|
23
23
|
return (
|
|
24
|
-
`${subject.repo}@${subject.sourceCommit}
|
|
24
|
+
`${subject.repo}@${subject.sourceCommit} ${channel} image ` +
|
|
25
25
|
`${subject.imageRef}:${subject.contentHash} not built yet`
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** The ONE refusal naming every develop
|
|
30
|
-
export function notBuiltYetError(distributionId, missingSubjects) {
|
|
29
|
+
/** The ONE refusal naming every subject of `channel` (develop unless named) whose coordinate is absent. */
|
|
30
|
+
export function notBuiltYetError(distributionId, missingSubjects, channel = 'develop') {
|
|
31
31
|
return new Error(
|
|
32
|
-
`${missingSubjects.length}
|
|
32
|
+
`${missingSubjects.length} ${channel} image(s) of "${distributionId}" are not built yet ` +
|
|
33
33
|
"(the producer's CI is not green or still running):\n " +
|
|
34
|
-
missingSubjects.map(notBuiltYetMessage).join('\n '),
|
|
34
|
+
missingSubjects.map((subject) => notBuiltYetMessage(subject, channel)).join('\n '),
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
import { compareStrings } from './string-order.mjs';
|
|
2
2
|
|
|
3
|
+
/** A `runtime-image` edge target that is neither `{biomeId, componentKey}` nor `{supplementalRuntimeComponentId}`, or is both. */
|
|
4
|
+
export class DispatchedEdgeError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'DispatchedEdgeError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A dispatched `supplemental:<id>` the runtime-image source index does not declare as a supplemental subject. */
|
|
12
|
+
export class UndeclaredSupplementalRuntimeComponentError extends Error {
|
|
13
|
+
constructor(supplementalRuntimeComponentId, dispatchingBiomes, context) {
|
|
14
|
+
super(
|
|
15
|
+
`${context}: supplemental runtime component "${supplementalRuntimeComponentId}", dispatched by biome ` +
|
|
16
|
+
`${dispatchingBiomes.map((id) => `"${id}"`).join(', ')}, is not declared as a runtime-component subject ` +
|
|
17
|
+
'by the runtime image source index — refusing rather than shipping the dispatcher without the image it launches.',
|
|
18
|
+
);
|
|
19
|
+
this.name = 'UndeclaredSupplementalRuntimeComponentError';
|
|
20
|
+
this.supplementalRuntimeComponentId = supplementalRuntimeComponentId;
|
|
21
|
+
this.dispatchingBiomes = dispatchingBiomes;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The `supplementalRuntimeComponentId` field is read as plain data: this CLI is on
|
|
26
|
+
// @xemahq/contracts 0.83, which does not type it. XEMA-171 (CLI edition protocol
|
|
27
|
+
// v9) adopts the typed field from @xemahq/contracts >= 0.91.
|
|
28
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
29
|
+
|
|
30
|
+
/** True for the supplemental form of a projected edge. */
|
|
31
|
+
export const isSupplementalEdge = (edge) => edge.supplementalRuntimeComponentId !== undefined;
|
|
32
|
+
|
|
33
|
+
/** The identity of a projected edge: `supplemental:<id>` or `<biomeId>\0<componentKey>`. */
|
|
34
|
+
export const dispatchedEdgeIdentity = (edge) =>
|
|
35
|
+
isSupplementalEdge(edge)
|
|
36
|
+
? `supplemental:${edge.supplementalRuntimeComponentId}`
|
|
37
|
+
: `${edge.biomeId}\0${edge.componentKey}`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Project one generated-index `runtime-image` edge target into the typed union
|
|
41
|
+
* the index entry carries. A target that is neither form, or both, is refused
|
|
42
|
+
* naming the dispatching biome; no `undefined` identity can form.
|
|
43
|
+
*/
|
|
44
|
+
export function projectDispatchedTarget(dispatchingBiomeId, target) {
|
|
45
|
+
const supplemental = target?.supplementalRuntimeComponentId;
|
|
46
|
+
const biomeForm = target?.biomeId !== undefined || target?.componentKey !== undefined;
|
|
47
|
+
if (supplemental !== undefined) {
|
|
48
|
+
if (biomeForm || !isNonEmptyString(supplemental)) {
|
|
49
|
+
throw new DispatchedEdgeError(
|
|
50
|
+
`biome "${dispatchingBiomeId}" has a runtime-image edge target that mixes supplementalRuntimeComponentId with ` +
|
|
51
|
+
'biomeId/componentKey, or names an empty id — a target is exactly one form',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return { supplementalRuntimeComponentId: supplemental };
|
|
55
|
+
}
|
|
56
|
+
if (!isNonEmptyString(target?.biomeId) || !isNonEmptyString(target?.componentKey)) {
|
|
57
|
+
throw new DispatchedEdgeError(
|
|
58
|
+
`biome "${dispatchingBiomeId}" has a runtime-image edge target that is neither { biomeId, componentKey } nor ` +
|
|
59
|
+
'{ supplementalRuntimeComponentId }',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return { biomeId: target.biomeId, componentKey: target.componentKey };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const edgeLabel = (edge) =>
|
|
66
|
+
isSupplementalEdge(edge) ? `supplemental:${edge.supplementalRuntimeComponentId}` : `${edge.biomeId}/${edge.componentKey}`;
|
|
67
|
+
|
|
3
68
|
/**
|
|
4
69
|
* The runtime components an edition must carry because a biome it rosters
|
|
5
70
|
* DISPATCHES them: the kernel's `runtime-image` edges, projected per biome into
|
|
@@ -21,6 +86,10 @@ import { compareStrings } from './string-order.mjs';
|
|
|
21
86
|
* no biome drawn from a declared source. A biome drawn from a declared source
|
|
22
87
|
* that dispatches is refused for the same reason.
|
|
23
88
|
*
|
|
89
|
+
* A supplemental edge (`{supplementalRuntimeComponentId}`) names no biome: it
|
|
90
|
+
* is carried as a row `{ supplementalRuntimeComponentId, consumers }` without
|
|
91
|
+
* a biome lookup.
|
|
92
|
+
*
|
|
24
93
|
* Returns one row per distinct (target biome, component key), sorted, with the
|
|
25
94
|
* rostered biomes that dispatch it. Every unresolvable edge is refused, naming
|
|
26
95
|
* the consumer and the target.
|
|
@@ -37,12 +106,24 @@ export function resolveDispatchedRuntimeComponents(context, consumers, index) {
|
|
|
37
106
|
if (consumer.source !== undefined) {
|
|
38
107
|
throw new Error(
|
|
39
108
|
`${context}: biome "${consumer.id}", drawn from declared source "${consumer.source}", dispatches ` +
|
|
40
|
-
`runtime components (${edges.map(
|
|
109
|
+
`runtime components (${edges.map(edgeLabel).join(', ')}). ` +
|
|
41
110
|
'A dispatched image is resolved from the first-party biome index, which carries no biome from a ' +
|
|
42
111
|
'declared source — refusing rather than shipping the dispatcher without the images it launches.',
|
|
43
112
|
);
|
|
44
113
|
}
|
|
45
114
|
for (const edge of edges) {
|
|
115
|
+
if (isSupplementalEdge(edge)) {
|
|
116
|
+
// Carried as a requirement owned by the dispatching biome. Whether the
|
|
117
|
+
// runtime image source index declares it is the image plane's refusal.
|
|
118
|
+
const supplementalKey = dispatchedEdgeIdentity(edge);
|
|
119
|
+
const row = rows.get(supplementalKey) ?? {
|
|
120
|
+
supplementalRuntimeComponentId: edge.supplementalRuntimeComponentId,
|
|
121
|
+
consumers: [],
|
|
122
|
+
};
|
|
123
|
+
if (!row.consumers.includes(consumer.id)) {row.consumers.push(consumer.id);}
|
|
124
|
+
rows.set(supplementalKey, row);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
46
127
|
const edgeText = `biome "${consumer.id}" dispatches runtime component "${edge.componentKey}" of biome "${edge.biomeId}"`;
|
|
47
128
|
const candidates = index.filter(
|
|
48
129
|
(entry) => entry.id === edge.biomeId && entry.target === 'server' && entry.source === undefined,
|
|
@@ -80,3 +161,11 @@ export function resolveDispatchedRuntimeComponents(context, consumers, index) {
|
|
|
80
161
|
.sort(([a], [b]) => compareStrings(a, b))
|
|
81
162
|
.map(([, row]) => row);
|
|
82
163
|
}
|
|
164
|
+
|
|
165
|
+
/** Sort order of projected edges: biome edges by (biomeId, componentKey), then supplemental edges by id. */
|
|
166
|
+
export function compareDispatchedEdges(a, b) {
|
|
167
|
+
if (isSupplementalEdge(a) !== isSupplementalEdge(b)) {return isSupplementalEdge(a) ? 1 : -1;}
|
|
168
|
+
return isSupplementalEdge(a)
|
|
169
|
+
? compareStrings(a.supplementalRuntimeComponentId, b.supplementalRuntimeComponentId)
|
|
170
|
+
: compareStrings(a.biomeId, b.biomeId) || compareStrings(a.componentKey, b.componentKey);
|
|
171
|
+
}
|