@xemahq/xema 0.40.3 → 0.41.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/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 +281 -9
- package/dist/edition/verify-image-evidence.bundle.provenance.json +65 -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/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 +7 -0
- 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/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
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// from edition-contract.json), addressed by digest, whose titled layers are
|
|
6
6
|
// `xema-edition.json` (the descriptor) plus the lock files. It is written to a
|
|
7
7
|
// scratch directory; the workflow pushes it with oras and signs the digest.
|
|
8
|
-
// Tags are for humans only (
|
|
8
|
+
// Tags are for humans only (an edition resolved from a snapshot: the hash of every input, snapshotEditionTag; vendor release: the
|
|
9
9
|
// coordinate). Admission rests on the signature of the manifest digest, never on
|
|
10
10
|
// a channel label: no reader branches on `provenance.channel`.
|
|
11
11
|
//
|
|
@@ -19,6 +19,8 @@ import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
|
19
19
|
import { EDITION_CHART_SET_FILE, assertRosteredChartSetLock } from './edition-chart-set.mjs';
|
|
20
20
|
import {
|
|
21
21
|
CATALOGUE_PIN_KEY,
|
|
22
|
+
DEFINITION_PIN_KEY,
|
|
23
|
+
DIGEST_PIN_KEYS,
|
|
22
24
|
EDITION_DESCRIPTOR_FILE,
|
|
23
25
|
EDITION_DESCRIPTOR_SCHEMA,
|
|
24
26
|
EDITION_LOCKS,
|
|
@@ -81,7 +83,7 @@ export function editionDescriptor({ distributionId, channel, pins, workflow, evi
|
|
|
81
83
|
locks: [...EDITION_LOCKS],
|
|
82
84
|
provenance: {
|
|
83
85
|
channel,
|
|
84
|
-
pins:
|
|
86
|
+
pins: sortedSnapshotPins(pins),
|
|
85
87
|
workflow: { repository: workflow?.repository, runId: workflow?.runId },
|
|
86
88
|
},
|
|
87
89
|
evidenceTrustPolicy,
|
|
@@ -90,7 +92,7 @@ export function editionDescriptor({ distributionId, channel, pins, workflow, evi
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
const COMMIT_PATTERN = /^[0-9a-f]{40}$/;
|
|
93
|
-
const
|
|
95
|
+
const DIGEST_PIN_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
94
96
|
|
|
95
97
|
const exactKeys = (value, keys, where) => {
|
|
96
98
|
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join() !== [...keys].sort().join()) {
|
|
@@ -114,13 +116,14 @@ export function parseEditionDescriptor(value) {
|
|
|
114
116
|
}
|
|
115
117
|
const { pins, workflow } = value.provenance;
|
|
116
118
|
if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
|
|
117
|
-
throw new Error(`${EDITION_DESCRIPTOR_FILE}: provenance.pins must map pin keys to 40-hex commits (the "${
|
|
119
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: provenance.pins must map pin keys to 40-hex commits (the ${DIGEST_PIN_KEYS.map((key) => `"${key}"`).join(' and ')} pins to sha256 digests)`);
|
|
118
120
|
}
|
|
119
121
|
for (const [key, pin] of Object.entries(pins)) {
|
|
120
|
-
const
|
|
122
|
+
const digestPin = DIGEST_PIN_KEYS.includes(key);
|
|
123
|
+
const pattern = digestPin ? DIGEST_PIN_PATTERN : COMMIT_PATTERN;
|
|
121
124
|
if (typeof pin !== 'string' || !pattern.test(pin)) {
|
|
122
125
|
throw new Error(
|
|
123
|
-
|
|
126
|
+
digestPin
|
|
124
127
|
? `${EDITION_DESCRIPTOR_FILE}: provenance.pins["${key}"] must be a sha256:<64 hex> digest`
|
|
125
128
|
: `${EDITION_DESCRIPTOR_FILE}: provenance.pins must map pin keys to 40-hex commits (${JSON.stringify(key)} is not)`,
|
|
126
129
|
);
|
|
@@ -238,15 +241,17 @@ export function canonicalSourceImages(sourceImages = new Map()) {
|
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
/**
|
|
241
|
-
*
|
|
242
|
-
* the bytes the artifact records is inside it. The pins fix the locks and the descriptor's pins
|
|
244
|
+
* The tag of an edition resolved from a snapshot (a develop edition, a customer release edition): sha256 of the canonical `{ cli, engine, pins, sourceImages }`. Every input of
|
|
245
|
+
* the bytes the artifact records is inside it. The pins fix the locks and the descriptor's pins: a vendor edition's
|
|
246
|
+
* commits fix its definitions; a customer edition's pins carry its sources' commits, the catalogue digest and the
|
|
247
|
+
* digest of its canonical definition (`definition`: vendor selection, image registry mirror, Store publisher anchors); the
|
|
243
248
|
* descriptor's `engine.image` is not a pin; a customer's `--source-image` map composes its own image rows
|
|
244
249
|
* (`<registry>/<prefix><subject>`) and is not a pin either; and the CLI version is the writer of every
|
|
245
250
|
* byte. A tag over the pins alone named two artifacts that differ in any of the others, and a re-run could
|
|
246
251
|
* only refuse the existing one (`check-existing`) or reuse a stale one. `provenance.workflow` (the run that
|
|
247
252
|
* wrote the bytes) is the one recorded field outside it: a fact about the artifact, not an input to it.
|
|
248
253
|
*/
|
|
249
|
-
export function
|
|
254
|
+
export function snapshotEditionTag({ pins, engineImage, sourceImages = new Map(), version = cliVersion() }) {
|
|
250
255
|
return sha256Hex(
|
|
251
256
|
serializeJson({
|
|
252
257
|
cli: version,
|
|
@@ -257,9 +262,14 @@ export function developEditionTag({ pins, engineImage, sourceImages = new Map(),
|
|
|
257
262
|
);
|
|
258
263
|
}
|
|
259
264
|
|
|
260
|
-
/**
|
|
265
|
+
/** The target of an edition resolved from a snapshot of pins, on either channel: the tag is `snapshotEditionTag`. */
|
|
266
|
+
export function snapshotEditionTarget(channel, distributionId, pins, { registry, engineImage, sourceImages }) {
|
|
267
|
+
return editionTarget(distributionId, registry, channel, snapshotEditionTag({ pins, engineImage, sourceImages }));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** A develop edition's target: the tag is `snapshotEditionTag` of the pins, the engine and the source images. */
|
|
261
271
|
export function developEditionTarget(distributionId, pins, { registry, engineImage, sourceImages }) {
|
|
262
|
-
return
|
|
272
|
+
return snapshotEditionTarget(EditionChannel.Develop, distributionId, pins, { registry, engineImage, sourceImages });
|
|
263
273
|
}
|
|
264
274
|
|
|
265
275
|
function realpathOfPlanned(path) {
|
|
@@ -329,10 +339,11 @@ export function writeEditionArtifact({
|
|
|
329
339
|
* An existing develop tag is reused only if it IS this snapshot: the tag
|
|
330
340
|
* is a hash of its inputs, but a tag is mutable registry state, so the pulled
|
|
331
341
|
* descriptor must say the same edition, the same channel, exactly the same pins,
|
|
332
|
-
* the same engine image, the same evidence trust state and the same lock set. A customer artifact (`catalogueDigest`
|
|
333
|
-
* as its catalogue provenance
|
|
342
|
+
* the same engine image, the same evidence trust state and the same lock set. A customer artifact (`catalogueDigest` and
|
|
343
|
+
* `definitionDigest` given) must also record those digests as its catalogue and definition provenance pins. Throws naming
|
|
344
|
+
* the first difference.
|
|
334
345
|
*/
|
|
335
|
-
export function assertExistingArtifactMatches({ artifactDir, distributionId, channel, pins, engineImage, evidenceTrustPolicy, catalogueDigest }) {
|
|
346
|
+
export function assertExistingArtifactMatches({ artifactDir, distributionId, channel, pins, engineImage, evidenceTrustPolicy, catalogueDigest, definitionDigest }) {
|
|
336
347
|
const descriptorPath = join(artifactDir, EDITION_DESCRIPTOR_FILE);
|
|
337
348
|
if (!existsSync(descriptorPath)) {
|
|
338
349
|
throw new Error(`the existing artifact holds no ${EDITION_DESCRIPTOR_FILE}; refusing to reuse a tag that is not an edition`);
|
|
@@ -354,6 +365,12 @@ export function assertExistingArtifactMatches({ artifactDir, distributionId, cha
|
|
|
354
365
|
`the pinned catalogue is ${catalogueDigest}; refusing to reuse it`,
|
|
355
366
|
);
|
|
356
367
|
}
|
|
368
|
+
if (definitionDigest !== undefined && existing.provenance.pins[DEFINITION_PIN_KEY] !== definitionDigest) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`the existing artifact was resolved from customer definition ${JSON.stringify(existing.provenance.pins[DEFINITION_PIN_KEY])}, ` +
|
|
371
|
+
`this definition is ${definitionDigest} (its selection, mirror or Store publisher anchors differ); refusing to reuse it`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
357
374
|
if (serializeJson(existing.provenance.pins) !== serializeJson(sortedSnapshotPins(pins))) {
|
|
358
375
|
throw new Error('the existing artifact was resolved from different pins than this snapshot (tag and content disagree); refusing to reuse it');
|
|
359
376
|
}
|
|
@@ -48,6 +48,15 @@ if (!(CATALOGUE_PIN_KEY in EDITION_CONTRACT.provenancePins)) {
|
|
|
48
48
|
throw new Error('edition-contract.json provenancePins does not declare the catalogue pin');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** The digest of the canonical customer definition a customer edition was resolved from: every input the pins of its sources and catalogue do not fix. */
|
|
52
|
+
export const DEFINITION_PIN_KEY = 'definition';
|
|
53
|
+
if (!(DEFINITION_PIN_KEY in EDITION_CONTRACT.provenancePins)) {
|
|
54
|
+
throw new Error('edition-contract.json provenancePins does not declare the definition pin');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The provenance pins that are sha256 digests, not commits. */
|
|
58
|
+
export const DIGEST_PIN_KEYS = Object.freeze([CATALOGUE_PIN_KEY, DEFINITION_PIN_KEY]);
|
|
59
|
+
|
|
51
60
|
/** The lock files an edition can carry. */
|
|
52
61
|
export const EditionLockFile = Object.freeze({
|
|
53
62
|
Distribution: 'distribution.lock.json',
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"catalogue": "catalogue.json"
|
|
24
24
|
},
|
|
25
25
|
"provenancePins": {
|
|
26
|
-
"catalogue": "the sha256 digest (sha256:<64 hex>) of the offering artifact a customer edition was resolved against
|
|
26
|
+
"catalogue": "the sha256 digest (sha256:<64 hex>) of the offering artifact a customer edition was resolved against",
|
|
27
|
+
"definition": "the sha256 digest (sha256:<64 hex>) of the canonical JSON of the customer definition a customer edition was resolved from (vendor selection, image registry mirror, Store publisher anchors, sources); every other pin is a 40-hex commit"
|
|
27
28
|
},
|
|
28
29
|
"imageNamespaceSegment": "dev",
|
|
29
30
|
"evidenceTrust": {
|