@xemahq/xema 0.25.1 → 0.27.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 +70 -37
- package/dist/assets/appliance-base/images.lock.json +126 -126
- package/dist/assets/appliance-lean/images.lock.json +123 -123
- package/dist/assets/cloud/images.lock.json +126 -126
- package/dist/assets/core/images.lock.json +120 -120
- package/dist/assets/distribution-assets.manifest.json +15 -16
- package/dist/assets/minimal/images.lock.json +108 -108
- package/dist/assets/oss/images.lock.json +126 -126
- package/dist/assets/starter/images.lock.json +123 -123
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +6 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/backing-registration.d.ts.map +1 -1
- package/dist/runtime/backing-registration.js +7 -2
- package/dist/runtime/backing-registration.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +3 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +20 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +3 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +37 -8
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +3 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +2 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +18 -7
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -154
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DISTRIBUTION_ID_PATTERN =
|
|
5
|
+
/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,126}[a-zA-Z0-9])?$/;
|
|
6
|
+
|
|
7
|
+
export function assertCanonicalDistributionId(distributionId) {
|
|
8
|
+
if (
|
|
9
|
+
typeof distributionId !== 'string' ||
|
|
10
|
+
!DISTRIBUTION_ID_PATTERN.test(distributionId)
|
|
11
|
+
) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'distribution id must be a canonical 1-128 character identifier',
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return distributionId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertContained(root, candidate, label) {
|
|
20
|
+
const rel = relative(root, candidate);
|
|
21
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
22
|
+
throw new Error(`${label} escapes the distributions root`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveDistributionDirectory(
|
|
27
|
+
distributionsRoot,
|
|
28
|
+
distributionId,
|
|
29
|
+
) {
|
|
30
|
+
assertCanonicalDistributionId(distributionId);
|
|
31
|
+
const root = realpathSync(distributionsRoot);
|
|
32
|
+
const expectedDirectory = join(root, distributionId);
|
|
33
|
+
if (!existsSync(expectedDirectory)) {
|
|
34
|
+
throw new Error(`distribution "${distributionId}" does not exist`);
|
|
35
|
+
}
|
|
36
|
+
const directory = realpathSync(expectedDirectory);
|
|
37
|
+
assertContained(root, directory, `distribution "${distributionId}"`);
|
|
38
|
+
if (directory !== expectedDirectory) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`distribution "${distributionId}" directory must not be a symlink`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return { root, directory };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveDistributionFile(
|
|
47
|
+
distributionsRoot,
|
|
48
|
+
distributionId,
|
|
49
|
+
fileName,
|
|
50
|
+
) {
|
|
51
|
+
const { directory } = resolveDistributionDirectory(
|
|
52
|
+
distributionsRoot,
|
|
53
|
+
distributionId,
|
|
54
|
+
);
|
|
55
|
+
const expectedPath = join(directory, fileName);
|
|
56
|
+
if (!existsSync(expectedPath)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`distribution "${distributionId}" has no ${fileName} (${expectedPath})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const path = realpathSync(expectedPath);
|
|
62
|
+
assertContained(directory, path, `${fileName} for "${distributionId}"`);
|
|
63
|
+
if (path !== expectedPath) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`${fileName} for distribution "${distributionId}" must not be a symlink`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readBoundDistributionFile({
|
|
72
|
+
distributionsRoot,
|
|
73
|
+
distributionId,
|
|
74
|
+
fileName,
|
|
75
|
+
parse,
|
|
76
|
+
}) {
|
|
77
|
+
const path = resolveDistributionFile(
|
|
78
|
+
distributionsRoot,
|
|
79
|
+
distributionId,
|
|
80
|
+
fileName,
|
|
81
|
+
);
|
|
82
|
+
const value = parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
83
|
+
if (value.distributionId !== distributionId) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${fileName} distributionId "${value.distributionId}" does not match requested distribution "${distributionId}"`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { path, value };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Every edition under `distributionsRoot`, sorted — DISCOVERED, never listed.
|
|
93
|
+
*
|
|
94
|
+
* An edition is a directory carrying `xema-distribution.json`; that file is
|
|
95
|
+
* what makes the directory an edition, so nothing else can be mistaken for one
|
|
96
|
+
* and a new edition needs no registration anywhere.
|
|
97
|
+
*
|
|
98
|
+
* Throws on an empty scan. A gate that iterates editions and finds none must
|
|
99
|
+
* fail rather than report success over nothing — the repeated defect in this
|
|
100
|
+
* tree is a check that reports green while executing nothing, and a literal
|
|
101
|
+
* id list is the other half of it: a narrowed list reads as coverage.
|
|
102
|
+
*
|
|
103
|
+
* `refresh-distribution-plane.mjs` carried this three-line scan privately;
|
|
104
|
+
* it lives here so the write path and the read paths cannot disagree about
|
|
105
|
+
* what an edition is.
|
|
106
|
+
*/
|
|
107
|
+
export function discoverDistributionIds(distributionsRoot) {
|
|
108
|
+
const root = realpathSync(distributionsRoot);
|
|
109
|
+
const ids = readdirSync(root, { withFileTypes: true })
|
|
110
|
+
.filter((entry) => entry.isDirectory())
|
|
111
|
+
.map((entry) => entry.name)
|
|
112
|
+
.filter((name) => existsSync(join(root, name, 'xema-distribution.json')))
|
|
113
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
114
|
+
if (ids.length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`found no edition under ${root} — an empty scan is not a result`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function distributionOutputPath(
|
|
123
|
+
distributionsRoot,
|
|
124
|
+
distributionId,
|
|
125
|
+
fileName,
|
|
126
|
+
) {
|
|
127
|
+
const { directory } = resolveDistributionDirectory(
|
|
128
|
+
distributionsRoot,
|
|
129
|
+
distributionId,
|
|
130
|
+
);
|
|
131
|
+
const path = resolve(directory, fileName);
|
|
132
|
+
assertContained(directory, path, `${fileName} output`);
|
|
133
|
+
if (existsSync(path) && realpathSync(path) !== path) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`${fileName} output for distribution "${distributionId}" must not be a symlink`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return path;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The directory name holding editions that are NOT yet promoted.
|
|
143
|
+
*
|
|
144
|
+
* `discoverDistributionIds` is deliberately a single-level scan, so an edition
|
|
145
|
+
* one level down is invisible to it and to `dist:check` — which is the whole
|
|
146
|
+
* mechanism of the holding pen, not an oversight.
|
|
147
|
+
*/
|
|
148
|
+
export const PENDING_EDITIONS_DIRNAME = 'editions-pending-merge';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Every edition under `distributionsRoot`, in BOTH tiers, as
|
|
152
|
+
* `{ id, directory, pending }` — sorted, promoted tier first.
|
|
153
|
+
*
|
|
154
|
+
* WHY BOTH TIERS. A generic test suite must be able to name its subjects
|
|
155
|
+
* without naming a customer, and a customer's edition is exactly the thing that
|
|
156
|
+
* is either pending here or has moved to a root of its own. Discovering both
|
|
157
|
+
* means one expression covers the edition wherever it currently sits, so
|
|
158
|
+
* promoting it — or removing it — changes no test.
|
|
159
|
+
*
|
|
160
|
+
* THE EMPTY CASES ARE ASYMMETRIC ON PURPOSE.
|
|
161
|
+
*
|
|
162
|
+
* * The PROMOTED tier delegates to {@link discoverDistributionIds}, which
|
|
163
|
+
* THROWS on an empty scan. An empty promoted tier means the root is wrong,
|
|
164
|
+
* and a checker that iterated zero editions and reported success is this
|
|
165
|
+
* tree's most-repeated defect.
|
|
166
|
+
* * The PENDING tier may legitimately be empty — that is what a fully
|
|
167
|
+
* promoted fleet looks like — so it is not an error. A CALLER that narrows
|
|
168
|
+
* to a subset of these and finds nothing must say so out loud rather than
|
|
169
|
+
* pass in silence; that reporting duty is the caller's, because only the
|
|
170
|
+
* caller knows what it narrowed to.
|
|
171
|
+
*/
|
|
172
|
+
export function discoverEditionDirectories(distributionsRoot) {
|
|
173
|
+
const root = realpathSync(distributionsRoot);
|
|
174
|
+
const promoted = discoverDistributionIds(root).map((id) => ({
|
|
175
|
+
id,
|
|
176
|
+
directory: join(root, id),
|
|
177
|
+
pending: false,
|
|
178
|
+
}));
|
|
179
|
+
const pendingRoot = join(root, PENDING_EDITIONS_DIRNAME);
|
|
180
|
+
const pending = !existsSync(pendingRoot)
|
|
181
|
+
? []
|
|
182
|
+
: readdirSync(pendingRoot, { withFileTypes: true })
|
|
183
|
+
.filter((entry) => entry.isDirectory())
|
|
184
|
+
.map((entry) => entry.name)
|
|
185
|
+
.filter((name) =>
|
|
186
|
+
existsSync(join(pendingRoot, name, 'xema-distribution.json')),
|
|
187
|
+
)
|
|
188
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
189
|
+
.map((id) => ({ id, directory: join(pendingRoot, id), pending: true }));
|
|
190
|
+
return [...promoted, ...pending];
|
|
191
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// edition-artifact — the files of an edition and where they may go.
|
|
2
|
+
//
|
|
3
|
+
// ONE layout for develop and release: an OCI artifact at
|
|
4
|
+
// `<registry>/xema-edition-<id>`, addressed by digest, whose titled layers are
|
|
5
|
+
// `xema-edition.json` (the descriptor) plus the lock files. It is written to a
|
|
6
|
+
// scratch directory; the workflow pushes it with oras and signs the digest.
|
|
7
|
+
// Tags are for humans only (develop: the snapshot hash; release: the
|
|
8
|
+
// coordinate). Admission rests on the signature of the manifest digest, never on
|
|
9
|
+
// a channel label: no reader branches on `provenance.channel`.
|
|
10
|
+
//
|
|
11
|
+
// The artifact is never written to git, so the writer REFUSES a target inside
|
|
12
|
+
// either the data tree or the editions root.
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { basename, dirname, join, relative, resolve, sep, isAbsolute } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
18
|
+
import { EDITION_CHART_SET_FILE, assertRosteredChartSetLock } from './edition-chart-set.mjs';
|
|
19
|
+
import {
|
|
20
|
+
CATALOGUE_PIN_KEY,
|
|
21
|
+
EDITION_DESCRIPTOR_FILE,
|
|
22
|
+
EDITION_DESCRIPTOR_SCHEMA,
|
|
23
|
+
EDITION_LOCKS,
|
|
24
|
+
EDITION_REPOSITORY_PREFIX,
|
|
25
|
+
EditionChannel,
|
|
26
|
+
EditionLockFile,
|
|
27
|
+
EvidenceTrustState,
|
|
28
|
+
} from './edition-constants.mjs';
|
|
29
|
+
import { dataRoot, editionsRoot } from './edition-workspace.mjs';
|
|
30
|
+
import { serializeJson } from './lib.mjs';
|
|
31
|
+
import { snapshotHash, sortedSnapshotPins } from './pin-source.mjs';
|
|
32
|
+
import { readStrictJsonFile } from './strict-json.mjs';
|
|
33
|
+
|
|
34
|
+
/** Which GitHub Actions run produced the artifact; recorded in its descriptor. */
|
|
35
|
+
export function workflowFromEnv(env) {
|
|
36
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
37
|
+
const runId = env.GITHUB_RUN_ID;
|
|
38
|
+
if (!repository || !runId) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'an edition records the workflow run that produced it: GITHUB_REPOSITORY and GITHUB_RUN_ID are required ' +
|
|
41
|
+
'(pass --local to write outside GitHub Actions)',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return { repository, runId };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The workflow stamp of a write that is explicitly not from CI. */
|
|
48
|
+
export const LOCAL_WORKFLOW = Object.freeze({ repository: 'local', runId: 'local' });
|
|
49
|
+
|
|
50
|
+
const ENGINE_IMAGE_PATTERN = /^[a-z0-9.-]+(:[0-9]+)?(\/[a-z0-9._-]+)+@sha256:[a-f0-9]{64}$/;
|
|
51
|
+
|
|
52
|
+
/** The deploy engine image, by digest only: `<repository>@sha256:<64 hex>`. No tag form, no default. */
|
|
53
|
+
export function assertEngineImage(image) {
|
|
54
|
+
if (typeof image !== 'string' || !ENGINE_IMAGE_PATTERN.test(image)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`--engine-image ${JSON.stringify(image)} is not <repository>@sha256:<64 hex>: the deploy engine is pinned by digest, never by tag`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return image;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The descriptor: what this artifact is, which locks it carries, where it came
|
|
64
|
+
* from, whether the data root's editions are evidence-gated, and the deploy
|
|
65
|
+
* engine image that consumes it.
|
|
66
|
+
*/
|
|
67
|
+
export function editionDescriptor({ distributionId, channel, pins, workflow, evidenceTrustPolicy, engineImage }) {
|
|
68
|
+
assertCanonicalDistributionId(distributionId);
|
|
69
|
+
return parseEditionDescriptor({
|
|
70
|
+
schema: EDITION_DESCRIPTOR_SCHEMA,
|
|
71
|
+
distributionId,
|
|
72
|
+
locks: [...EDITION_LOCKS],
|
|
73
|
+
provenance: {
|
|
74
|
+
channel,
|
|
75
|
+
pins: channel === EditionChannel.Develop ? sortedSnapshotPins(pins) : { ...pins },
|
|
76
|
+
workflow: { repository: workflow?.repository, runId: workflow?.runId },
|
|
77
|
+
},
|
|
78
|
+
evidenceTrustPolicy,
|
|
79
|
+
engine: { image: engineImage },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const COMMIT_PATTERN = /^[0-9a-f]{40}$/;
|
|
84
|
+
const CATALOGUE_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
85
|
+
|
|
86
|
+
const exactKeys = (value, keys, where) => {
|
|
87
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join() !== [...keys].sort().join()) {
|
|
88
|
+
throw new Error(`${where} must be exactly {${keys.join(', ')}}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** The strict parse of a descriptor: exact keys, closed vocabularies, an engine image by digest. Returns it unchanged. */
|
|
93
|
+
export function parseEditionDescriptor(value) {
|
|
94
|
+
exactKeys(value, ['schema', 'distributionId', 'locks', 'provenance', 'evidenceTrustPolicy', 'engine'], EDITION_DESCRIPTOR_FILE);
|
|
95
|
+
if (value.schema !== EDITION_DESCRIPTOR_SCHEMA) {
|
|
96
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: schema is ${JSON.stringify(value.schema)}, expected ${JSON.stringify(EDITION_DESCRIPTOR_SCHEMA)}`);
|
|
97
|
+
}
|
|
98
|
+
assertCanonicalDistributionId(value.distributionId);
|
|
99
|
+
if (!Array.isArray(value.locks) || value.locks.length === 0 || value.locks.some((file) => typeof file !== 'string' || file === '')) {
|
|
100
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: locks must be a non-empty array of file names`);
|
|
101
|
+
}
|
|
102
|
+
exactKeys(value.provenance, ['channel', 'pins', 'workflow'], `${EDITION_DESCRIPTOR_FILE} provenance`);
|
|
103
|
+
if (!Object.values(EditionChannel).includes(value.provenance.channel)) {
|
|
104
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: provenance.channel must be one of ${Object.values(EditionChannel).join(' | ')}`);
|
|
105
|
+
}
|
|
106
|
+
const { pins, workflow } = value.provenance;
|
|
107
|
+
if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
|
|
108
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: provenance.pins must map pin keys to 40-hex commits (the "${CATALOGUE_PIN_KEY}" pin to a sha256 digest)`);
|
|
109
|
+
}
|
|
110
|
+
for (const [key, pin] of Object.entries(pins)) {
|
|
111
|
+
const pattern = key === CATALOGUE_PIN_KEY ? CATALOGUE_DIGEST_PATTERN : COMMIT_PATTERN;
|
|
112
|
+
if (typeof pin !== 'string' || !pattern.test(pin)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
key === CATALOGUE_PIN_KEY
|
|
115
|
+
? `${EDITION_DESCRIPTOR_FILE}: provenance.pins["${key}"] must be a sha256:<64 hex> digest`
|
|
116
|
+
: `${EDITION_DESCRIPTOR_FILE}: provenance.pins must map pin keys to 40-hex commits (${JSON.stringify(key)} is not)`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exactKeys(workflow, ['repository', 'runId'], `${EDITION_DESCRIPTOR_FILE} provenance.workflow`);
|
|
121
|
+
if (!workflow.repository || !workflow.runId || typeof workflow.repository !== 'string' || typeof workflow.runId !== 'string') {
|
|
122
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: provenance.workflow is {repository, runId}, both non-empty strings`);
|
|
123
|
+
}
|
|
124
|
+
if (!Object.values(EvidenceTrustState).includes(value.evidenceTrustPolicy)) {
|
|
125
|
+
throw new Error(`${EDITION_DESCRIPTOR_FILE}: evidenceTrustPolicy must be one of ${Object.values(EvidenceTrustState).join(' | ')}`);
|
|
126
|
+
}
|
|
127
|
+
exactKeys(value.engine, ['image'], `${EDITION_DESCRIPTOR_FILE} engine`);
|
|
128
|
+
assertEngineImage(value.engine.image);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const REGISTRY_PATTERN = /^[a-z0-9.-]+(:[0-9]+)?(\/[a-z0-9._-]+)+$/;
|
|
133
|
+
const TAG_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
|
|
134
|
+
|
|
135
|
+
/** The registry is the caller's namespace; the CLI appends `xema-edition-<id>` itself, so a registry already naming an edition repository is refused. */
|
|
136
|
+
export function assertRegistry(registry) {
|
|
137
|
+
if (typeof registry !== 'string' || !REGISTRY_PATTERN.test(registry)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`--registry ${JSON.stringify(registry)} is not <registry-host>[:port]/<namespace>[/...] (lowercase, e.g. registry.example.com/acme)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const last = registry.split('/').pop();
|
|
143
|
+
if (last.startsWith(EDITION_REPOSITORY_PREFIX)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`--registry ${registry} ends in "${last}", which the CLI appends itself: ` +
|
|
146
|
+
`the repository would be ${registry}/${EDITION_REPOSITORY_PREFIX}<id>. Pass the namespace only`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** `<registry>/xema-edition-<id>`: the one repository of an edition, for either channel. */
|
|
152
|
+
export function editionRepository(distributionId, registry) {
|
|
153
|
+
assertCanonicalDistributionId(distributionId);
|
|
154
|
+
assertRegistry(registry);
|
|
155
|
+
return `${registry}/${EDITION_REPOSITORY_PREFIX}${distributionId}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `<repository>:<tag>`; the tag is for humans, the artifact is addressed by its digest. */
|
|
159
|
+
export function editionTarget(distributionId, registry, tag) {
|
|
160
|
+
if (typeof tag !== 'string' || !TAG_PATTERN.test(tag)) {
|
|
161
|
+
throw new Error(`tag ${JSON.stringify(tag)} is not an OCI tag`);
|
|
162
|
+
}
|
|
163
|
+
return `${editionRepository(distributionId, registry)}:${tag}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A develop edition's target: the tag is its snapshot hash. */
|
|
167
|
+
export function developEditionTarget(distributionId, pins, { registry }) {
|
|
168
|
+
return editionTarget(distributionId, registry, snapshotHash(pins));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function realpathOfPlanned(path) {
|
|
172
|
+
const absolute = resolve(path);
|
|
173
|
+
let existing = absolute;
|
|
174
|
+
const rest = [];
|
|
175
|
+
while (!existsSync(existing)) {
|
|
176
|
+
rest.unshift(basename(existing));
|
|
177
|
+
existing = dirname(existing);
|
|
178
|
+
}
|
|
179
|
+
return join(realpathSync(existing), ...rest);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isWithin(path, directory) {
|
|
183
|
+
const inside = relative(realpathOfPlanned(directory), path);
|
|
184
|
+
return !(inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function assertOutsideCommittedTrees(outDir) {
|
|
188
|
+
const planned = realpathOfPlanned(outDir);
|
|
189
|
+
for (const [label, tree] of [
|
|
190
|
+
['the data tree', dataRoot()],
|
|
191
|
+
['the editions root', editionsRoot()],
|
|
192
|
+
]) {
|
|
193
|
+
if (isWithin(planned, tree)) {
|
|
194
|
+
throw new Error(`refusing to write an edition artifact to ${outDir}: it lies inside ${label} (${tree}), which is committed content`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Write the descriptor and the locks of an edition of `channel` to `outDir`; returns the descriptor. */
|
|
200
|
+
export function writeEditionArtifact({
|
|
201
|
+
outDir,
|
|
202
|
+
channel,
|
|
203
|
+
distributionId,
|
|
204
|
+
pins,
|
|
205
|
+
workflow,
|
|
206
|
+
evidenceTrustPolicy,
|
|
207
|
+
engineImage,
|
|
208
|
+
distributionLock,
|
|
209
|
+
imageLock,
|
|
210
|
+
authorizationLock,
|
|
211
|
+
chartSetLock,
|
|
212
|
+
}) {
|
|
213
|
+
assertOutsideCommittedTrees(outDir);
|
|
214
|
+
const byFile = [
|
|
215
|
+
[EditionLockFile.Distribution, distributionLock],
|
|
216
|
+
[EditionLockFile.Images, imageLock],
|
|
217
|
+
[EditionLockFile.Authorization, authorizationLock],
|
|
218
|
+
[EditionLockFile.ChartSet, chartSetLock],
|
|
219
|
+
];
|
|
220
|
+
for (const [file, lock] of byFile) {
|
|
221
|
+
if (lock === null || lock === undefined) {
|
|
222
|
+
throw new Error(`edition "${distributionId}" has no ${file}: an edition carries all four locks`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
assertRosteredChartSetLock(chartSetLock, EDITION_CHART_SET_FILE, channel);
|
|
226
|
+
const descriptor = editionDescriptor({ distributionId, channel, pins, workflow, evidenceTrustPolicy, engineImage });
|
|
227
|
+
mkdirSync(outDir, { recursive: true });
|
|
228
|
+
for (const [file, value] of [[EDITION_DESCRIPTOR_FILE, descriptor], ...byFile]) {
|
|
229
|
+
writeFileSync(join(outDir, file), serializeJson(value));
|
|
230
|
+
}
|
|
231
|
+
return descriptor;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* An existing snapshot-hash tag is reused only if it IS this snapshot: the tag
|
|
236
|
+
* is a hash of the pins, but a tag is mutable registry state, so the pulled
|
|
237
|
+
* descriptor must say the same edition, the same channel, exactly the same pins,
|
|
238
|
+
* the same engine image, the same evidence trust state and the same lock set. Throws naming the first difference.
|
|
239
|
+
*/
|
|
240
|
+
export function assertExistingArtifactMatches({ artifactDir, distributionId, channel, pins, engineImage, evidenceTrustPolicy }) {
|
|
241
|
+
const descriptorPath = join(artifactDir, EDITION_DESCRIPTOR_FILE);
|
|
242
|
+
if (!existsSync(descriptorPath)) {
|
|
243
|
+
throw new Error(`the existing artifact holds no ${EDITION_DESCRIPTOR_FILE}; refusing to reuse a tag that is not an edition`);
|
|
244
|
+
}
|
|
245
|
+
const existing = parseEditionDescriptor(readStrictJsonFile(descriptorPath));
|
|
246
|
+
assertCanonicalDistributionId(distributionId);
|
|
247
|
+
if (existing.distributionId !== distributionId) {
|
|
248
|
+
throw new Error(`the existing artifact's distributionId is ${JSON.stringify(existing.distributionId)}, expected ${JSON.stringify(distributionId)}; refusing to reuse it`);
|
|
249
|
+
}
|
|
250
|
+
if (existing.provenance.channel !== channel) {
|
|
251
|
+
throw new Error(`the existing artifact was produced on channel ${JSON.stringify(existing.provenance.channel)}, expected ${JSON.stringify(channel)}; refusing to reuse it`);
|
|
252
|
+
}
|
|
253
|
+
if (existing.evidenceTrustPolicy !== evidenceTrustPolicy) {
|
|
254
|
+
throw new Error(`the existing artifact records evidenceTrustPolicy ${JSON.stringify(existing.evidenceTrustPolicy)}, this data root's is ${JSON.stringify(evidenceTrustPolicy)}; refusing to reuse it`);
|
|
255
|
+
}
|
|
256
|
+
if (serializeJson(existing.provenance.pins) !== serializeJson(sortedSnapshotPins(pins))) {
|
|
257
|
+
throw new Error('the existing artifact was resolved from different pins than this snapshot (tag and content disagree); refusing to reuse it');
|
|
258
|
+
}
|
|
259
|
+
if (existing.engine.image !== assertEngineImage(engineImage)) {
|
|
260
|
+
throw new Error(`the existing artifact names engine ${existing.engine.image}, not ${engineImage}; refusing to reuse it`);
|
|
261
|
+
}
|
|
262
|
+
if (serializeJson(existing.locks) !== serializeJson(EDITION_LOCKS)) {
|
|
263
|
+
throw new Error('the existing artifact lists a different lock set than an edition carries; refusing to reuse it');
|
|
264
|
+
}
|
|
265
|
+
for (const file of existing.locks) {
|
|
266
|
+
if (!existsSync(join(artifactDir, file))) {
|
|
267
|
+
throw new Error(`the existing artifact lacks ${file}, which its descriptor lists; refusing to reuse it`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `xema edition biome-manifests <id> --json`: the PINNED xema-biome.json of every
|
|
3
|
+
// biome of one edition, as JSON.
|
|
4
|
+
//
|
|
5
|
+
// usage: xema edition biome-manifests <id> --json --data-root <dir> --editions-root <dir>
|
|
6
|
+
// --sources-root <dir> --source-hash <dir> --kernel <dir>
|
|
7
|
+
//
|
|
8
|
+
// For every biome of the edition's resolved lock this reads the manifest AT the
|
|
9
|
+
// commit the lock pins (the way the seal and `xema edition check` read it) and
|
|
10
|
+
// prints one document on stdout:
|
|
11
|
+
// {"schema":"xema.edition-biome-manifests/v1","distributionId","biomes":[{"id","repo","sourceCommit","manifest"}]}
|
|
12
|
+
// sorted by biome id. A consumer (the store catalogue) reads this instead of
|
|
13
|
+
// reading the sources itself. Read-only. When the lock is absent or a pinned
|
|
14
|
+
// manifest cannot be read it prints NOTHING on stdout, a message on stderr, and exits non-zero.
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
import { pinnedBiomeManifests } from './biome-content.mjs';
|
|
20
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
21
|
+
import {
|
|
22
|
+
WorkspaceRoot,
|
|
23
|
+
configureEditionWorkspaceFromArgv,
|
|
24
|
+
editionsRoot,
|
|
25
|
+
isEntryModule,
|
|
26
|
+
} from './edition-workspace.mjs';
|
|
27
|
+
import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
28
|
+
import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
|
|
29
|
+
import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
|
|
30
|
+
|
|
31
|
+
export const EDITION_BIOME_MANIFESTS_SCHEMA = 'xema.edition-biome-manifests/v1';
|
|
32
|
+
|
|
33
|
+
const USAGE =
|
|
34
|
+
'usage: xema edition biome-manifests <id> --json --data-root <dir> --editions-root <dir> --sources-root <dir> --source-hash <dir> --kernel <dir>';
|
|
35
|
+
|
|
36
|
+
function readLock(distributionId, file) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(join(editionsRoot(), distributionId, file), 'utf8'));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`edition "${distributionId}" has no readable ${file} under ${editionsRoot()}: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function editionBiomeManifests({ distributionId, schemas }) {
|
|
45
|
+
assertCanonicalDistributionId(distributionId);
|
|
46
|
+
const lock = schemas.parseDistributionLock(readLock(distributionId, 'distribution.lock.json'));
|
|
47
|
+
const imageLock = schemas.parseImageLock(readLock(distributionId, 'images.lock.json'));
|
|
48
|
+
const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
|
|
49
|
+
return {
|
|
50
|
+
schema: EDITION_BIOME_MANIFESTS_SCHEMA,
|
|
51
|
+
distributionId,
|
|
52
|
+
biomes: await pinnedBiomeManifests({
|
|
53
|
+
schemas,
|
|
54
|
+
lock,
|
|
55
|
+
imageLock,
|
|
56
|
+
locate: sourceTreeLocator(externalSourceTrees(lock)),
|
|
57
|
+
vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
|
|
64
|
+
WorkspaceRoot.Data,
|
|
65
|
+
WorkspaceRoot.Editions,
|
|
66
|
+
WorkspaceRoot.Sources,
|
|
67
|
+
WorkspaceRoot.SourceHash,
|
|
68
|
+
WorkspaceRoot.Kernel,
|
|
69
|
+
]);
|
|
70
|
+
const operands = rest.filter((arg) => arg !== '--json');
|
|
71
|
+
if (operands.length !== 1 || rest.length - operands.length !== 1) {
|
|
72
|
+
throw new Error(USAGE);
|
|
73
|
+
}
|
|
74
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
75
|
+
// Built whole before anything is written, so a refusal leaves stdout empty.
|
|
76
|
+
const document = await editionBiomeManifests({ distributionId: operands[0], schemas });
|
|
77
|
+
process.stdout.write(serializeJson(document));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isEntryModule(import.meta.url)) {
|
|
81
|
+
main().catch((error) => {
|
|
82
|
+
console.error(`[edition-biome-manifests] ${error.message}`);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// edition-channel — where an edition was produced. Its own module so the chart-set
|
|
2
|
+
// validator (which a channel selects) and edition-constants (which imports the
|
|
3
|
+
// chart-set file name) do not import each other.
|
|
4
|
+
|
|
5
|
+
/** Where an edition was produced. Recorded in the descriptor's provenance; readers never branch on it. */
|
|
6
|
+
export const EditionChannel = Object.freeze({ Develop: 'develop', Release: 'release' });
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// edition-chart-set.mjs — every edition carries the chart generation it is
|
|
2
|
+
// rendered with.
|
|
3
|
+
//
|
|
4
|
+
// Owner ruling, 2026-10-04: everything that affects a deployment's rendered
|
|
5
|
+
// runtime manifest is selected by its sealed edition — image digests, the
|
|
6
|
+
// authorization lock, AND the chart generation. Until then the chart set was one
|
|
7
|
+
// fleet-wide pin (chart-set.lock.json at this repository's root) that no edition
|
|
8
|
+
// carried, so xema-deploy rendered every deployment through whichever charts it
|
|
9
|
+
// happened to hold, and an appliance sealed on 2026-09-27 was rendered with
|
|
10
|
+
// charts that assume images built on 2026-10-01.
|
|
11
|
+
//
|
|
12
|
+
// So each edition directory holds `<id>/chart-set.lock.json`: a byte-identical
|
|
13
|
+
// copy of the fleet pin AT THE MOMENT IT IS WRITTEN, with each publisher's
|
|
14
|
+
// ROSTER (chart name → {version, digest}) read from the index, so a
|
|
15
|
+
// consumer selects every chart without asking the registry. It is part of the
|
|
16
|
+
// edition's file set (compute-edition-coordinates.sh), hashed into its
|
|
17
|
+
// coordinate and published with it, and xema-deploy vendors it beside the other
|
|
18
|
+
// halves.
|
|
19
|
+
//
|
|
20
|
+
// ONE FACT, STILL. The fleet pin stays the single answer to "which chart set do
|
|
21
|
+
// editions take now"; resolve-chart-set.mjs is its one writer and writes every
|
|
22
|
+
// edition's copy in the same run, and dist-check refuses a copy that is missing
|
|
23
|
+
// or differs from the pin. N copies of one fact cannot disagree when one writer
|
|
24
|
+
// writes them all and a check refuses any that does.
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
30
|
+
import { dataRoot } from './edition-workspace.mjs';
|
|
31
|
+
|
|
32
|
+
export const EDITION_CHART_SET_FILE = 'chart-set.lock.json';
|
|
33
|
+
/** The fleet pin: this repository's root chart-set.lock.json, beside this file in every layout. */
|
|
34
|
+
export const fleetChartSetLockPath = () => join(dataRoot(), EDITION_CHART_SET_FILE);
|
|
35
|
+
|
|
36
|
+
export const EditionChartSetCheck = Object.freeze({
|
|
37
|
+
GREEN: 'green',
|
|
38
|
+
MISSING: 'missing',
|
|
39
|
+
STALE: 'stale',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A published chart's version coordinate, per channel: `-h<16 hex>` (content
|
|
44
|
+
* hash) in a release chart set, `-dev.<12 hex>` (the frozen xema-deploy SHA) in a
|
|
45
|
+
* develop one. One lock SHAPE serves both; the coordinate is what the producer
|
|
46
|
+
* of each channel writes, so each producer validates its own channel's (this is
|
|
47
|
+
* a check of what is being written, not a branch of any reader).
|
|
48
|
+
*/
|
|
49
|
+
const CHART_VERSION_COORDINATE = Object.freeze({
|
|
50
|
+
[EditionChannel.Release]: /-h[0-9a-f]{16}$/,
|
|
51
|
+
[EditionChannel.Develop]: /-dev\.[0-9a-f]{12}$/,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** A chart-set lock (schemaVersion 3) that pins every member by version and OCI manifest digest, with `channel`'s version coordinate. */
|
|
55
|
+
export function assertRosteredChartSetLock(lock, label, channel) {
|
|
56
|
+
const coordinate = CHART_VERSION_COORDINATE[channel];
|
|
57
|
+
if (coordinate === undefined) {
|
|
58
|
+
throw new Error(`${label}: cannot validate a chart-set lock for channel ${JSON.stringify(channel)}; expected ${Object.values(EditionChannel).join(' | ')}`);
|
|
59
|
+
}
|
|
60
|
+
const fail = (why) => {
|
|
61
|
+
throw new Error(`${label} is not a rostered chart-set lock: ${why}`);
|
|
62
|
+
};
|
|
63
|
+
if (lock?.schemaVersion !== 3) {fail('schemaVersion must be 3');}
|
|
64
|
+
if (!Array.isArray(lock.publishers) || lock.publishers.length === 0) {fail('publishers must be a non-empty array');}
|
|
65
|
+
for (const pin of lock.publishers) {
|
|
66
|
+
if (!pin?.charts || typeof pin.charts !== 'object' || Array.isArray(pin.charts) || Object.keys(pin.charts).length === 0) {
|
|
67
|
+
fail(`${pin?.index ?? '?'} carries no roster (charts: name -> member version) — run resolve-chart-set.mjs`);
|
|
68
|
+
}
|
|
69
|
+
for (const [name, member] of Object.entries(pin.charts)) {
|
|
70
|
+
if (!member || typeof member !== 'object' || Array.isArray(member)) {fail(`${pin.index}: ${name} must be {version, digest}`);}
|
|
71
|
+
if (!coordinate.test(String(member.version))) {fail(`${pin.index}: ${name} has no ${channel} content coordinate (${member.version}); a ${channel} chart set carries ${coordinate}`);}
|
|
72
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(String(member.digest))) {fail(`${pin.index}: ${name} has no sha256 manifest digest`);}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lock;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write the RELEASE edition's copy of the fleet pin — byte-identical to
|
|
80
|
+
* `pinText`. Only a rostered pin (schemaVersion 3) with release chart
|
|
81
|
+
* coordinates is written.
|
|
82
|
+
*/
|
|
83
|
+
export function writeEditionChartSet(editionDir, pinText) {
|
|
84
|
+
assertRosteredChartSetLock(JSON.parse(pinText), EDITION_CHART_SET_FILE, EditionChannel.Release);
|
|
85
|
+
writeFileSync(join(editionDir, EDITION_CHART_SET_FILE), pinText);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Does the edition carry the fleet pin, byte for byte? */
|
|
89
|
+
export function checkEditionChartSet(editionDir, pinText) {
|
|
90
|
+
const file = join(editionDir, EDITION_CHART_SET_FILE);
|
|
91
|
+
if (!existsSync(file)) {return EditionChartSetCheck.MISSING;}
|
|
92
|
+
return readFileSync(file, 'utf8') === pinText ? EditionChartSetCheck.GREEN : EditionChartSetCheck.STALE;
|
|
93
|
+
}
|