@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
|
@@ -78,6 +78,22 @@ function requireSourceAuthUsername(config) {
|
|
|
78
78
|
return value;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** `chartRegistry` and `chartPublishers` are a vendor-only pair: both declared or both absent. */
|
|
82
|
+
function optionalChartSet(document) {
|
|
83
|
+
const registry = document.chartRegistry !== undefined;
|
|
84
|
+
const publishers = document.chartPublishers !== undefined;
|
|
85
|
+
if (registry !== publishers) {
|
|
86
|
+
throw new EditionWorkspaceConfigError(
|
|
87
|
+
`${EDITION_WORKSPACE_CONFIG_FILE}: "chartRegistry" and "chartPublishers" are a vendor-only pair, both declared or both absent; only "${registry ? 'chartRegistry' : 'chartPublishers'}" is`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!registry) {return {};}
|
|
91
|
+
return {
|
|
92
|
+
chartRegistry: requireString(document, 'chartRegistry', CHART_REGISTRY, 'oci://<host>/<path>'),
|
|
93
|
+
chartPublishers: requirePublishers(document),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
function requirePublishers(config) {
|
|
82
98
|
const value = config.chartPublishers;
|
|
83
99
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -99,7 +115,7 @@ function requirePublishers(config) {
|
|
|
99
115
|
);
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
const EVIDENCE_TRUST_POLICY_KEYS = Object.freeze(['pin', 'policy'
|
|
118
|
+
const EVIDENCE_TRUST_POLICY_KEYS = Object.freeze(['pin', 'policy']);
|
|
103
119
|
|
|
104
120
|
function isDataRootRelativePath(value) {
|
|
105
121
|
return (
|
|
@@ -110,33 +126,31 @@ function isDataRootRelativePath(value) {
|
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
113
|
-
* The MANDATORY `evidenceTrustPolicy` declaration
|
|
114
|
-
*
|
|
115
|
-
* policy is
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* `xema edition check` refuses (EvidenceTrustRequiredError), so the refusal
|
|
119
|
-
* names the missing policy rather than a malformed file.
|
|
129
|
+
* The MANDATORY `evidenceTrustPolicy` declaration: `{policy, pin}` (two
|
|
130
|
+
* data-root-relative paths) or `null`, the explicit statement that this data
|
|
131
|
+
* root declares no policy. There is no switch that admits an ungated release:
|
|
132
|
+
* a release edition is written only against a policy ARMED on the release
|
|
133
|
+
* channel, so a data root that declares none serves develop editions only.
|
|
120
134
|
*/
|
|
121
135
|
function requireEvidenceTrustPolicy(config) {
|
|
122
136
|
if (!Object.hasOwn(config, 'evidenceTrustPolicy')) {
|
|
123
137
|
throw new EditionWorkspaceConfigError(
|
|
124
|
-
`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is required: {"
|
|
125
|
-
'or
|
|
138
|
+
`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is required: {"policy": ..., "pin": ...}, ` +
|
|
139
|
+
'or null to state that this data root declares no policy (no release edition can then be sealed from it)',
|
|
126
140
|
);
|
|
127
141
|
}
|
|
128
142
|
const value = config.evidenceTrustPolicy;
|
|
129
|
-
|
|
143
|
+
if (value === null) {return Object.freeze({ policy: null, pin: null });}
|
|
144
|
+
const shapeError = (detail) =>
|
|
130
145
|
new EditionWorkspaceConfigError(
|
|
131
|
-
`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is exactly
|
|
146
|
+
`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy" is exactly null or {policy, pin} with both paths data-root-relative${detail}`,
|
|
132
147
|
);
|
|
133
|
-
if (
|
|
148
|
+
if (typeof value !== 'object' || Array.isArray(value)) {throw shapeError('');}
|
|
134
149
|
const keys = Object.keys(value).sort();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
for (const key of ['policy', 'pin']) {
|
|
150
|
+
const unknown = keys.filter((key) => !EVIDENCE_TRUST_POLICY_KEYS.includes(key));
|
|
151
|
+
if (unknown.length > 0) {throw shapeError(`; unknown key ${unknown.map((key) => `"${key}"`).join(', ')}`);}
|
|
152
|
+
if (keys.join() !== EVIDENCE_TRUST_POLICY_KEYS.join()) {throw shapeError('');}
|
|
153
|
+
for (const key of EVIDENCE_TRUST_POLICY_KEYS) {
|
|
140
154
|
if (!isDataRootRelativePath(value[key])) {
|
|
141
155
|
throw new EditionWorkspaceConfigError(
|
|
142
156
|
`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy.${key}" must be a data-root-relative POSIX path without "." or ".." segments; got ${JSON.stringify(value[key])}`,
|
|
@@ -146,7 +160,7 @@ function requireEvidenceTrustPolicy(config) {
|
|
|
146
160
|
if (value.policy === value.pin) {
|
|
147
161
|
throw new EditionWorkspaceConfigError(`${EDITION_WORKSPACE_CONFIG_FILE}: "evidenceTrustPolicy.policy" and ".pin" must be different files`);
|
|
148
162
|
}
|
|
149
|
-
return Object.freeze({
|
|
163
|
+
return Object.freeze({ policy: value.policy, pin: value.pin });
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
/** Validate a parsed config document; returns the frozen, typed config. */
|
|
@@ -168,8 +182,7 @@ export function parseEditionWorkspaceConfig(document) {
|
|
|
168
182
|
return Object.freeze({
|
|
169
183
|
...sections,
|
|
170
184
|
imageRegistry: requireString(document, 'imageRegistry', IMAGE_REGISTRY, '<host>/<namespace> with no scheme or trailing slash'),
|
|
171
|
-
|
|
172
|
-
chartPublishers: requirePublishers(document),
|
|
185
|
+
...optionalChartSet(document),
|
|
173
186
|
sourceRepositoryUrlPrefix: requireString(document, 'sourceRepositoryUrlPrefix', SOURCE_URL_PREFIX, 'https://<host>/<organisation>/ with a trailing slash'),
|
|
174
187
|
sourceAuthUsername: requireSourceAuthUsername(document),
|
|
175
188
|
evidenceTrustPolicy: requireEvidenceTrustPolicy(document),
|
|
@@ -192,6 +205,17 @@ export function resetEditionWorkspaceConfig() {
|
|
|
192
205
|
cached = undefined;
|
|
193
206
|
}
|
|
194
207
|
|
|
208
|
+
/** The vendor chart set's coordinates `{chartRegistry, chartPublishers}`; a data root that declares neither (a customer's) is refused, naming the file and both keys. */
|
|
209
|
+
export function vendorChartSetConfig() {
|
|
210
|
+
const { chartRegistry, chartPublishers } = editionWorkspaceConfig();
|
|
211
|
+
if (chartRegistry === undefined) {
|
|
212
|
+
throw new EditionWorkspaceConfigError(
|
|
213
|
+
`${EDITION_WORKSPACE_CONFIG_FILE} in ${dataRoot()} declares no "chartRegistry" and "chartPublishers", which vendor chart-set code needs`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return { chartRegistry, chartPublishers };
|
|
217
|
+
}
|
|
218
|
+
|
|
195
219
|
/**
|
|
196
220
|
* One product-data section, refused by name when the data root does not carry
|
|
197
221
|
* it. `needed` says which operation wanted it, so the refusal is actionable.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// edition-constants — the closed vocabulary of the edition OCI artifact, in one
|
|
2
2
|
// place so the writer, the guards and the workflow cannot disagree about what an
|
|
3
3
|
// edition IS. ONE layout serves both channels: an artifact at
|
|
4
|
-
// `<registry>/xema-edition-<id>`, addressed by digest,
|
|
5
|
-
// descriptor layer plus the lock layers.
|
|
4
|
+
// `<registry root>/<channel segment>/xema-edition-<id>`, addressed by digest,
|
|
5
|
+
// carrying a titled descriptor layer plus the lock layers.
|
|
6
6
|
|
|
7
7
|
import { EditionChannel } from './edition-channel.mjs';
|
|
8
8
|
import { EDITION_CHART_SET_FILE } from './edition-chart-set.mjs';
|
|
9
|
+
import { parseEditionContract } from './edition-contract-parse.mjs';
|
|
9
10
|
import editionContract from './edition-contract.json' with { type: 'json' };
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -15,16 +16,21 @@ import editionContract from './edition-contract.json' with { type: 'json' };
|
|
|
15
16
|
* It is a JSON import, not a read at run time, so a bundle (the evidence
|
|
16
17
|
* verifier) carries it.
|
|
17
18
|
*/
|
|
18
|
-
export const EDITION_CONTRACT = Object.freeze(structuredClone(editionContract));
|
|
19
|
+
export const EDITION_CONTRACT = Object.freeze(structuredClone(parseEditionContract(editionContract)));
|
|
19
20
|
|
|
20
21
|
/** The descriptor layer's file name; its `org.opencontainers.image.title`. */
|
|
21
22
|
export const EDITION_DESCRIPTOR_FILE = EDITION_CONTRACT.descriptorFile;
|
|
22
23
|
export const EDITION_DESCRIPTOR_SCHEMA = EDITION_CONTRACT.descriptorSchema;
|
|
23
24
|
/** The ONE artifactType of every edition artifact, develop or release. */
|
|
24
25
|
export const EDITION_ARTIFACT_TYPE = EDITION_CONTRACT.artifactType;
|
|
25
|
-
/** The repository name prefix: `<registry>/<prefix><id>`. */
|
|
26
|
+
/** The repository name prefix: `<registry root>/<channel segment>/<prefix><id>`. */
|
|
26
27
|
export const EDITION_REPOSITORY_PREFIX = EDITION_CONTRACT.repositoryPrefix;
|
|
27
28
|
|
|
29
|
+
/** The namespace segment of each channel's edition repositories, keyed by channel: `<registry root>/<segment>/<prefix><id>`. */
|
|
30
|
+
export const EDITION_REPOSITORY_SEGMENTS = Object.freeze(
|
|
31
|
+
Object.fromEntries(Object.values(EditionChannel).map((channel) => [channel, EDITION_CONTRACT.repositories[channel].namespaceSegment])),
|
|
32
|
+
);
|
|
33
|
+
|
|
28
34
|
export { EditionChannel };
|
|
29
35
|
|
|
30
36
|
/** Whether the data root's editions are evidence-gated, as the descriptor records it. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// edition-contract-parse — the strict parse of edition-contract.json. A consumer
|
|
2
|
+
// that vendors the contract holds it to this same shape: a contract that is not
|
|
3
|
+
// v2, or lacks `repositories`, or names a channel this CLI does not know, is
|
|
4
|
+
// refused rather than read with a default.
|
|
5
|
+
|
|
6
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
7
|
+
|
|
8
|
+
export const EDITION_CONTRACT_SCHEMA = 'xema.edition-contract/v2';
|
|
9
|
+
|
|
10
|
+
const SEGMENT = /^[a-z0-9][a-z0-9.-]*$/;
|
|
11
|
+
|
|
12
|
+
/** The strict parse of the contract; returns it unchanged. */
|
|
13
|
+
export function parseEditionContract(contract) {
|
|
14
|
+
if (contract === null || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
15
|
+
throw new Error('edition-contract.json must be a JSON object');
|
|
16
|
+
}
|
|
17
|
+
if (contract.schema !== EDITION_CONTRACT_SCHEMA) {
|
|
18
|
+
throw new Error(`edition-contract.json: schema is ${JSON.stringify(contract.schema)}, expected ${JSON.stringify(EDITION_CONTRACT_SCHEMA)}`);
|
|
19
|
+
}
|
|
20
|
+
if (typeof contract.vendoring !== 'string' || contract.vendoring === '') {
|
|
21
|
+
throw new Error('edition-contract.json: "vendoring" must be the non-empty statement of how a consumer vendors this file');
|
|
22
|
+
}
|
|
23
|
+
if (typeof contract.repositoryPrefix !== 'string' || contract.repositoryPrefix === '') {
|
|
24
|
+
throw new Error('edition-contract.json: "repositoryPrefix" must be a non-empty string');
|
|
25
|
+
}
|
|
26
|
+
const repositories = contract.repositories;
|
|
27
|
+
if (repositories === null || typeof repositories !== 'object' || Array.isArray(repositories)) {
|
|
28
|
+
throw new Error('edition-contract.json has no "repositories": the namespace segment of each channel is read from it and nowhere else');
|
|
29
|
+
}
|
|
30
|
+
const channels = Object.values(EditionChannel);
|
|
31
|
+
const keys = Object.keys(repositories);
|
|
32
|
+
const unknown = keys.filter((key) => !channels.includes(key));
|
|
33
|
+
if (unknown.length > 0 || keys.length !== channels.length) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`edition-contract.json: "repositories" is exactly one entry per channel (${channels.join(' | ')}); ` +
|
|
36
|
+
(unknown.length > 0 ? `unknown channel ${unknown.map((key) => JSON.stringify(key)).join(', ')}` : `got ${keys.join(', ') || 'none'}`),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const segments = new Set();
|
|
40
|
+
for (const channel of channels) {
|
|
41
|
+
const entry = repositories[channel];
|
|
42
|
+
const entryKeys = entry !== null && typeof entry === 'object' && !Array.isArray(entry) ? Object.keys(entry) : [];
|
|
43
|
+
if (entryKeys.join() !== 'namespaceSegment' || typeof entry.namespaceSegment !== 'string' || !SEGMENT.test(entry.namespaceSegment)) {
|
|
44
|
+
throw new Error(`edition-contract.json: repositories.${channel} is exactly {namespaceSegment}, one lowercase path segment`);
|
|
45
|
+
}
|
|
46
|
+
segments.add(entry.namespaceSegment);
|
|
47
|
+
}
|
|
48
|
+
if (segments.size !== channels.length) {
|
|
49
|
+
throw new Error('edition-contract.json: the channels\' namespace segments must differ');
|
|
50
|
+
}
|
|
51
|
+
return contract;
|
|
52
|
+
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schema": "xema.edition-contract/
|
|
2
|
+
"schema": "xema.edition-contract/v2",
|
|
3
|
+
"vendoring": "This file is the single source of the edition artifact coordinates and names. A consumer vendors it BYTE-IDENTICAL from a stated xema-cli commit and never edits it. It reads both repository namespace segments (repositories.<channel>.namespaceSegment) and the repository prefix (repositoryPrefix) only from this file, and refuses a contract that has no `repositories`.",
|
|
3
4
|
"artifactType": "application/vnd.xema.edition.v1+json",
|
|
4
5
|
"repositoryPrefix": "xema-edition-",
|
|
6
|
+
"repositories": {
|
|
7
|
+
"release": {
|
|
8
|
+
"namespaceSegment": "editions"
|
|
9
|
+
},
|
|
10
|
+
"develop": {
|
|
11
|
+
"namespaceSegment": "develop-editions"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
5
14
|
"descriptorFile": "xema-edition.json",
|
|
6
15
|
"descriptorSchema": "xema.edition/v1",
|
|
7
|
-
"locks": [
|
|
8
|
-
|
|
16
|
+
"locks": [
|
|
17
|
+
"distribution.lock.json",
|
|
18
|
+
"images.lock.json",
|
|
19
|
+
"authorization.lock.json",
|
|
20
|
+
"chart-set.lock.json"
|
|
21
|
+
],
|
|
22
|
+
"offeringLayers": {
|
|
23
|
+
"catalogue": "catalogue.json"
|
|
24
|
+
},
|
|
9
25
|
"provenancePins": {
|
|
10
26
|
"catalogue": "the sha256 digest (sha256:<64 hex>) of the offering artifact a customer edition was resolved against; every other pin is a 40-hex commit"
|
|
11
27
|
},
|
|
@@ -13,11 +29,11 @@
|
|
|
13
29
|
"evidenceTrust": {
|
|
14
30
|
"states": {
|
|
15
31
|
"armed": "the data root declares an evidence trust policy that is pinned and ARMED on the channel of the edition",
|
|
16
|
-
"not-armed": "a
|
|
32
|
+
"not-armed": "a develop edition only: its data root declares no policy, or one that is not armed on the develop channel. A release edition never records not-armed"
|
|
17
33
|
},
|
|
18
34
|
"refusal": {
|
|
19
35
|
"code": "EVIDENCE_TRUST_REQUIRED_UNARMED",
|
|
20
|
-
"when": "
|
|
36
|
+
"when": "a release edition is written while the data root's evidence trust policy is not armed on the release channel (none declared, or declared but UNARMED or PARTIALLY ARMED there): no release descriptor or artifact is written, and no configuration key admits one. Arming is judged per channel: a develop edition admits not-armed"
|
|
21
37
|
}
|
|
22
38
|
}
|
|
23
39
|
}
|
|
@@ -14,7 +14,6 @@ export const WorkspaceRoot = Object.freeze({
|
|
|
14
14
|
Kernel: 'kernelPackageDir',
|
|
15
15
|
Deploy: 'deployRoot',
|
|
16
16
|
Cache: 'cacheDir',
|
|
17
|
-
SourceHash: 'sourceHashPackageDir',
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
export const WORKSPACE_FLAGS = Object.freeze({
|
|
@@ -24,7 +23,6 @@ export const WORKSPACE_FLAGS = Object.freeze({
|
|
|
24
23
|
[WorkspaceRoot.Kernel]: '--kernel',
|
|
25
24
|
[WorkspaceRoot.Deploy]: '--deploy-root',
|
|
26
25
|
[WorkspaceRoot.Cache]: '--cache-dir',
|
|
27
|
-
[WorkspaceRoot.SourceHash]: '--source-hash',
|
|
28
26
|
});
|
|
29
27
|
|
|
30
28
|
const ROOT_MEANING = Object.freeze({
|
|
@@ -37,8 +35,6 @@ const ROOT_MEANING = Object.freeze({
|
|
|
37
35
|
[WorkspaceRoot.Kernel]: 'the BUILT kernel-contracts package directory',
|
|
38
36
|
[WorkspaceRoot.Deploy]: 'the deploy (chart tree) checkout',
|
|
39
37
|
[WorkspaceRoot.Cache]: 'a writable directory for fetched artifacts',
|
|
40
|
-
[WorkspaceRoot.SourceHash]:
|
|
41
|
-
'the BUILT distribution-source-hash package directory (the source-tree and image-input hash implementation)',
|
|
42
38
|
});
|
|
43
39
|
|
|
44
40
|
const KERNEL_PACKAGE_SUBPATH = ['packages', 'kernel', 'kernel-contracts'];
|
|
@@ -90,7 +86,6 @@ export const sourcesRoot = () => root(WorkspaceRoot.Sources);
|
|
|
90
86
|
export const kernelPackageDir = () => root(WorkspaceRoot.Kernel);
|
|
91
87
|
export const deployRoot = () => root(WorkspaceRoot.Deploy);
|
|
92
88
|
export const cacheDir = () => root(WorkspaceRoot.Cache);
|
|
93
|
-
export const sourceHashPackageDir = () => root(WorkspaceRoot.SourceHash);
|
|
94
89
|
|
|
95
90
|
/** A source checkout at its aggregator-relative path, e.g. `repos/xema-base`. */
|
|
96
91
|
export const sourcePath = (repo) => join(sourcesRoot(), repo);
|
|
@@ -113,9 +108,11 @@ export const runtimeImageSourceIndexPath = () =>
|
|
|
113
108
|
/**
|
|
114
109
|
* Parse the workspace flags out of an argv (both `--flag v` and `--flag=v`),
|
|
115
110
|
* configure the roots, and return the remaining argv. `needs` names the roots
|
|
116
|
-
* the entry requires: a missing one is refused here, before any work.
|
|
111
|
+
* the entry requires: a missing one is refused here, before any work. With
|
|
112
|
+
* `refuseOthers` a root flag whose root `needs` does not name is refused too
|
|
113
|
+
* (a customer verb takes only the roots it needs).
|
|
117
114
|
*/
|
|
118
|
-
export function configureEditionWorkspaceFromArgv(argv, needs) {
|
|
115
|
+
export function configureEditionWorkspaceFromArgv(argv, needs, { refuseOthers = false } = {}) {
|
|
119
116
|
const byFlag = new Map(
|
|
120
117
|
Object.entries(WORKSPACE_FLAGS).map(([name, flag]) => [flag, name]),
|
|
121
118
|
);
|
|
@@ -130,6 +127,11 @@ export function configureEditionWorkspaceFromArgv(argv, needs) {
|
|
|
130
127
|
rest.push(arg);
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
130
|
+
if (refuseOthers && !needs.includes(name)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${flag} is not a root this command takes (it takes: ${needs.length === 0 ? 'no root' : needs.map((root) => WORKSPACE_FLAGS[root]).join(' ')})`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
133
135
|
const value = eq > 0 ? arg.slice(eq + 1) : argv[(i += 1)];
|
|
134
136
|
if (typeof value !== 'string' || value.length === 0 || value.startsWith('--')) {
|
|
135
137
|
throw new Error(`${flag} requires a directory path`);
|
|
@@ -169,8 +171,20 @@ export function isEntryModule(moduleUrl) {
|
|
|
169
171
|
/**
|
|
170
172
|
* The entry's one call: configure the roots from `process.argv` and remove the
|
|
171
173
|
* root flags from it, so the entry's own parser sees only its own arguments.
|
|
174
|
+
* `options` are those of `configureEditionWorkspaceFromArgv`.
|
|
172
175
|
*/
|
|
173
|
-
export function configureEditionWorkspaceFromProcess(needs) {
|
|
174
|
-
|
|
176
|
+
export function configureEditionWorkspaceFromProcess(needs, options) {
|
|
177
|
+
let rest;
|
|
178
|
+
try {
|
|
179
|
+
rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), needs, options);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// An entry calls this before it has a `main` to catch anything: a missing
|
|
182
|
+
// or malformed root is the operator's mistake, answered with the roots the
|
|
183
|
+
// command takes, not with a stack trace.
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`${error.message}\nusage: this command takes ${needs.map((name) => `${WORKSPACE_FLAGS[name]} <dir>`).join(' ')}\n`,
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
175
189
|
process.argv.splice(2, process.argv.length - 2, ...rest);
|
|
176
190
|
}
|
|
@@ -386,6 +386,24 @@ export function gradeTrustedCheckouts({ grades, trustedGit, expected }) {
|
|
|
386
386
|
group.requirements.push({ root: canonicalRoot, label, requiredPaths });
|
|
387
387
|
byRepository.set(location.repositoryRoot, group);
|
|
388
388
|
}
|
|
389
|
+
// A re-grade covers exactly the roots first graded: a root the first grade
|
|
390
|
+
// never saw has no HEAD to be held to, and is refused rather than re-graded
|
|
391
|
+
// unchecked.
|
|
392
|
+
if (expected !== undefined) {
|
|
393
|
+
const regraded = new Set(
|
|
394
|
+
[...byRepository.values()].flatMap((group) => group.requirements.map(({ root }) => root)),
|
|
395
|
+
);
|
|
396
|
+
for (const root of regraded) {
|
|
397
|
+
if (typeof expected.get(root)?.commit !== 'string') {
|
|
398
|
+
throw new Error(`${root} was not graded first, so its HEAD cannot be held unchanged`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const root of expected.keys()) {
|
|
402
|
+
if (!regraded.has(root)) {
|
|
403
|
+
throw new Error(`${root} was graded first and is missing from the re-grade`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
389
407
|
const result = new Map();
|
|
390
408
|
for (const [repositoryRoot, group] of byRepository) {
|
|
391
409
|
assertTrustedCheckoutLocation(group.location, group.labels.join(' + '));
|
|
@@ -393,9 +411,12 @@ export function gradeTrustedCheckouts({ grades, trustedGit, expected }) {
|
|
|
393
411
|
repositoryRoot,
|
|
394
412
|
requirements: group.requirements,
|
|
395
413
|
trustedGit,
|
|
396
|
-
expectedCommit: expected?.get(group.requirements[0].root)
|
|
414
|
+
expectedCommit: expected?.get(group.requirements[0].root).commit,
|
|
397
415
|
});
|
|
398
416
|
for (const { root } of group.requirements) {
|
|
417
|
+
if (expected !== undefined && expected.get(root).commit !== graded.commit) {
|
|
418
|
+
throw new Error(`${repositoryRoot} HEAD changed during verification`);
|
|
419
|
+
}
|
|
399
420
|
result.set(root, graded);
|
|
400
421
|
}
|
|
401
422
|
}
|