@xemahq/xema 0.52.0 → 0.54.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/README.md +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +66 -0
- package/dist/commands/auth.js.map +1 -0
- 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 +1013 -870
- package/dist/edition/verify-image-evidence.bundle.provenance.json +501 -495
- package/dist/runtime/edition/verb-map.d.ts +2 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- package/edition/README.md +54 -2
- package/edition/adapter-bundled-versions.mjs +2 -1
- package/edition/authorization-base.mjs +9 -8
- package/edition/biome-content.mjs +23 -29
- package/edition/candidate-edition.mjs +692 -0
- package/edition/checkout-indexed-source-repositories.mjs +6 -8
- package/edition/collected-refusals.mjs +59 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +18 -8
- package/edition/customer-edition-verbs.mjs +30 -8
- package/edition/develop-chart-set.mjs +7 -6
- package/edition/develop-edition-invariants.mjs +2 -1
- package/edition/develop-edition-resolve.mjs +109 -31
- package/edition/develop-edition.mjs +49 -8
- package/edition/develop-engine-image.mjs +95 -0
- package/edition/develop-image-probe.mjs +2 -1
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-snapshot-inputs.mjs +4 -3
- package/edition/develop-verify-image-record.mjs +23 -4
- package/edition/edition-artifact.mjs +17 -7
- package/edition/edition-protocol.mjs +8 -2
- package/edition/evidence-input-mode.mjs +4 -22
- package/edition/images-from-record.mjs +362 -0
- package/edition/lib.mjs +1 -19
- package/edition/locked-bundle-signer.mjs +15 -3
- package/edition/operation-grant-admission.mjs +51 -9
- package/edition/operation-requirements.mjs +11 -0
- package/edition/platform-service-class.mjs +4 -2
- package/edition/resolve-distribution.mjs +95 -82
- package/edition/resolve-images.mjs +12 -5
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/workspace-sections.mjs +23 -3
- package/package.json +7 -3
|
@@ -11,6 +11,7 @@ import { readEditionSourcePins } from './edition-source-pins.mjs';
|
|
|
11
11
|
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, configuredWorkspace, editionsRoot, isEntryModule, platformServiceIndexPath, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
|
|
12
12
|
import { holdSource, withSourceGitEnv } from './git-source-auth.mjs';
|
|
13
13
|
import { applyPinSource, snapshotPinSource } from './pin-source.mjs';
|
|
14
|
+
import { shallowCheckout, shallowFetch } from './shallow-checkout.mjs';
|
|
14
15
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
15
16
|
|
|
16
17
|
const REPO_PATH = /^(repos|submodules)\/([a-z0-9][a-z0-9-]*)$/;
|
|
@@ -188,7 +189,7 @@ export function fetchCrossRepoVendoredCommits({ workspaceRoot, plan, index, work
|
|
|
188
189
|
if (!commits) {continue;}
|
|
189
190
|
withSourceGitEnv({ name: entry.repo, url: entry.repositoryUrl }, { workspace, env }, (gitEnv) => {
|
|
190
191
|
for (const commit of commits) {
|
|
191
|
-
runGit
|
|
192
|
+
shallowFetch({ runGit, gitEnv, target: resolve(workspaceRoot, entry.repo), commit });
|
|
192
193
|
console.log(`[source-checkout] ${entry.repo} also holds cross-repo vendored commit ${commit}`);
|
|
193
194
|
}
|
|
194
195
|
});
|
|
@@ -210,10 +211,7 @@ export function checkoutIndexedSources({ workspaceRoot, index, platformServiceIn
|
|
|
210
211
|
}
|
|
211
212
|
withSourceGitEnv({ name: entry.repo, url: entry.repositoryUrl }, { workspace, env }, (gitEnv) => {
|
|
212
213
|
mkdirSync(dirname(target), { recursive: true });
|
|
213
|
-
runGit
|
|
214
|
-
runGit(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], gitEnv);
|
|
215
|
-
runGit(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], gitEnv);
|
|
216
|
-
runGit(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], gitEnv);
|
|
214
|
+
shallowCheckout({ runGit, gitEnv, target, url: entry.repositoryUrl, commit: entry.sourceCommit });
|
|
217
215
|
const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
|
|
218
216
|
env: gitEnv,
|
|
219
217
|
encoding: 'utf8',
|
|
@@ -223,11 +221,11 @@ export function checkoutIndexedSources({ workspaceRoot, index, platformServiceIn
|
|
|
223
221
|
}
|
|
224
222
|
if (sourceAxisCommits.has(entry.repo)) {
|
|
225
223
|
const commit = sourceAxisCommits.get(entry.repo);
|
|
226
|
-
|
|
224
|
+
shallowFetch({ runGit, gitEnv, target, commit });
|
|
227
225
|
console.log(`[source-checkout] ${entry.repo} also holds source-axis commit ${commit}`);
|
|
228
226
|
}
|
|
229
227
|
for (const commit of extraCommits.get(entry.repo) ?? []) {
|
|
230
|
-
|
|
228
|
+
shallowFetch({ runGit, gitEnv, target, commit });
|
|
231
229
|
console.log(`[source-checkout] ${entry.repo} also holds platform-service commit ${commit}`);
|
|
232
230
|
}
|
|
233
231
|
// A repository an edition holds: its main history (containment in origin/main is checked from it,
|
|
@@ -235,7 +233,7 @@ export function checkoutIndexedSources({ workspaceRoot, index, platformServiceIn
|
|
|
235
233
|
if (heldCommits.has(entry.repo)) {
|
|
236
234
|
runGit(['-C', target, 'fetch', '--quiet', 'origin', '+refs/heads/main:refs/remotes/origin/main'], gitEnv);
|
|
237
235
|
for (const commit of heldCommits.get(entry.repo)) {
|
|
238
|
-
|
|
236
|
+
shallowFetch({ runGit, gitEnv, target, commit });
|
|
239
237
|
console.log(`[source-checkout] ${entry.repo} also holds edition-held commit ${commit}`);
|
|
240
238
|
}
|
|
241
239
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The one rule for a check that runs every independent assertion before it refuses.
|
|
2
|
+
//
|
|
3
|
+
// A REFUSAL is the answer "no" to the input: a lock the data does not admit, a commit that is not pinned, an image that is
|
|
4
|
+
// not built. It is a `RefusalError` and is collected so one run names them all. Anything else is a DEFECT (a TypeError, a
|
|
5
|
+
// missing file, a registry that answered 5xx, an unnamed Error): it is never collected, never reported as a refusal, and
|
|
6
|
+
// propagates unchanged with its stack.
|
|
7
|
+
//
|
|
8
|
+
// Nothing refused returns; exactly one refusal is rethrown unchanged (its type and message are the contract of whoever
|
|
9
|
+
// catches it); several become ONE error of the caller's class that joins their messages and keeps them as `.errors`.
|
|
10
|
+
|
|
11
|
+
/** An error that is a REFUSAL of the input. Extend it for a named refusal; throw it directly for an unnamed one. */
|
|
12
|
+
export class RefusalError extends Error {}
|
|
13
|
+
|
|
14
|
+
/** The kernel's own bundle-signer refusals, named as the kernel names them (their classes load lazily from the built kernel). */
|
|
15
|
+
export const KernelRefusalName = Object.freeze({
|
|
16
|
+
UnknownPolicy: 'LockedBundleSignerUnknownPolicyError',
|
|
17
|
+
AmbiguousPolicy: 'LockedBundleSignerAmbiguousPolicyError',
|
|
18
|
+
Mismatch: 'LockedBundleSignerMismatchError',
|
|
19
|
+
});
|
|
20
|
+
const KERNEL_REFUSAL_NAMES = new Set(Object.values(KernelRefusalName));
|
|
21
|
+
|
|
22
|
+
/** Whether a thrown value is a refusal. Everything else (a TypeError, a failed system call, an unnamed Error) is a defect. */
|
|
23
|
+
export const isRefusal = (error) => error instanceof RefusalError || (error instanceof Error && KERNEL_REFUSAL_NAMES.has(error.name));
|
|
24
|
+
|
|
25
|
+
/** Run `fn`; a refusal is pushed on `errors` and `undefined` returned, anything else is rethrown unchanged. */
|
|
26
|
+
export function captureRefusal(errors, fn, refusal) {
|
|
27
|
+
try {
|
|
28
|
+
return fn();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!refusal(error)) {throw error;}
|
|
31
|
+
errors.push(error);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn` on every item, keeping each refusal, then refuse once all were seen: one refusal is rethrown unchanged, several
|
|
38
|
+
* are one `CollectedError(errors)`. A defect stops the run at once and is rethrown as it was thrown.
|
|
39
|
+
*
|
|
40
|
+
* @param {Iterable<T>} items
|
|
41
|
+
* @param {(item: T) => void} fn
|
|
42
|
+
* @param {(error: unknown) => boolean} refusal which thrown values are refusals (`isRefusal`)
|
|
43
|
+
* @param {new (errors: readonly Error[]) => Error} CollectedError the class that carries several
|
|
44
|
+
*/
|
|
45
|
+
export function collectRefusals(items, fn, refusal, CollectedError) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
for (const item of items) {captureRefusal(errors, () => fn(item), refusal);}
|
|
48
|
+
throwCollectedRefusals(errors, CollectedError);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Nothing collected returns, one is rethrown unchanged, several become one `CollectedError`. */
|
|
52
|
+
export function throwCollectedRefusals(errors, CollectedError) {
|
|
53
|
+
if (errors.length === 0) {return;}
|
|
54
|
+
if (errors.length === 1) {throw errors[0];}
|
|
55
|
+
throw new CollectedError(errors);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The message of an error that stands for several: each refusal's message, separated by a blank line. */
|
|
59
|
+
export const joinRefusalMessages = (errors) => errors.map((error) => error.message).join('\n\n');
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `xema edition contract-authority`: the full kernel contract authority of the `@xemahq/contracts` this CLI
|
|
3
|
+
// ships, as canonical JSON on stdout — the `--contract-authority <file>` a third-party publisher's public-input
|
|
4
|
+
// run passes (Third-party sources, README).
|
|
5
|
+
//
|
|
6
|
+
// usage: xema edition contract-authority
|
|
7
|
+
// stdout: the KernelContractAuthority object, canonical JSON, one line.
|
|
8
|
+
//
|
|
9
|
+
// The kernel's release derives the object at publish time, `sourceCommit` = the commit as the kernel release states it (carried, never proven here: only contentDigest and
|
|
10
|
+
// sourcePaths are proven against the package), and ships
|
|
11
|
+
// it as `dist/edition-protocol/contract-authority.json` (exported by name). This verb resolves that export from
|
|
12
|
+
// this CLI's own resolution, parses it with the kernel's KernelContractAuthoritySchema, refuses it unless its
|
|
13
|
+
// contentDigest is the bundled kernel's edition protocol digest, and then proves the bundled package IS that
|
|
14
|
+
// authority (assertKernelPackageIsCatalogueAuthority). It fails closed: a kernel with no file (a dev build, or a
|
|
15
|
+
// release older than the file) is refused by name, with no fallback to any derived or remembered authority.
|
|
16
|
+
|
|
17
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { sep } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { KernelContractAuthoritySchema } from '@xemahq/contracts/distribution';
|
|
22
|
+
|
|
23
|
+
import { canonicalJson } from './canonical-evidence-json.mjs';
|
|
24
|
+
import { readKernelEditionProtocol } from './edition-protocol.mjs';
|
|
25
|
+
import { configureEditionWorkspaceFromArgv, isEntryModule, stateKernelPackageDir } from './edition-workspace.mjs';
|
|
26
|
+
import { KERNEL_PACKAGE_NAME, assertKernelPackageIsCatalogueAuthority, shippedKernelPackageDir } from './kernel-package.mjs';
|
|
27
|
+
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
28
|
+
|
|
29
|
+
export const CONTRACT_AUTHORITY_EXPORT = '@xemahq/contracts/edition-protocol/contract-authority.json';
|
|
30
|
+
export const CONTRACT_AUTHORITY_USAGE = 'usage: xema edition contract-authority';
|
|
31
|
+
|
|
32
|
+
export class BundledContractAuthorityError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'BundledContractAuthorityError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const ownRequire = createRequire(import.meta.url);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The ONE refusal of a kernel that carries no contract-authority.json, for the verb and for every edition test
|
|
43
|
+
* fixture that reads the authority through this module: the file is emitted only at release, so a dev build (an
|
|
44
|
+
* aggregator-linked kernel) never carries one, by design.
|
|
45
|
+
*/
|
|
46
|
+
function missingContractAuthority(kernelDir, detail) {
|
|
47
|
+
return new BundledContractAuthorityError(
|
|
48
|
+
`the ${KERNEL_PACKAGE_NAME} at ${kernelDir} carries no edition-protocol/contract-authority.json (${detail}): the file is emitted ` +
|
|
49
|
+
`only at release, so a dev build of the kernel never has one. The verb, and the edition evidence tests, need a published ` +
|
|
50
|
+
`${KERNEL_PACKAGE_NAME} (install a released @xemahq/xema, or run the tests against the published kernel the CLI's lockfile installs)`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The bundled kernel's contract-authority.json, resolved by its package export from this CLI's own resolution. */
|
|
55
|
+
export function resolveBundledContractAuthorityFile({ resolve = (specifier) => ownRequire.resolve(specifier), kernelDir = shippedKernelPackageDir() } = {}) {
|
|
56
|
+
try {
|
|
57
|
+
return resolve(CONTRACT_AUTHORITY_EXPORT);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {throw error;}
|
|
60
|
+
throw missingContractAuthority(kernelDir, error.code);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read, parse and prove the contract authority at `file` against the kernel package at `kernelDir`: the file must
|
|
66
|
+
* be inside that package, parse as KernelContractAuthoritySchema, carry the package's edition protocol digest, and
|
|
67
|
+
* the package must be that authority. Returns the parsed authority.
|
|
68
|
+
*/
|
|
69
|
+
export async function readBundledContractAuthority({
|
|
70
|
+
kernelDir = shippedKernelPackageDir(),
|
|
71
|
+
file = resolveBundledContractAuthorityFile({ kernelDir }),
|
|
72
|
+
loadSchemas = loadKernelDistributionSchemas,
|
|
73
|
+
} = {}) {
|
|
74
|
+
let real;
|
|
75
|
+
try {
|
|
76
|
+
real = realpathSync(file);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error?.code === 'ENOENT') {throw missingContractAuthority(kernelDir, `${file} does not exist`);}
|
|
79
|
+
throw new BundledContractAuthorityError(`${file} cannot be read: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
if (!real.startsWith(`${realpathSync(kernelDir)}${sep}`)) {
|
|
82
|
+
throw new BundledContractAuthorityError(`${real} is not inside the kernel package this CLI ships (${kernelDir})`);
|
|
83
|
+
}
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = JSON.parse(readFileSync(real, 'utf8'));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new BundledContractAuthorityError(`${real} is not JSON: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
const parsed = KernelContractAuthoritySchema.safeParse(raw);
|
|
91
|
+
if (!parsed.success) {
|
|
92
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`).join('; ');
|
|
93
|
+
throw new BundledContractAuthorityError(`${real} is not a kernel contract authority: ${issues}`);
|
|
94
|
+
}
|
|
95
|
+
const authority = parsed.data;
|
|
96
|
+
const bundledDigest = readKernelEditionProtocol(kernelDir).contentDigest;
|
|
97
|
+
if (authority.contentDigest !== bundledDigest) {
|
|
98
|
+
throw new BundledContractAuthorityError(
|
|
99
|
+
`${real} states contentDigest ${authority.contentDigest}, but the bundled kernel's edition protocol digest is ${bundledDigest}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
await assertKernelPackageIsCatalogueAuthority({ catalogue: { contractAuthority: authority }, kernelPackageDir: kernelDir, loadSchemas });
|
|
103
|
+
return authority;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main() {
|
|
107
|
+
const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [], { refuseOthers: true });
|
|
108
|
+
if (rest.length > 0) {throw new Error(CONTRACT_AUTHORITY_USAGE);}
|
|
109
|
+
const kernelDir = shippedKernelPackageDir();
|
|
110
|
+
stateKernelPackageDir(kernelDir);
|
|
111
|
+
const authority = await readBundledContractAuthority({ kernelDir });
|
|
112
|
+
process.stdout.write(`${canonicalJson(authority)}\n`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isEntryModule(import.meta.url)) {
|
|
116
|
+
try {
|
|
117
|
+
await main();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[contract-authority] ${error.message}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -14,17 +14,17 @@ function requireText(value, what) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
+
* The flags that pin a signer's exact identity, issuer, workflow repository and ref. They carry no workflow commit: use
|
|
18
|
+
* {@link certificateConstraintArgs} unless the signing workflow's own commit is, by design, not known to the verifier (a
|
|
19
|
+
* `workflow_run` follower runs the default branch's copy), and then bind the built commit by a signed annotation.
|
|
20
|
+
*
|
|
17
21
|
* @param {{ certificateIdentity: string, oidcIssuer: string, githubWorkflow: { repository: string, ref: string } }} signer
|
|
18
|
-
* @
|
|
19
|
-
* @returns {string[]} the cosign flags that pin identity, issuer, workflow repository, ref and sha
|
|
22
|
+
* @returns {string[]}
|
|
20
23
|
*/
|
|
21
|
-
export function
|
|
24
|
+
export function certificateIdentityArgs(signer) {
|
|
22
25
|
if (signer === null || typeof signer !== 'object') {
|
|
23
26
|
throw new Error('certificate constraints: the signer must be an object');
|
|
24
27
|
}
|
|
25
|
-
if (typeof workflowSha !== 'string' || !WORKFLOW_SHA.test(workflowSha)) {
|
|
26
|
-
throw new Error(`certificate constraints: the workflow sha must be 40 lowercase hex characters; got ${JSON.stringify(workflowSha)}`);
|
|
27
|
-
}
|
|
28
28
|
return [
|
|
29
29
|
'--certificate-identity',
|
|
30
30
|
requireText(signer.certificateIdentity, 'certificateIdentity'),
|
|
@@ -34,7 +34,17 @@ export function certificateConstraintArgs(signer, workflowSha) {
|
|
|
34
34
|
requireText(signer.githubWorkflow?.repository, 'githubWorkflow.repository'),
|
|
35
35
|
'--certificate-github-workflow-ref',
|
|
36
36
|
requireText(signer.githubWorkflow?.ref, 'githubWorkflow.ref'),
|
|
37
|
-
'--certificate-github-workflow-sha',
|
|
38
|
-
workflowSha,
|
|
39
37
|
];
|
|
40
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{ certificateIdentity: string, oidcIssuer: string, githubWorkflow: { repository: string, ref: string } }} signer
|
|
42
|
+
* @param {string} workflowSha the 40-hex commit the signing workflow ran at
|
|
43
|
+
* @returns {string[]} the cosign flags that pin identity, issuer, workflow repository, ref and sha
|
|
44
|
+
*/
|
|
45
|
+
export function certificateConstraintArgs(signer, workflowSha) {
|
|
46
|
+
if (typeof workflowSha !== 'string' || !WORKFLOW_SHA.test(workflowSha)) {
|
|
47
|
+
throw new Error(`certificate constraints: the workflow sha must be 40 lowercase hex characters; got ${JSON.stringify(workflowSha)}`);
|
|
48
|
+
}
|
|
49
|
+
return [...certificateIdentityArgs(signer), '--certificate-github-workflow-sha', workflowSha];
|
|
50
|
+
}
|
|
@@ -178,21 +178,35 @@ export function customerRootsOf(command) {
|
|
|
178
178
|
// customer form: the flags only it takes, the further flags it requires, and the vendor flags it refuses.
|
|
179
179
|
const CREDENTIAL_FLAGS = Object.freeze([CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
|
|
180
180
|
const REPEATABLE = Object.freeze([SOURCE_IMAGE_FLAG]);
|
|
181
|
+
// A vendor develop resolve finds and proves its engine itself (develop-engine-image.mjs); a customer resolve is handed one.
|
|
182
|
+
const ENGINE_IMAGE_FLAG = '--engine-image';
|
|
183
|
+
const ENGINE_REPOSITORY_FLAG = '--engine-repository';
|
|
184
|
+
const ENGINE_SIGNER_FLAG = '--engine-signer';
|
|
185
|
+
export const IMAGES_FROM_FLAG = '--images-from';
|
|
181
186
|
|
|
182
187
|
const COMMANDS = Object.freeze({
|
|
183
188
|
resolve: {
|
|
184
189
|
positional: true,
|
|
185
|
-
|
|
190
|
+
// --images-from: the everyday job's image build record (images-from-record.mjs) instead of the registry probe; vendor only.
|
|
191
|
+
values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG],
|
|
186
192
|
switches: ['--local'],
|
|
187
|
-
required: ['--pins', '--out', '--registry',
|
|
188
|
-
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [], cosign: true },
|
|
193
|
+
required: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
|
|
194
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG], cosign: true },
|
|
195
|
+
// A vendor resolve finds its engine itself; the old flag is refused by name, never read as a customer flag.
|
|
196
|
+
removedFromVendor: { [ENGINE_IMAGE_FLAG]: `use ${ENGINE_REPOSITORY_FLAG} and ${ENGINE_SIGNER_FLAG}` },
|
|
197
|
+
},
|
|
198
|
+
engine: {
|
|
199
|
+
positional: true,
|
|
200
|
+
values: ['--pins', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
|
|
201
|
+
switches: [],
|
|
202
|
+
required: ['--pins', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
|
|
189
203
|
},
|
|
190
204
|
target: {
|
|
191
205
|
positional: true,
|
|
192
|
-
values: ['--pins', '--registry', '--engine-image'],
|
|
206
|
+
values: ['--pins', '--registry', '--engine-image', IMAGES_FROM_FLAG],
|
|
193
207
|
switches: [],
|
|
194
208
|
required: ['--pins', '--registry', '--engine-image'],
|
|
195
|
-
customer: { values: [DEFINITION_FLAG, SOURCE_IMAGE_FLAG], required: ['--pins', '--registry', '--engine-image'], refused: [] },
|
|
209
|
+
customer: { values: [DEFINITION_FLAG, SOURCE_IMAGE_FLAG], required: ['--pins', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG] },
|
|
196
210
|
},
|
|
197
211
|
'freeze-graph': {
|
|
198
212
|
positional: false,
|
|
@@ -256,7 +270,8 @@ export function parseEditionVerbArgs(channel, argv) {
|
|
|
256
270
|
if (spec === undefined) {
|
|
257
271
|
throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(commands).join(' | ')}`);
|
|
258
272
|
}
|
|
259
|
-
|
|
273
|
+
// The flags only the customer form takes: a flag the vendor form takes too (`--cosign` of a develop resolve) does not make it one.
|
|
274
|
+
const customerValues = (spec.customer?.values ?? []).filter((flag) => !spec.values.includes(flag));
|
|
260
275
|
const options = {};
|
|
261
276
|
const positional = [];
|
|
262
277
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -283,13 +298,20 @@ export function parseEditionVerbArgs(channel, argv) {
|
|
|
283
298
|
}
|
|
284
299
|
}
|
|
285
300
|
const customer = spec.customer !== undefined && customerValues.some((flag) => options[flag] !== undefined);
|
|
286
|
-
// `--cosign` of a customer form verifies a release catalogue; a vendor verb (develop `coverage`) takes its own `--cosign`.
|
|
287
|
-
if (spec.customer?.cosign === true && options[COSIGN_FLAG] !== undefined && channel !== EditionChannel.Release) {
|
|
301
|
+
// `--cosign` of a customer form verifies a release catalogue; a vendor verb (develop `coverage`, `resolve`, `engine`) takes its own `--cosign`.
|
|
302
|
+
if (customer && spec.customer?.cosign === true && options[COSIGN_FLAG] !== undefined && channel !== EditionChannel.Release) {
|
|
288
303
|
throw new Error(`${COSIGN_FLAG} verifies a release catalogue's evidence; a ${channel} ${command} takes none`);
|
|
289
304
|
}
|
|
290
305
|
if (channel === EditionChannel.Release && options[DEFINITION_FLAG] === undefined) {
|
|
291
306
|
throw new Error(`a ${channel} ${command} requires ${DEFINITION_FLAG}`);
|
|
292
307
|
}
|
|
308
|
+
// A removed vendor flag with no other sign of a customer form (no --definition, no customer-only flag) is a stale vendor call.
|
|
309
|
+
const removedFlags = Object.keys(spec.removedFromVendor ?? {});
|
|
310
|
+
if (!customerValues.some((flag) => !removedFlags.includes(flag) && options[flag] !== undefined)) {
|
|
311
|
+
for (const flag of removedFlags) {
|
|
312
|
+
if (options[flag] !== undefined) {throw new Error(`${flag} was removed from a vendor ${command}: ${spec.removedFromVendor[flag]}`);}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
293
315
|
if (customer) {
|
|
294
316
|
if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
|
|
295
317
|
for (const refused of spec.customer.refused) {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { basename } from 'node:path';
|
|
31
31
|
|
|
32
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
32
33
|
import { readDevelopChartRoster } from './develop-chart-roster.mjs';
|
|
33
34
|
import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
|
|
34
35
|
import { deployRoot } from './edition-workspace.mjs';
|
|
@@ -53,7 +54,7 @@ function deployPublisher() {
|
|
|
53
54
|
const deployRepo = basename(deployPinKey());
|
|
54
55
|
const publishers = chartSetPublishers().filter((publisher) => basename(publisher.repo) === deployRepo);
|
|
55
56
|
if (publishers.length !== 1) {
|
|
56
|
-
throw new
|
|
57
|
+
throw new RefusalError(
|
|
57
58
|
`${publishers.length} configured chart publishers have repository ${deployRepo} (the deploy pin role); exactly one must, ` +
|
|
58
59
|
'since a develop chart set is that publisher\'s set at the deploy commit',
|
|
59
60
|
);
|
|
@@ -72,13 +73,13 @@ function committedInfraPublisher() {
|
|
|
72
73
|
const path = chartSetLockPath();
|
|
73
74
|
const committed = readLock(path);
|
|
74
75
|
if (committed === null) {
|
|
75
|
-
throw new
|
|
76
|
+
throw new RefusalError(`${path} is absent or unreadable; a develop edition carries the release infra chart set from it`);
|
|
76
77
|
}
|
|
77
78
|
assertRosteredChartSetLock(committed, path);
|
|
78
79
|
const indexes = infraPublisherIndexes();
|
|
79
80
|
const found = committed.publishers.filter((publisher) => indexes.includes(publisher.index));
|
|
80
81
|
if (indexes.length !== 1 || found.length !== 1) {
|
|
81
|
-
throw new
|
|
82
|
+
throw new RefusalError(
|
|
82
83
|
`${path} carries ${found.length} infra publisher entries (${indexes.length} infra publishers configured); exactly one is required`,
|
|
83
84
|
);
|
|
84
85
|
}
|
|
@@ -91,7 +92,7 @@ async function published(read, what, { publisher, xemaDeploySha }) {
|
|
|
91
92
|
return await read();
|
|
92
93
|
} catch (error) {
|
|
93
94
|
if (error?.status !== NOT_FOUND) {throw error;}
|
|
94
|
-
throw new
|
|
95
|
+
throw new RefusalError(`${what} is not published yet; ${publisher.repo} publish-charts for ${xemaDeploySha} not green`, { cause: error });
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -102,7 +103,7 @@ function assertIndexIsRoster(indexRoster, members, label) {
|
|
|
102
103
|
const missing = expected.filter((line) => !actual.includes(line));
|
|
103
104
|
const extra = actual.filter((line) => !expected.includes(line));
|
|
104
105
|
if (missing.length > 0 || extra.length > 0) {
|
|
105
|
-
throw new
|
|
106
|
+
throw new RefusalError(
|
|
106
107
|
`${label} does not carry the deploy checkout's develop roster: ` +
|
|
107
108
|
`missing [${missing.join(', ')}], unexpected [${extra.join(', ')}]`,
|
|
108
109
|
);
|
|
@@ -116,7 +117,7 @@ function assertIndexIsRoster(indexRoster, members, label) {
|
|
|
116
117
|
*/
|
|
117
118
|
export async function resolveDevelopChartSet({ xemaDeploySha, deployDir = deployRoot(), readRoster = readDevelopChartRoster, client }) {
|
|
118
119
|
if (!COMMIT_PATTERN.test(xemaDeploySha ?? '')) {
|
|
119
|
-
throw new
|
|
120
|
+
throw new RefusalError(`the deploy checkout SHA is not a 40-hex commit: ${JSON.stringify(xemaDeploySha)}`);
|
|
120
121
|
}
|
|
121
122
|
const publisher = deployPublisher();
|
|
122
123
|
const infraPublisher = committedInfraPublisher();
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// admission of this edition for a biome it does not ship is a stale permission
|
|
23
23
|
// and is refused; a declaration without a substantive consequence is refused.
|
|
24
24
|
|
|
25
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
25
26
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
26
27
|
import { buildCapabilityDomainOwners, serializeJson } from './lib.mjs';
|
|
27
28
|
|
|
@@ -40,7 +41,7 @@ export const DevelopEditionInvariantReason = Object.freeze({
|
|
|
40
41
|
const MIN_CONSEQUENCE_LENGTH = 40;
|
|
41
42
|
|
|
42
43
|
/** A develop candidate broke a plane invariant. `violations` carries every one, each with its reason. */
|
|
43
|
-
export class DevelopEditionInvariantError extends
|
|
44
|
+
export class DevelopEditionInvariantError extends RefusalError {
|
|
44
45
|
/** @param {string} id @param {{ reason: string, detail: string }[]} violations */
|
|
45
46
|
constructor(id, violations) {
|
|
46
47
|
super(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// develop-edition-resolve — resolve and seal one DEVELOP edition from a snapshot.
|
|
2
2
|
//
|
|
3
|
-
// The whole pipeline, in the order the release runs it, over the snapshot's
|
|
3
|
+
// The whole pipeline, in the order the release runs it (every check reports its refusal; see develop-resolve-report.mjs), over the snapshot's
|
|
4
4
|
// injected inputs: distribution lock -> plane invariants (reservation table,
|
|
5
5
|
// confinement; develop-edition-invariants.mjs) -> authorization base (composed, never
|
|
6
6
|
// written) -> images lock -> develop seal, plus the chart-set lock: the frozen
|
|
@@ -12,15 +12,21 @@ import { biomeSourceTreeIds, readBiomeSourceFacts, sealLock } from './biome-cont
|
|
|
12
12
|
import { buildCatalogue } from './catalogue.mjs';
|
|
13
13
|
import { resolveDevelopChartSet } from './develop-chart-set.mjs';
|
|
14
14
|
import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
|
|
15
|
+
import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
|
|
15
16
|
import { createDevelopImageProbe } from './develop-image-probe.mjs';
|
|
17
|
+
import { BLOCKED, DevelopResolveCheck, RESOLVE_CHECK_DEPENDENCIES, createResolveReport } from './develop-resolve-report.mjs';
|
|
16
18
|
import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
|
|
17
19
|
import { EditionChannel } from './edition-constants.mjs';
|
|
18
20
|
import { sealImages } from './edition-image-seal.mjs';
|
|
21
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
19
22
|
import { dataRoot, kernelPackageDir } from './edition-workspace.mjs';
|
|
23
|
+
import { resolveImagesFromRecord, sealImagesFromRecord } from './images-from-record.mjs';
|
|
20
24
|
import { kernelPackageOfAuthority } from './kernel-package.mjs';
|
|
21
25
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
26
|
+
import { assertLockBundleSigners, isBundleSignerRefusal } from './locked-bundle-signer.mjs';
|
|
27
|
+
import { admitDistributionLock, ungrantedOptionalLinesOfRefusal } from './operation-grant-admission.mjs';
|
|
22
28
|
import { deployPinKey } from './pin-source.mjs';
|
|
23
|
-
import { composeAuthorizationBase,
|
|
29
|
+
import { admitResolvedLock, composeAuthorizationBase, prepareDistributionWithIndex } from './resolve-distribution.mjs';
|
|
24
30
|
import { externalSourceTrees, resolveImages, sourceTreeLocator } from './resolve-images.mjs';
|
|
25
31
|
import { createPinnedSourceAuthority } from './source-authority.mjs';
|
|
26
32
|
import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
|
|
@@ -32,12 +38,15 @@ import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs'
|
|
|
32
38
|
export const DEVELOP_EDITION_STEPS = Object.freeze({
|
|
33
39
|
assertFrozenCheckouts,
|
|
34
40
|
buildSnapshotInputs,
|
|
35
|
-
|
|
41
|
+
assertKernelEditionProtocol,
|
|
42
|
+
prepareDistributionWithIndex,
|
|
43
|
+
resolveDevelopEngineImage,
|
|
36
44
|
assertDevelopEditionInvariants,
|
|
37
45
|
assertAdapterBundlesMatchHost,
|
|
38
46
|
composeAuthorizationBase,
|
|
39
47
|
resolveImages,
|
|
40
48
|
sealImages,
|
|
49
|
+
sealImagesFromRecord,
|
|
41
50
|
resolveDevelopChartSet,
|
|
42
51
|
});
|
|
43
52
|
|
|
@@ -45,46 +54,117 @@ const stderrProgress = (line) => process.stderr.write(`${line}\n`);
|
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* The first half of every develop resolve: the frozen snapshot's inputs and the
|
|
48
|
-
* distribution lock composed over them. `resolveDevelopEdition`
|
|
49
|
-
*
|
|
50
|
-
* for a subject is the same by construction.
|
|
57
|
+
* distribution lock composed over them, NOT yet admitted. `resolveDevelopEdition`
|
|
58
|
+
* admits it as one of its checks; `resolveDevelopSnapshot` admits it at once. Both
|
|
59
|
+
* start here, so the content hash each computes for a subject is the same by construction.
|
|
51
60
|
*/
|
|
52
|
-
|
|
61
|
+
async function prepareDevelopSnapshot({ id, pins, steps, phase }) {
|
|
53
62
|
const schemas = await loadKernelDistributionSchemas();
|
|
54
63
|
steps.assertFrozenCheckouts(pins);
|
|
55
64
|
const inputs = await steps.buildSnapshotInputs({ id, pins, schemas });
|
|
56
65
|
phase('snapshot inputs built');
|
|
57
|
-
const
|
|
58
|
-
|
|
66
|
+
const prepared = await steps.prepareDistributionWithIndex(id, { inputs });
|
|
67
|
+
// The schemas the lock was composed with are the ones that admit it.
|
|
68
|
+
return { inputs, ...prepared };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The read-only `coverage` verb's snapshot: prepared as the resolve prepares it, then admitted (a refusal throws). */
|
|
72
|
+
export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION_STEPS, phase = () => {} }) {
|
|
73
|
+
const snapshot = await prepareDevelopSnapshot({ id, pins, steps, phase });
|
|
74
|
+
admitResolvedLock(snapshot.schemas, snapshot.lock);
|
|
75
|
+
return snapshot;
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} request
|
|
80
|
+
* @param {object} [request.imagesFrom] a parsed image build record (images-from-record.mjs): the digests are the record's,
|
|
81
|
+
* with no probe and no rehash, and `verifySatellites` (createSatelliteVerifier) admits its satellite entries
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites }) {
|
|
84
|
+
if ((imagesFrom === undefined) !== (verifySatellites === undefined)) {
|
|
85
|
+
throw new Error('resolveDevelopEdition takes an image build record and its satellite verifier together, or neither');
|
|
86
|
+
}
|
|
62
87
|
const startedAt = Date.now();
|
|
63
88
|
// One line per phase, with the time since the resolve began, so the step is never silent for long.
|
|
64
89
|
const phase = (name) => progress(`[develop-edition] ${name} (+${Date.now() - startedAt}ms)`);
|
|
65
|
-
|
|
90
|
+
// Every check runs unless a check it needs was refused; all refusals are reported together, and nothing is sealed or written then.
|
|
91
|
+
const report = createResolveReport();
|
|
92
|
+
const checked = (check, fn, options) => report.run(check, RESOLVE_CHECK_DEPENDENCIES[check], fn, options);
|
|
93
|
+
const chartSetLock = await checked(DevelopResolveCheck.ChartSet, async () => {
|
|
94
|
+
const chartSet = await steps.resolveDevelopChartSet({ xemaDeploySha: pins[deployPinKey()] });
|
|
95
|
+
phase('chart set resolved');
|
|
96
|
+
return chartSet;
|
|
97
|
+
});
|
|
98
|
+
const engineImage = await checked(DevelopResolveCheck.EngineImage, async () => {
|
|
99
|
+
const image = await steps.resolveDevelopEngineImage({ pins, engine, cosignBinary, env });
|
|
100
|
+
phase('engine image resolved');
|
|
101
|
+
return image;
|
|
102
|
+
});
|
|
103
|
+
await checked(DevelopResolveCheck.EditionProtocol, () => steps.assertKernelEditionProtocol(kernelPackageDir()));
|
|
104
|
+
const snapshot = await checked(DevelopResolveCheck.DistributionLock, () => prepareDevelopSnapshot({ id, pins, steps, phase }), {
|
|
105
|
+
classify: (error) => (isBundleSignerRefusal(error) ? DevelopResolveCheck.BundleSigner : DevelopResolveCheck.DistributionLock),
|
|
106
|
+
});
|
|
107
|
+
const lockProduced = snapshot !== BLOCKED;
|
|
108
|
+
const { schemas, inputs, lock, index } = lockProduced ? snapshot : {};
|
|
109
|
+
if (lockProduced) {
|
|
110
|
+
await checked(DevelopResolveCheck.BundleSigner, () => assertLockBundleSigners({ lock, schemas }));
|
|
111
|
+
}
|
|
66
112
|
// The plane invariants, against the graph this candidate froze (develop-edition-invariants.mjs).
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
await checked(DevelopResolveCheck.PlaneInvariants, () => {
|
|
114
|
+
const graded = steps.assertDevelopEditionInvariants({ id, lock, biomeIndex: inputs.biomeIndex, schemas });
|
|
115
|
+
phase(`reservation table asserted (${graded.reservedDomains} domains); confinement asserted (${graded.confinedBiomes} declaration(s))`);
|
|
116
|
+
});
|
|
117
|
+
await checked(DevelopResolveCheck.OperationAdmission, () => {
|
|
118
|
+
try {
|
|
119
|
+
admitDistributionLock(schemas, lock, { report: (line) => report.notice(line) });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
ungrantedOptionalLinesOfRefusal(error).forEach((line) => report.notice(line));
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
phase('operation requirements admitted');
|
|
125
|
+
});
|
|
126
|
+
await checked(DevelopResolveCheck.AdapterBundles, () => steps.assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest }));
|
|
127
|
+
const authorizationLock = await checked(DevelopResolveCheck.AuthorizationBase, async () => {
|
|
128
|
+
const composed = await steps.composeAuthorizationBase(id, lock, index);
|
|
129
|
+
phase('distribution lock resolved');
|
|
130
|
+
return composed;
|
|
131
|
+
});
|
|
72
132
|
// Every subject's develop image is probed the moment its hash is known, in
|
|
73
133
|
// parallel with the hashing of the rest; an absent image refuses here, naming
|
|
74
134
|
// all of them, before any later phase runs.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
135
|
+
let recorded;
|
|
136
|
+
const imageLock = await checked(DevelopResolveCheck.DevelopImages, async () => {
|
|
137
|
+
if (imagesFrom !== undefined) {
|
|
138
|
+
const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites });
|
|
139
|
+
({ recorded } = fromRecord);
|
|
140
|
+
phase(`image subjects taken from the build record (${fromRecord.imageLock.subjects.length} subjects); satellites verified`);
|
|
141
|
+
return fromRecord.imageLock;
|
|
142
|
+
}
|
|
143
|
+
const probe = createDevelopImageProbe({
|
|
144
|
+
distributionId: id,
|
|
145
|
+
env,
|
|
146
|
+
...(sealOptions.distributionsRoot === undefined ? {} : { distributionsRoot: sealOptions.distributionsRoot }),
|
|
147
|
+
...(sealOptions.fetchImpl === undefined ? {} : { fetchImpl: sealOptions.fetchImpl }),
|
|
148
|
+
...(sealOptions.run === undefined ? {} : { run: sealOptions.run }),
|
|
149
|
+
progress,
|
|
150
|
+
});
|
|
151
|
+
const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe });
|
|
152
|
+
phase(`image content hashes computed (${resolved.imageLock.subjects.length} subjects)`);
|
|
153
|
+
await probe.settle();
|
|
154
|
+
phase('every develop image is present');
|
|
155
|
+
return resolved.imageLock;
|
|
82
156
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
157
|
+
try {
|
|
158
|
+
report.settle();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
progress(error.message);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
if (report.noticeText() !== '') {progress(report.noticeText());}
|
|
164
|
+
const sealArguments = { imageLock, env, progress, thirdPartyPins: loadTrustedThirdPartySourcePins(dataRoot()), ...sealOptions };
|
|
165
|
+
const sealedImageLock = recorded === undefined
|
|
166
|
+
? await steps.sealImages({ channel: EditionChannel.Develop, ...sealArguments })
|
|
167
|
+
: await steps.sealImagesFromRecord({ ...sealArguments, recorded });
|
|
88
168
|
phase('images sealed');
|
|
89
169
|
// The unsealed lock becomes the one v5 grammar only here, once the images it
|
|
90
170
|
// owns are sealed: each biomeContentHash is recomputed from the frozen sources.
|
|
@@ -100,8 +180,6 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
100
180
|
phase(`biome sources read (${sourceFactsById.size} biomes)`);
|
|
101
181
|
const sealedLock = await sealLock({ ...biomeSources, unsealed: lock, sourceFactsById });
|
|
102
182
|
phase('distribution lock sealed');
|
|
103
|
-
const chartSetLock = await steps.resolveDevelopChartSet({ xemaDeploySha: pins[deployPinKey()] });
|
|
104
|
-
phase('chart set resolved');
|
|
105
183
|
const sourceTreeIds = await biomeSourceTreeIds({ ...biomeSources, lock, sourceFactsById });
|
|
106
184
|
const catalogue = buildCatalogue({
|
|
107
185
|
sourceTreeIds,
|
|
@@ -121,5 +199,5 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
121
199
|
leaf: inputs.composed.leaf,
|
|
122
200
|
});
|
|
123
201
|
phase('catalogue built');
|
|
124
|
-
return { distributionLock: sealedLock, imageLock: sealedImageLock, authorizationLock, chartSetLock, catalogue };
|
|
202
|
+
return { distributionLock: sealedLock, imageLock: sealedImageLock, authorizationLock, chartSetLock, catalogue, engineImage };
|
|
125
203
|
}
|