@xemahq/xema 0.27.0 → 0.29.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 +8 -7
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +0 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +231 -50
- package/dist/edition/verify-image-evidence.bundle.provenance.json +55 -43
- 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/dist/runtime/runner/chart-set-lock.d.ts +4 -1
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +39 -4
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/edition/README.md +76 -41
- package/edition/build-biome-index.mjs +1 -1
- package/edition/build-platform-service-index.mjs +1 -1
- package/edition/build-runtime-image-source-index.mjs +64 -20
- package/edition/catalogue-fetch.mjs +36 -9
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +11 -0
- package/edition/chart-set-source-commit.mjs +17 -7
- package/edition/checkout-indexed-source-repositories.mjs +17 -17
- package/edition/customer-flags.mjs +0 -1
- package/edition/develop-customer-edition.mjs +23 -5
- package/edition/develop-edition.mjs +147 -48
- package/edition/develop-freeze.mjs +69 -25
- package/edition/develop-seal.mjs +39 -25
- package/edition/dist-check.mjs +1 -1
- package/edition/edition-artifact.mjs +79 -18
- package/edition/edition-biome-manifests.mjs +2 -3
- package/edition/edition-chart-set.mjs +59 -6
- package/edition/edition-config.mjs +45 -21
- package/edition/edition-constants.mjs +10 -4
- package/edition/edition-contract-parse.mjs +52 -0
- package/edition/edition-contract.json +21 -5
- package/edition/edition-workspace.mjs +23 -9
- package/edition/evidence-checkout.mjs +22 -1
- package/edition/evidence-trust-policy.mjs +142 -78
- package/edition/external-source-walk.mjs +48 -61
- package/edition/git-source-auth.mjs +104 -25
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence.mjs +21 -3
- package/edition/kernel-contract-authority.mjs +23 -1
- package/edition/lib.mjs +16 -24
- package/edition/oras.mjs +1 -1
- package/edition/pin-source.mjs +6 -1
- package/edition/produce-image-delivery-entitlement-evidence.mjs +12 -12
- package/edition/registry-credentials.mjs +11 -1
- package/edition/release-channel-guard.mjs +11 -1
- package/edition/release-edition-artifact.mjs +5 -6
- package/edition/resolve-chart-set.mjs +50 -30
- package/edition/resolve-customer-edition.mjs +63 -7
- package/edition/resolve-distribution.mjs +1 -1
- package/edition/resolve-edition.mjs +12 -11
- package/edition/resolve-images.mjs +36 -15
- package/edition/seal-image-digests.mjs +31 -7
- package/edition/source-closure.mjs +1 -2
- package/edition/trust-policy-arming-verb.mjs +68 -0
- package/edition/verify-image-evidence.mjs +40 -12
- package/package.json +3 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// catalogue-pin — `catalogue.pin.json`, the catalogues a customer edition is resolved against.
|
|
2
|
+
//
|
|
3
|
+
// It sits in the SAME directory as the customer's definition file:
|
|
4
|
+
// { "schemaVersion": 1, "develop": "oci://<repository>@sha256:<64 hex>" | null, "release": ... | null }
|
|
5
|
+
// Exactly these keys, both channel keys present; null states that the edition is not produced on
|
|
6
|
+
// that channel. A reference is parsed by `parseCatalogueRef`, and the repository channel of the
|
|
7
|
+
// reference under a key must be that key. This is the only reader of the file: every customer
|
|
8
|
+
// resolve takes the catalogue reference of its channel from here.
|
|
9
|
+
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import { parseCatalogueRef } from './catalogue-fetch.mjs';
|
|
14
|
+
import { CatalogueError } from './catalogue.mjs';
|
|
15
|
+
import { EditionChannel } from './edition-constants.mjs';
|
|
16
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
17
|
+
|
|
18
|
+
export const CATALOGUE_PIN_FILE = 'catalogue.pin.json';
|
|
19
|
+
export const CATALOGUE_PIN_SCHEMA_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
const SCHEMA_VERSION_KEY = 'schemaVersion';
|
|
22
|
+
|
|
23
|
+
/** The catalogue pin file that sits beside `definitionFile`. */
|
|
24
|
+
export const cataloguePinFile = (definitionFile) => join(dirname(definitionFile), CATALOGUE_PIN_FILE);
|
|
25
|
+
|
|
26
|
+
/** The strict parse of a pin document read from `file`; returns it unchanged. */
|
|
27
|
+
export function parseCataloguePins(document, file) {
|
|
28
|
+
const channels = Object.values(EditionChannel);
|
|
29
|
+
const expected = [SCHEMA_VERSION_KEY, ...channels].sort().join();
|
|
30
|
+
if (document === null || typeof document !== 'object' || Array.isArray(document) || Object.keys(document).sort().join() !== expected) {
|
|
31
|
+
throw new CatalogueError(`${file} must be exactly {${[SCHEMA_VERSION_KEY, ...channels].join(', ')}}, every key present`);
|
|
32
|
+
}
|
|
33
|
+
if (document[SCHEMA_VERSION_KEY] !== CATALOGUE_PIN_SCHEMA_VERSION) {
|
|
34
|
+
throw new CatalogueError(`${file}: ${SCHEMA_VERSION_KEY} must be ${CATALOGUE_PIN_SCHEMA_VERSION}`);
|
|
35
|
+
}
|
|
36
|
+
for (const channel of channels) {
|
|
37
|
+
const ref = document[channel];
|
|
38
|
+
if (ref === null) {continue;}
|
|
39
|
+
if (typeof ref !== 'string') {
|
|
40
|
+
throw new CatalogueError(`${file}: "${channel}" must be an oci://<repository>@sha256:<64 hex> reference or null`);
|
|
41
|
+
}
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = parseCatalogueRef(ref);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new CatalogueError(`${file}: "${channel}": ${error.message}`, { cause: error });
|
|
47
|
+
}
|
|
48
|
+
if (parsed.edition.channel !== channel) {
|
|
49
|
+
throw new CatalogueError(
|
|
50
|
+
`${file}: "${channel}" pins ${ref}, whose repository is a ${parsed.edition.channel} repository; a ${channel} catalogue is pinned in a ${channel} repository`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return document;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The catalogue reference pinned for `channel` beside `definitionFile`; a null pin or a missing file is refused. */
|
|
58
|
+
export function readCataloguePin({ definitionFile, channel }) {
|
|
59
|
+
const file = cataloguePinFile(definitionFile);
|
|
60
|
+
if (!existsSync(file)) {
|
|
61
|
+
throw new CatalogueError(`${file} does not exist: a customer edition pins its catalogues in ${CATALOGUE_PIN_FILE}, beside the definition`);
|
|
62
|
+
}
|
|
63
|
+
const ref = parseCataloguePins(readStrictJsonFile(file), file)[channel];
|
|
64
|
+
if (ref === undefined) {
|
|
65
|
+
throw new CatalogueError(`${file}: ${JSON.stringify(channel)} is not a channel (${Object.values(EditionChannel).join(' | ')})`);
|
|
66
|
+
}
|
|
67
|
+
if (ref === null) {
|
|
68
|
+
throw new CatalogueError(`${file}: no ${channel} catalogue is pinned for this edition`);
|
|
69
|
+
}
|
|
70
|
+
return ref;
|
|
71
|
+
}
|
package/edition/catalogue.mjs
CHANGED
|
@@ -133,3 +133,14 @@ export function writeCatalogue(outDir, catalogue) {
|
|
|
133
133
|
writeFileSync(path, serializeJson(catalogue));
|
|
134
134
|
return path;
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
const KERNEL_COMMIT = /^[0-9a-f]{40}$/;
|
|
138
|
+
|
|
139
|
+
/** The commit the catalogue's contract authority was taken at (40 hex); read without the kernel schemas, which are not checked out yet where this is used. */
|
|
140
|
+
export function contractAuthoritySourceCommit(catalogue) {
|
|
141
|
+
const commit = catalogue.contractAuthority.sourceCommit;
|
|
142
|
+
if (typeof commit !== 'string' || !KERNEL_COMMIT.test(commit)) {
|
|
143
|
+
throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority.sourceCommit is not a 40-hex commit: ${JSON.stringify(commit)}`);
|
|
144
|
+
}
|
|
145
|
+
return commit;
|
|
146
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The publisher stamps its index chart with `xema.dev/chart-set-source-sha`
|
|
5
5
|
// next to `xema.dev/chart-set-charts`. This verb reads that stamp from the index
|
|
6
|
-
// chart at the
|
|
7
|
-
// prints one JSON document. It refuses,
|
|
8
|
-
// absent or is not a 40-hex commit,
|
|
9
|
-
//
|
|
6
|
+
// chart at the digest the committed fleet `chart-set.lock.json` pins (a develop
|
|
7
|
+
// lock has none and is refused), and prints one JSON document. It refuses,
|
|
8
|
+
// printing nothing, when the stamp is absent or is not a 40-hex commit, when the
|
|
9
|
+
// registry does not serve the locked digest, or when the index's roster is not
|
|
10
|
+
// the one the lock records for that publisher: the commit it would name would then be
|
|
10
11
|
// the commit of a different set than the locked one.
|
|
11
12
|
|
|
12
13
|
import { EditionChannel } from './edition-channel.mjs';
|
|
@@ -19,9 +20,10 @@ import {
|
|
|
19
20
|
INDEX_ROSTER_ANNOTATION,
|
|
20
21
|
chartRegistryClient,
|
|
21
22
|
chartSetLockPath,
|
|
22
|
-
|
|
23
|
+
manifestAnnotations,
|
|
23
24
|
parseIndexRoster,
|
|
24
25
|
readLock,
|
|
26
|
+
registryRepository,
|
|
25
27
|
} from './resolve-chart-set.mjs';
|
|
26
28
|
|
|
27
29
|
if (isEntryModule(import.meta.url)) {
|
|
@@ -55,9 +57,17 @@ function rosterDifference(lockCharts, indexRoster) {
|
|
|
55
57
|
*/
|
|
56
58
|
export async function chartSetSourceCommits({ client, lock }) {
|
|
57
59
|
const publishers = [];
|
|
58
|
-
for (const { index, repo, version, charts } of lock.publishers) {
|
|
60
|
+
for (const { index, repo, version, indexDigest, charts } of lock.publishers) {
|
|
59
61
|
const label = `${index}@${version}`;
|
|
60
|
-
|
|
62
|
+
if (indexDigest === null) {
|
|
63
|
+
throw new Error(`${label}: a develop chart set names its deploy commit in the snapshot pins, not in an index chart (indexDigest is null)`);
|
|
64
|
+
}
|
|
65
|
+
// The index is read BY the digest the lock pins, so a re-pushed tag cannot answer.
|
|
66
|
+
const served = await client.getManifestByDigest(registryRepository(index), indexDigest);
|
|
67
|
+
if (served.digest !== indexDigest) {
|
|
68
|
+
throw new Error(`${label}: the registry served index manifest ${served.digest} but ${CHART_SET_LOCK_FILE} pins ${indexDigest}`);
|
|
69
|
+
}
|
|
70
|
+
const annotations = await manifestAnnotations(client, index, version, served);
|
|
61
71
|
const difference = rosterDifference(charts, parseIndexRoster(annotations[INDEX_ROSTER_ANNOTATION], label));
|
|
62
72
|
if (difference !== null) {
|
|
63
73
|
throw new Error(`${label}: the index roster is not the set ${CHART_SET_LOCK_FILE} locks (${difference}), so its source commit is not the locked set's`);
|
|
@@ -6,7 +6,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
6
6
|
|
|
7
7
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
8
8
|
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { holdSource, withSourceGitEnv } from './git-source-auth.mjs';
|
|
10
10
|
import { applyPinSource, snapshotPinSource } from './pin-source.mjs';
|
|
11
11
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
12
12
|
|
|
@@ -45,29 +45,30 @@ function git(args, env) {
|
|
|
45
45
|
execFileSync('git', args, { env, stdio: 'inherit' });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function checkoutIndexedSources({ workspaceRoot,
|
|
48
|
+
export function checkoutIndexedSources({ workspaceRoot, index, env = process.env }) {
|
|
49
49
|
if (!workspaceRoot) {throw new Error('workspaceRoot is required');}
|
|
50
|
-
if (!token) {throw new Error('EVIDENCE_REPOSITORIES_TOKEN is required');}
|
|
51
50
|
const workspace = editionWorkspaceConfig();
|
|
52
|
-
const env = gitAuthEnv(workspace.sourceRepositoryUrlPrefix, workspace.sourceAuthUsername, token);
|
|
53
51
|
const plan = sourceCheckoutPlan(index, workspace);
|
|
52
|
+
for (const entry of plan) {holdSource({ name: entry.repo, url: entry.repositoryUrl }, workspace, env);}
|
|
54
53
|
for (const entry of plan) {
|
|
55
54
|
const target = resolve(workspaceRoot, entry.repo);
|
|
56
55
|
if (existsSync(target)) {
|
|
57
56
|
throw new Error(`${entry.repo} target already exists; refusing to reuse an unverified checkout`);
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
withSourceGitEnv({ name: entry.repo, url: entry.repositoryUrl }, { workspace, env }, (gitEnv) => {
|
|
59
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
60
|
+
git(['init', '--quiet', target], gitEnv);
|
|
61
|
+
git(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], gitEnv);
|
|
62
|
+
git(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], gitEnv);
|
|
63
|
+
git(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], gitEnv);
|
|
64
|
+
const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
|
|
65
|
+
env: gitEnv,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
}).trim();
|
|
68
|
+
if (actual !== entry.sourceCommit) {
|
|
69
|
+
throw new Error(`${entry.repo} resolved ${actual}, expected ${entry.sourceCommit}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
71
72
|
console.log(`[source-checkout] ${entry.repo} @ ${entry.sourceCommit}`);
|
|
72
73
|
}
|
|
73
74
|
return plan.length;
|
|
@@ -94,7 +95,6 @@ if (isEntryModule(import.meta.url)) {
|
|
|
94
95
|
const index = indexForCheckout(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8')), process.argv.slice(2));
|
|
95
96
|
const count = checkoutIndexedSources({
|
|
96
97
|
workspaceRoot: sourcesRoot(),
|
|
97
|
-
token: process.env.EVIDENCE_REPOSITORIES_TOKEN,
|
|
98
98
|
index,
|
|
99
99
|
});
|
|
100
100
|
console.log(`[source-checkout] materialized ${count} indexed repositories at exact commits`);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// The flags that make a resolve a CUSTOMER resolve (against a catalogue).
|
|
2
2
|
export const DEFINITION_FLAG = '--definition';
|
|
3
|
-
export const CATALOGUE_FLAG = '--catalogue';
|
|
4
3
|
export const CHANNEL_FLAG = '--channel';
|
|
5
4
|
export const SOURCE_IMAGE_FLAG = '--source-image';
|
|
6
5
|
export const CATALOGUE_USERNAME_FLAG = '--catalogue-username';
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
// sealed against the develop image repositories.
|
|
11
11
|
|
|
12
12
|
import { sealLock } from './biome-content.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { parseCatalogueRef } from './catalogue-fetch.mjs';
|
|
14
|
+
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
15
|
+
import { sealDevelopImageSubjects } from './develop-seal.mjs';
|
|
16
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
14
17
|
import { CATALOGUE_PIN_KEY, EditionChannel } from './edition-constants.mjs';
|
|
15
18
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
16
|
-
import {
|
|
19
|
+
import { isSourcePinKey, sortedSnapshotPins, sourcePinKey } from './pin-source.mjs';
|
|
17
20
|
import { resolveCustomerEdition } from './resolve-customer-edition.mjs';
|
|
18
21
|
import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
|
|
22
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
19
23
|
|
|
20
24
|
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
21
25
|
|
|
@@ -39,11 +43,11 @@ export function customerDevelopPins({ pins, declaredSourceNames, catalogueDigest
|
|
|
39
43
|
for (const [key, value] of Object.entries(pins)) {
|
|
40
44
|
if (key === CATALOGUE_PIN_KEY) {
|
|
41
45
|
if (value !== catalogueDigest) {
|
|
42
|
-
throw new CustomerDevelopPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but
|
|
46
|
+
throw new CustomerDevelopPinsError(`--pins "${CATALOGUE_PIN_KEY}" is ${JSON.stringify(value)}, but the pinned develop catalogue is ${catalogueDigest}`);
|
|
43
47
|
}
|
|
44
48
|
continue;
|
|
45
49
|
}
|
|
46
|
-
if (
|
|
50
|
+
if (!isSourcePinKey(key)) {
|
|
47
51
|
throw new CustomerDevelopPinsError(`--pins "${key}" is not one of the customer's own repositories: a customer develop pins only sources/<name> (the vendor's pins are the catalogue's)`);
|
|
48
52
|
}
|
|
49
53
|
if (typeof value !== 'string' || !SHA_PATTERN.test(value)) {
|
|
@@ -79,7 +83,7 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
|
|
|
79
83
|
catalogueDigest,
|
|
80
84
|
...(verifyAuthority === undefined ? {} : { verifyAuthority }),
|
|
81
85
|
...(materialize === undefined ? {} : { materialize }),
|
|
82
|
-
sealOwn: (
|
|
86
|
+
sealOwn: ({ distributionId, subjects }) => sealDevelopImageSubjects({ distributionId, subjects, env, ...sealOptions }),
|
|
83
87
|
});
|
|
84
88
|
// The customer edition is sealed here: every biomeContentHash is recomputed through the
|
|
85
89
|
// same code as the vendor's, vendor biomes on the CATALOGUE's authority (biome-content.mjs).
|
|
@@ -97,3 +101,17 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
|
|
|
97
101
|
});
|
|
98
102
|
return { ...resolved, distributionLock, pins: frozen };
|
|
99
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* What a develop verb of a customer edition derives from its definition file and its `--pins` file with no
|
|
107
|
+
* network: the id (the definition's), the pinned develop catalogue reference and digest, and the frozen
|
|
108
|
+
* provenance pins (the customer's `sources/<name>` commits plus that digest).
|
|
109
|
+
*/
|
|
110
|
+
export function customerDevelopFrame({ definitionFile, pinsFile }) {
|
|
111
|
+
const rawDefinition = readStrictJsonFile(definitionFile);
|
|
112
|
+
assertCanonicalDistributionId(rawDefinition?.id);
|
|
113
|
+
const catalogueRef = readCataloguePin({ definitionFile, channel: EditionChannel.Develop });
|
|
114
|
+
const { digest } = parseCatalogueRef(catalogueRef);
|
|
115
|
+
const pins = customerDevelopPins({ pins: readStrictJsonFile(pinsFile), declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest: digest });
|
|
116
|
+
return { id: rawDefinition.id, rawDefinition, catalogueRef, catalogueDigest: digest, pins };
|
|
117
|
+
}
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// develop-edition — the develop edition's command line.
|
|
3
3
|
//
|
|
4
|
-
// resolve <id> --pins <file> --out <dir> --registry <
|
|
4
|
+
// resolve <id> --pins <file> --out <dir> --registry <root>
|
|
5
5
|
// --engine-image <repository>@sha256:<digest> [--local]
|
|
6
6
|
// resolve the edition from the snapshot, seal it against the develop image
|
|
7
7
|
// repositories, write the descriptor and the four locks to <dir>, print the target.
|
|
8
8
|
// The workflow run is recorded from GITHUB_REPOSITORY / GITHUB_RUN_ID;
|
|
9
9
|
// --local stamps `local` instead, for a resolve outside GitHub Actions.
|
|
10
|
-
// resolve --definition <file> --
|
|
10
|
+
// resolve --definition <file> --pins <file> --out <dir> --registry <root>
|
|
11
11
|
// --engine-image <repository>@sha256:<digest> [--source-image <source>=<registry>/<prefix>]...
|
|
12
12
|
// [--catalogue-username <name> --catalogue-token-env <VAR>] [--local]
|
|
13
|
-
// a CUSTOMER develop edition: the definition resolved against the catalogue
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// edition
|
|
13
|
+
// a CUSTOMER develop edition: the definition resolved against the develop catalogue pinned in
|
|
14
|
+
// catalogue.pin.json beside it (see catalogue-pin.mjs and resolve-customer-edition.mjs), frozen at the
|
|
15
|
+
// customer's own pins. --pins holds exactly one `sources/<name>` commit per source the definition
|
|
16
|
+
// declares (`freeze-sources --definition` writes it); the catalogue digest is frozen into the descriptor's
|
|
17
|
+
// provenance pins as `catalogue`. The id is the definition's. Takes --data-root, --editions-root,
|
|
18
|
+
// --sources-root, --kernel, --cache-dir (no --deploy-root).
|
|
19
|
+
// target <id> --pins <file> --registry <root>
|
|
20
|
+
// print <root>/<develop segment>/xema-edition-<id>:<snapshot hash>. The root is always the caller's
|
|
21
|
+
// registry root (e.g. registry.example.com/acme); the develop namespace segment and the repository
|
|
22
|
+
// prefix come from edition-contract.json, and no root is implied.
|
|
23
|
+
// target --definition <file> --pins <file> --registry <root>
|
|
24
|
+
// the same for a customer edition (id = the definition's; the tag covers the pinned catalogue digest).
|
|
21
25
|
// freeze-graph --data-sha <sha> --kernel-sha <sha>
|
|
22
26
|
// --deploy-sha <sha> [--editions-root-url <https url>]
|
|
23
27
|
// [--overrides <json>] --out <file>
|
|
@@ -26,12 +30,21 @@
|
|
|
26
30
|
// repositories are pinned at what the workflow checked out; write the pins.
|
|
27
31
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
28
32
|
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
33
|
+
// check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
34
|
+
// the same for a customer artifact; its descriptor's catalogue provenance pin must also equal the
|
|
35
|
+
// pinned develop catalogue digest.
|
|
29
36
|
// check-overrides --overrides <json>
|
|
30
37
|
// refuse a pins override that would select the tooling this run executes
|
|
31
38
|
// (the edition tooling's own repository); the workflow runs it before any other checkout.
|
|
32
39
|
// freeze-sources <id> --pins <file> [--overrides <json>] --out <file>
|
|
33
40
|
// add one pin per source the edition's chain declares (run after the editions
|
|
34
41
|
// root is checked out and passed as --editions-root); write the final pins.
|
|
42
|
+
// freeze-sources --definition <file> --out <file>
|
|
43
|
+
// a CUSTOMER edition: one `sources/<name>` pin per source the definition declares, each the head of
|
|
44
|
+
// refs/heads/develop of that source's repository, resolved once; written exclusively (wx).
|
|
45
|
+
// kernel-commit --definition <file> [--catalogue-username <name> --catalogue-token-env <VAR>]
|
|
46
|
+
// fetch the pinned develop catalogue and print its contractAuthority.sourceCommit (one 40-hex line):
|
|
47
|
+
// the commit the kernel contracts are checked out at. Takes --data-root only (no --kernel).
|
|
35
48
|
//
|
|
36
49
|
// Tokens come from the environment (EVIDENCE_REPOSITORIES_TOKEN, XEMA_GHCR_PULL_TOKEN),
|
|
37
50
|
// never argv. Nothing here writes git or publishes a release edition.
|
|
@@ -40,11 +53,12 @@ import { writeFileSync } from 'node:fs';
|
|
|
40
53
|
import { resolve } from 'node:path';
|
|
41
54
|
|
|
42
55
|
import { fetchCatalogue } from './catalogue-fetch.mjs';
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
56
|
+
import { readCataloguePin } from './catalogue-pin.mjs';
|
|
57
|
+
import { contractAuthoritySourceCommit, writeCatalogue } from './catalogue.mjs';
|
|
58
|
+
import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG, SOURCE_IMAGE_FLAG } from './customer-flags.mjs';
|
|
59
|
+
import { customerDevelopFrame, resolveCustomerDevelopEdition } from './develop-customer-edition.mjs';
|
|
46
60
|
import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
47
|
-
import { assertNoToolingOverride, freezeGraph, freezeSources, lsRemoteDevelopHead } from './develop-freeze.mjs';
|
|
61
|
+
import { assertNoToolingOverride, freezeCustomerSources, freezeGraph, freezeSources, lsRemoteDevelopHead } from './develop-freeze.mjs';
|
|
48
62
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
49
63
|
import { LOCAL_WORKFLOW, assertEngineImage, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
50
64
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
@@ -57,38 +71,73 @@ import { parseSourceImages } from './resolve-customer-edition.mjs';
|
|
|
57
71
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
58
72
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
59
73
|
|
|
74
|
+
// The roots a verb takes. A customer form (it names --definition) takes fewer: `target` none, `check-existing`
|
|
75
|
+
// the data root and the kernel, `freeze-sources` and `kernel-commit` the data root only (`kernel-commit` runs
|
|
76
|
+
// before the kernel is checked out), `resolve` all but the deploy root.
|
|
77
|
+
const VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache]);
|
|
78
|
+
const CUSTOMER_ROOTS = Object.freeze({
|
|
79
|
+
resolve: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
|
|
80
|
+
target: [],
|
|
81
|
+
'check-existing': [WorkspaceRoot.Data, WorkspaceRoot.Kernel],
|
|
82
|
+
'freeze-sources': [WorkspaceRoot.Data],
|
|
83
|
+
'kernel-commit': [WorkspaceRoot.Data],
|
|
84
|
+
});
|
|
85
|
+
|
|
60
86
|
if (isEntryModule(import.meta.url)) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
: [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Deploy, WorkspaceRoot.Cache, WorkspaceRoot.SourceHash],
|
|
65
|
-
);
|
|
87
|
+
const [command] = process.argv.slice(2);
|
|
88
|
+
const customerForm = process.argv.includes(DEFINITION_FLAG) || command === 'kernel-commit';
|
|
89
|
+
configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, { refuseOthers: customerForm && CUSTOMER_ROOTS[command] !== undefined });
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
const
|
|
92
|
+
// Per verb: the vendor form (`positional` id, `values`, `switches`) and, where the verb has one, the customer
|
|
93
|
+
// form: the flags only it takes, the further flags it requires, and the vendor flags it refuses.
|
|
94
|
+
const CREDENTIAL_FLAGS = Object.freeze([CATALOGUE_USERNAME_FLAG, CATALOGUE_TOKEN_ENV_FLAG]);
|
|
71
95
|
const REPEATABLE = Object.freeze([SOURCE_IMAGE_FLAG]);
|
|
72
96
|
|
|
73
97
|
const COMMANDS = Object.freeze({
|
|
74
|
-
resolve: {
|
|
75
|
-
|
|
98
|
+
resolve: {
|
|
99
|
+
positional: true,
|
|
100
|
+
values: ['--pins', '--out', '--registry', '--engine-image'],
|
|
101
|
+
switches: ['--local'],
|
|
102
|
+
required: ['--pins', '--out', '--registry', '--engine-image'],
|
|
103
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [] },
|
|
104
|
+
},
|
|
105
|
+
target: {
|
|
106
|
+
positional: true,
|
|
107
|
+
values: ['--pins', '--registry'],
|
|
108
|
+
switches: [],
|
|
109
|
+
required: ['--pins', '--registry'],
|
|
110
|
+
customer: { values: [DEFINITION_FLAG], required: ['--pins', '--registry'], refused: [] },
|
|
111
|
+
},
|
|
76
112
|
'freeze-graph': {
|
|
77
113
|
positional: false,
|
|
78
114
|
values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
|
|
79
115
|
switches: [],
|
|
116
|
+
required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
|
|
117
|
+
},
|
|
118
|
+
'freeze-sources': {
|
|
119
|
+
positional: true,
|
|
120
|
+
values: ['--pins', '--overrides', '--out'],
|
|
121
|
+
switches: [],
|
|
122
|
+
required: ['--pins', '--out'],
|
|
123
|
+
customer: { values: [DEFINITION_FLAG], required: ['--out'], refused: ['--pins', '--overrides'] },
|
|
124
|
+
},
|
|
125
|
+
'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
|
|
126
|
+
'check-existing': {
|
|
127
|
+
positional: true,
|
|
128
|
+
values: ['--pins', '--artifact', '--engine-image'],
|
|
129
|
+
switches: [],
|
|
130
|
+
required: ['--pins', '--artifact', '--engine-image'],
|
|
131
|
+
customer: { values: [DEFINITION_FLAG], required: ['--pins', '--artifact', '--engine-image'], refused: [] },
|
|
132
|
+
},
|
|
133
|
+
'kernel-commit': {
|
|
134
|
+
positional: false,
|
|
135
|
+
values: [],
|
|
136
|
+
switches: [],
|
|
137
|
+
required: [],
|
|
138
|
+
customerOnly: true,
|
|
139
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS], required: [], refused: [] },
|
|
80
140
|
},
|
|
81
|
-
'freeze-sources': { positional: true, values: ['--pins', '--overrides', '--out'], switches: [] },
|
|
82
|
-
'check-overrides': { positional: false, values: ['--overrides'], switches: [] },
|
|
83
|
-
'check-existing': { positional: true, values: ['--pins', '--artifact', '--engine-image'], switches: [] },
|
|
84
|
-
});
|
|
85
|
-
const REQUIRED = Object.freeze({
|
|
86
|
-
resolve: ['--pins', '--out', '--registry', '--engine-image'],
|
|
87
|
-
target: ['--pins', '--registry'],
|
|
88
|
-
'freeze-graph': ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
|
|
89
|
-
'freeze-sources': ['--pins', '--out'],
|
|
90
|
-
'check-overrides': ['--overrides'],
|
|
91
|
-
'check-existing': ['--pins', '--artifact', '--engine-image'],
|
|
92
141
|
});
|
|
93
142
|
|
|
94
143
|
/** Parse `<command> [<id>] --flag value ...` against the closed vocabulary of the command. */
|
|
@@ -98,13 +147,14 @@ export function parseDevelopEditionArgs(argv) {
|
|
|
98
147
|
if (spec === undefined) {
|
|
99
148
|
throw new Error(`unknown command ${JSON.stringify(command)}; expected ${Object.keys(COMMANDS).join(' | ')}`);
|
|
100
149
|
}
|
|
150
|
+
const customerValues = spec.customer?.values ?? [];
|
|
101
151
|
const options = {};
|
|
102
152
|
const positional = [];
|
|
103
153
|
for (let i = 0; i < rest.length; i += 1) {
|
|
104
154
|
const arg = rest[i];
|
|
105
155
|
if (spec.switches.includes(arg)) {
|
|
106
156
|
options[arg] = true;
|
|
107
|
-
} else if (spec.values.includes(arg)) {
|
|
157
|
+
} else if (spec.values.includes(arg) || customerValues.includes(arg)) {
|
|
108
158
|
const value = rest[i + 1];
|
|
109
159
|
if (value === undefined || (value.startsWith('--') && value.length > 2)) {
|
|
110
160
|
throw new Error(`${arg} requires a value`);
|
|
@@ -123,21 +173,22 @@ export function parseDevelopEditionArgs(argv) {
|
|
|
123
173
|
positional.push(arg);
|
|
124
174
|
}
|
|
125
175
|
}
|
|
126
|
-
const customer =
|
|
176
|
+
const customer = spec.customer !== undefined && (spec.customerOnly === true || customerValues.some((flag) => options[flag] !== undefined));
|
|
127
177
|
if (customer) {
|
|
128
|
-
|
|
129
|
-
|
|
178
|
+
if (options[DEFINITION_FLAG] === undefined) {throw new Error(`a customer ${command} requires ${DEFINITION_FLAG}`);}
|
|
179
|
+
for (const refused of spec.customer.refused) {
|
|
180
|
+
if (options[refused] !== undefined) {throw new Error(`a customer ${command} takes no ${refused}`);}
|
|
130
181
|
}
|
|
131
|
-
if (positional.length !== 0) {throw new Error(
|
|
182
|
+
if (positional.length !== 0) {throw new Error(`a customer ${command} takes no <distribution-id>: the id is the definition's`);}
|
|
132
183
|
} else if (spec.positional ? positional.length !== 1 : positional.length !== 0) {
|
|
133
184
|
throw new Error(`${command} takes ${spec.positional ? 'exactly one <distribution-id>' : 'no positional argument'}`);
|
|
134
185
|
}
|
|
135
|
-
for (const required of
|
|
186
|
+
for (const required of customer ? spec.customer.required : spec.required) {
|
|
136
187
|
if (options[required] === undefined) {throw new Error(`${command} requires ${required}`);}
|
|
137
188
|
}
|
|
138
189
|
if (spec.positional && !customer) {assertCanonicalDistributionId(positional[0]);}
|
|
139
190
|
if (options['--engine-image'] !== undefined) {assertEngineImage(options['--engine-image']);}
|
|
140
|
-
return { command, id: positional[0], options };
|
|
191
|
+
return { command, id: positional[0], options, customer };
|
|
141
192
|
}
|
|
142
193
|
|
|
143
194
|
function parseOverrides(text) {
|
|
@@ -154,15 +205,15 @@ function writeOnce(path, value) {
|
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
function headResolver(env) {
|
|
157
|
-
|
|
158
|
-
const username = editionWorkspaceConfig().sourceAuthUsername;
|
|
159
|
-
return (url) => lsRemoteDevelopHead(url, { username, token });
|
|
208
|
+
return (source) => lsRemoteDevelopHead(source, { workspace: editionWorkspaceConfig(), env });
|
|
160
209
|
}
|
|
161
210
|
|
|
162
211
|
/** A customer develop resolve: the artifact (descriptor and four locks, no catalogue: it is not an offering) goes to --out. */
|
|
163
212
|
async function resolveCustomer(options, env) {
|
|
213
|
+
const ref = readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop });
|
|
164
214
|
const { catalogue, digest } = await fetchCatalogue({
|
|
165
|
-
ref
|
|
215
|
+
ref,
|
|
216
|
+
channel: EditionChannel.Develop,
|
|
166
217
|
username: options[CATALOGUE_USERNAME_FLAG],
|
|
167
218
|
tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
|
|
168
219
|
env,
|
|
@@ -194,15 +245,37 @@ async function resolveCustomer(options, env) {
|
|
|
194
245
|
console.log(target);
|
|
195
246
|
}
|
|
196
247
|
|
|
248
|
+
/**
|
|
249
|
+
* The commit the pinned develop catalogue's contract authority was taken at. The kernel contracts are
|
|
250
|
+
* not checked out yet when this runs, so nothing here validates against kernel schemas: the catalogue
|
|
251
|
+
* parser is kernel-free, and the one field read is `contractAuthority.sourceCommit`, held to 40 hex.
|
|
252
|
+
*/
|
|
253
|
+
async function customerKernelCommit(options, env) {
|
|
254
|
+
const ref = readCataloguePin({ definitionFile: options[DEFINITION_FLAG], channel: EditionChannel.Develop });
|
|
255
|
+
const { catalogue } = await fetchCatalogue({
|
|
256
|
+
ref,
|
|
257
|
+
channel: EditionChannel.Develop,
|
|
258
|
+
username: options[CATALOGUE_USERNAME_FLAG],
|
|
259
|
+
tokenEnv: options[CATALOGUE_TOKEN_ENV_FLAG],
|
|
260
|
+
env,
|
|
261
|
+
});
|
|
262
|
+
console.log(contractAuthoritySourceCommit(catalogue));
|
|
263
|
+
}
|
|
264
|
+
|
|
197
265
|
async function run(argv, env) {
|
|
198
|
-
const { command, id, options } = parseDevelopEditionArgs(argv);
|
|
266
|
+
const { command, id, options, customer } = parseDevelopEditionArgs(argv);
|
|
199
267
|
switch (command) {
|
|
200
268
|
case 'target': {
|
|
269
|
+
if (customer) {
|
|
270
|
+
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
271
|
+
console.log(developEditionTarget(frame.id, frame.pins, { registry: options['--registry'] }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
201
274
|
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'] }));
|
|
202
275
|
return;
|
|
203
276
|
}
|
|
204
277
|
case 'resolve': {
|
|
205
|
-
if (
|
|
278
|
+
if (customer) {return resolveCustomer(options, env);}
|
|
206
279
|
const pins = readStrictJsonFile(options['--pins']);
|
|
207
280
|
const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
|
|
208
281
|
const target = developEditionTarget(id, pins, { registry: options['--registry'] });
|
|
@@ -224,6 +297,19 @@ async function run(argv, env) {
|
|
|
224
297
|
return;
|
|
225
298
|
}
|
|
226
299
|
case 'check-existing': {
|
|
300
|
+
if (customer) {
|
|
301
|
+
const frame = customerDevelopFrame({ definitionFile: options[DEFINITION_FLAG], pinsFile: options['--pins'] });
|
|
302
|
+
assertExistingArtifactMatches({
|
|
303
|
+
artifactDir: resolve(options['--artifact']),
|
|
304
|
+
distributionId: frame.id,
|
|
305
|
+
channel: EditionChannel.Develop,
|
|
306
|
+
evidenceTrustPolicy: await descriptorEvidenceTrust({ channel: EditionChannel.Develop }),
|
|
307
|
+
pins: frame.pins,
|
|
308
|
+
engineImage: options['--engine-image'],
|
|
309
|
+
catalogueDigest: frame.catalogueDigest,
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
227
313
|
assertExistingArtifactMatches({
|
|
228
314
|
artifactDir: resolve(options['--artifact']),
|
|
229
315
|
distributionId: id,
|
|
@@ -252,6 +338,17 @@ async function run(argv, env) {
|
|
|
252
338
|
return;
|
|
253
339
|
}
|
|
254
340
|
case 'freeze-sources': {
|
|
341
|
+
if (customer) {
|
|
342
|
+
writeOnce(
|
|
343
|
+
options['--out'],
|
|
344
|
+
freezeCustomerSources({
|
|
345
|
+
definition: readStrictJsonFile(options[DEFINITION_FLAG]),
|
|
346
|
+
resolveHead: headResolver(env),
|
|
347
|
+
sourceRepositoryUrlPrefix: editionWorkspaceConfig().sourceRepositoryUrlPrefix,
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
255
352
|
const pins = readStrictJsonFile(options['--pins']);
|
|
256
353
|
const { parseDistribution } = await loadKernelDistributionSchemas();
|
|
257
354
|
const declaredSources = composeEdition(parseDistribution, id, snapshotPinSource(pins)).declaredSources;
|
|
@@ -261,6 +358,8 @@ async function run(argv, env) {
|
|
|
261
358
|
);
|
|
262
359
|
return;
|
|
263
360
|
}
|
|
361
|
+
case 'kernel-commit':
|
|
362
|
+
return customerKernelCommit(options, env);
|
|
264
363
|
default:
|
|
265
364
|
throw new Error(`unhandled command ${command}`);
|
|
266
365
|
}
|