@xemahq/edition-tooling 0.0.0-stage → 0.1.1
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/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -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-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `xema-edition ceiling`: the biomes a dev deployment may run, resolved at one distributions commit.
|
|
3
|
+
//
|
|
4
|
+
// usage: xema-edition ceiling --distribution <id> --at <distributions sha> --producer-commit <owner/repo>=<sha> [...]
|
|
5
|
+
// --data-root <dir> --sources-root <dir> --kernel <dir>
|
|
6
|
+
//
|
|
7
|
+
// stdout: a `DevelopCeilingSchema` document (`@xemahq/contracts/develop-ceiling`): schema, distributionsCommit,
|
|
8
|
+
// distributionId and biomes sorted by id, each { id, origin, version, repo, services, runtimeComponents,
|
|
9
|
+
// serviceProcessFacts, trustCeiling? }. No images and no seal.
|
|
10
|
+
//
|
|
11
|
+
// THE SELECTION is the distribution definition `<id>/xema-distribution.json` and the committed `biome-index.json`
|
|
12
|
+
// and `platform-service-index.json`, all read from the data root (the distributions checkout) AT `--at`, resolved
|
|
13
|
+
// by the release resolver itself (`composeManifest` + `resolveDistributionWithIndex`): id, origin, repo and
|
|
14
|
+
// trustCeiling of each biome are the resolution's, never re-derived here.
|
|
15
|
+
//
|
|
16
|
+
// THE PROCESS FACTS (version, services, runtimeComponents, serviceProcessFacts) are the producer's own: its
|
|
17
|
+
// `xema-biome.json` and generated `xema-biome.index.json` AT the producer commit given by `--producer-commit`,
|
|
18
|
+
// projected by `manifestToIndexEntry`, the function that builds `biome-index.json`. A service new on the
|
|
19
|
+
// producer's develop inside a selected biome is admitted; a biome the selection does not name is never read.
|
|
20
|
+
// A selected biome whose producer has no stated commit, or whose manifest is absent at that commit, is refused:
|
|
21
|
+
// there is no fallback to the committed index's facts.
|
|
22
|
+
|
|
23
|
+
import { DevelopCeilingSchema } from '@xemahq/contracts/develop-ceiling';
|
|
24
|
+
|
|
25
|
+
import { canonicalPublicHttpsRepositoryUrl, repositoryOwnerName } from './canonical-repository-url.mjs';
|
|
26
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
27
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
28
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
29
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromArgv, dataRoot, isEntryModule, sourcePath } from './edition-workspace.mjs';
|
|
30
|
+
import { readGitJsonAtCommit } from './git-json.mjs';
|
|
31
|
+
import {
|
|
32
|
+
BIOME_GENERATED_INDEX_FILENAME,
|
|
33
|
+
compareStrings,
|
|
34
|
+
loadKernelDistributionSchemas,
|
|
35
|
+
manifestToIndexEntry,
|
|
36
|
+
serializeJson,
|
|
37
|
+
} from './lib.mjs';
|
|
38
|
+
import { composeManifest, lockedServiceProcessFacts, parseEditionManifest, resolveDistributionWithIndex } from './resolve-distribution.mjs';
|
|
39
|
+
|
|
40
|
+
const USAGE =
|
|
41
|
+
'usage: xema-edition ceiling --distribution <id> --at <distributions sha> --producer-commit <owner/repo>=<sha> [--producer-commit ...] --data-root <dir> --sources-root <dir> --kernel <dir>';
|
|
42
|
+
const GIT_COMMIT = /^[0-9a-f]{40}$/;
|
|
43
|
+
const PRODUCER_COMMIT = /^([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)=([0-9a-f]{40})$/;
|
|
44
|
+
const MANIFEST_FILE = 'xema-biome.json';
|
|
45
|
+
|
|
46
|
+
/** The ceiling named a producer or a commit this verb cannot read: nothing is guessed. */
|
|
47
|
+
export class CeilingRefused extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = 'CeilingRefused';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `owner/name` -> the sources-root handle (`repos/<name>`) of the first-party repository it is, per the develop graph. */
|
|
55
|
+
function producersByHandle(members) {
|
|
56
|
+
return new Map(
|
|
57
|
+
members.map(({ repo, repositoryUrl }) => [
|
|
58
|
+
repo,
|
|
59
|
+
repositoryOwnerName(canonicalPublicHttpsRepositoryUrl(repositoryUrl, `firstPartySources["${repo}"].repositoryUrl`), `firstPartySources["${repo}"].repositoryUrl`),
|
|
60
|
+
]),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One selected biome's facts, read from its producer's manifest and ledger at the producer commit. */
|
|
65
|
+
async function producerFacts({ selected, producer, commit, repoDir }) {
|
|
66
|
+
const manifestDirectory = selected.path.slice(`${selected.repo}/`.length);
|
|
67
|
+
const manifestPath = `${manifestDirectory}/${MANIFEST_FILE}`;
|
|
68
|
+
const ledgerPath = `${manifestDirectory}/${BIOME_GENERATED_INDEX_FILENAME}`;
|
|
69
|
+
const rel = `${producer}/${manifestPath}`;
|
|
70
|
+
let raw;
|
|
71
|
+
let generatedIndex;
|
|
72
|
+
try {
|
|
73
|
+
raw = readGitJsonAtCommit(repoDir, manifestPath, commit);
|
|
74
|
+
generatedIndex = readGitJsonAtCommit(repoDir, ledgerPath, commit);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new CeilingRefused(`biome "${selected.id}": ${rel} or its ${BIOME_GENERATED_INDEX_FILENAME} cannot be read at ${producer}@${commit}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
const entry = await manifestToIndexEntry({ raw, generatedIndex, rel, repo: selected.repo, path: selected.path });
|
|
79
|
+
if (entry.id !== selected.id) {
|
|
80
|
+
throw new CeilingRefused(`${rel} at ${commit} declares biome "${entry.id}", but the selection names "${selected.id}" there`);
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {object} request
|
|
87
|
+
* @param {string} request.distributionId
|
|
88
|
+
* @param {string} request.distributionsCommit 40-hex commit of the data root checkout
|
|
89
|
+
* @param {ReadonlyMap<string, string>} request.producerCommits `owner/repo` -> the commit its facts are read at
|
|
90
|
+
* @param {object} request.schemas the kernel's distribution exports
|
|
91
|
+
* @returns {Promise<object>} the validated ceiling
|
|
92
|
+
*/
|
|
93
|
+
export async function composeCeiling({ distributionId, distributionsCommit, producerCommits, schemas }) {
|
|
94
|
+
assertCanonicalDistributionId(distributionId);
|
|
95
|
+
if (!GIT_COMMIT.test(distributionsCommit)) {throw new Error(`--at must be a 40-hex commit; got ${JSON.stringify(distributionsCommit)}`);}
|
|
96
|
+
const read = (path) => readGitJsonAtCommit(dataRoot(), path, distributionsCommit);
|
|
97
|
+
const manifest = parseEditionManifest(schemas.parseDistribution, distributionId, read(`${distributionId}/xema-distribution.json`), `${distributionsCommit}:${distributionId}/xema-distribution.json`);
|
|
98
|
+
const composed = composeManifest(manifest);
|
|
99
|
+
if (composed.declaredSources.length > 0) {
|
|
100
|
+
throw new CeilingRefused(`distribution "${distributionId}" declares external sources[]; a ceiling is resolved from the committed indexes alone`);
|
|
101
|
+
}
|
|
102
|
+
const { lock } = await resolveDistributionWithIndex(distributionId, {
|
|
103
|
+
channel: EditionChannel.Release,
|
|
104
|
+
inputs: {
|
|
105
|
+
composed,
|
|
106
|
+
biomeIndex: read('biome-index.json'),
|
|
107
|
+
platformServiceIndex: read('platform-service-index.json'),
|
|
108
|
+
sourcesAvailable: false,
|
|
109
|
+
storePublisherTrust: undefined,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const producers = producersByHandle(developGraphMembers());
|
|
113
|
+
const biomes = [];
|
|
114
|
+
for (const selected of lock.biomes) {
|
|
115
|
+
const producer = producers.get(selected.repo);
|
|
116
|
+
if (producer === undefined) {
|
|
117
|
+
throw new CeilingRefused(`biome "${selected.id}" is selected from "${selected.repo}", which is not a first-party repository of the develop graph`);
|
|
118
|
+
}
|
|
119
|
+
const commit = producerCommits.get(producer);
|
|
120
|
+
if (commit === undefined) {
|
|
121
|
+
throw new CeilingRefused(`biome "${selected.id}" is selected from ${producer}, but no --producer-commit states the commit its facts are read at`);
|
|
122
|
+
}
|
|
123
|
+
const entry = await producerFacts({ selected, producer, commit, repoDir: sourcePath(selected.repo) });
|
|
124
|
+
biomes.push({
|
|
125
|
+
id: selected.id,
|
|
126
|
+
origin: selected.origin,
|
|
127
|
+
version: entry.version,
|
|
128
|
+
repo: selected.repo,
|
|
129
|
+
services: entry.services,
|
|
130
|
+
runtimeComponents: entry.runtimeComponents,
|
|
131
|
+
serviceProcessFacts: lockedServiceProcessFacts(entry),
|
|
132
|
+
...(selected.trustCeiling === undefined ? {} : { trustCeiling: selected.trustCeiling }),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
biomes.sort((a, b) => compareStrings(a.id, b.id));
|
|
136
|
+
return DevelopCeilingSchema.parse({ schema: 1, distributionsCommit, distributionId, biomes });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `--distribution`, `--at` and the repeatable `--producer-commit <owner/repo>=<sha>` of the argv left after the root flags. */
|
|
140
|
+
export function parseCeilingArgs(argv) {
|
|
141
|
+
const options = { producerCommits: new Map() };
|
|
142
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
143
|
+
const flag = argv[i];
|
|
144
|
+
const value = argv[i + 1];
|
|
145
|
+
if (value === undefined || (value.startsWith('--') && value.length > 2)) {throw new Error(`${flag} requires a value\n${USAGE}`);}
|
|
146
|
+
if (flag === '--producer-commit') {
|
|
147
|
+
const match = PRODUCER_COMMIT.exec(value);
|
|
148
|
+
if (match === null) {throw new Error(`--producer-commit must be <owner/repo>=<40-hex commit>; got ${JSON.stringify(value)}`);}
|
|
149
|
+
if (options.producerCommits.has(match[1])) {throw new Error(`--producer-commit given twice for ${match[1]}`);}
|
|
150
|
+
options.producerCommits.set(match[1], match[2]);
|
|
151
|
+
} else if (flag === '--distribution' || flag === '--at') {
|
|
152
|
+
const key = flag.slice(2);
|
|
153
|
+
if (options[key] !== undefined) {throw new Error(`${flag} given twice`);}
|
|
154
|
+
options[key] = value;
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(`unknown argument ${flag}\n${USAGE}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const required of ['distribution', 'at']) {
|
|
160
|
+
if (options[required] === undefined) {throw new Error(`--${required} is required\n${USAGE}`);}
|
|
161
|
+
}
|
|
162
|
+
return options;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function main() {
|
|
166
|
+
const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Data, WorkspaceRoot.Sources, WorkspaceRoot.Kernel]);
|
|
167
|
+
const options = parseCeilingArgs(rest);
|
|
168
|
+
const ceiling = await composeCeiling({
|
|
169
|
+
distributionId: options.distribution,
|
|
170
|
+
distributionsCommit: options.at,
|
|
171
|
+
producerCommits: options.producerCommits,
|
|
172
|
+
schemas: await loadKernelDistributionSchemas(),
|
|
173
|
+
});
|
|
174
|
+
process.stdout.write(serializeJson(ceiling));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isEntryModule(import.meta.url)) {
|
|
178
|
+
main().catch((error) => {
|
|
179
|
+
console.error(`[edition-ceiling] ${error.message}`);
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Where an edition was produced. Recorded in the descriptor's provenance. verify-evidence selects its policy row by the
|
|
7
|
+
* descriptor's channel (a predicate channel mismatch is still refused after the signature check); lock readers require one.
|
|
8
|
+
*/
|
|
9
|
+
export const EditionChannel = Object.freeze({ Develop: 'develop', Release: 'release' });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The channel of `--channel <channel>`, which must be all of `rest`; anything
|
|
13
|
+
* else is a usage error. Shared by every verb that operates on one channel.
|
|
14
|
+
*/
|
|
15
|
+
export function parseChannelArgs(rest) {
|
|
16
|
+
if (rest.length !== 2 || rest[0] !== '--channel' || !Object.values(EditionChannel).includes(rest[1])) {
|
|
17
|
+
throw new Error(`--channel <${Object.values(EditionChannel).join('|')}> is required and nothing else is accepted (got ${rest.length === 0 ? 'nothing' : JSON.stringify(rest.join(' '))})`);
|
|
18
|
+
}
|
|
19
|
+
return { channel: rest[1] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `channel` is a stated `EditionChannel`; an internal function that needs a lane never defaults one. */
|
|
23
|
+
export function assertEditionChannel(channel) {
|
|
24
|
+
if (!Object.values(EditionChannel).includes(channel)) {
|
|
25
|
+
throw new Error(`an edition channel (${Object.values(EditionChannel).join('|')}) is required, got ${JSON.stringify(channel)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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, and the index chart's own manifest digest (`indexDigest`), 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 { dataRoot } from './edition-workspace.mjs';
|
|
30
|
+
|
|
31
|
+
export const EDITION_CHART_SET_FILE = 'chart-set.lock.json';
|
|
32
|
+
/** The fleet pin: this repository's root chart-set.lock.json, beside this file in every layout. */
|
|
33
|
+
export const fleetChartSetLockPath = () => join(dataRoot(), EDITION_CHART_SET_FILE);
|
|
34
|
+
|
|
35
|
+
export const EditionChartSetCheck = Object.freeze({
|
|
36
|
+
GREEN: 'green',
|
|
37
|
+
MISSING: 'missing',
|
|
38
|
+
STALE: 'stale',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** A published chart's version coordinate: `-h<16 hex>`, the content hash its publisher mints, on both channels (develop: `0.0.0-h<16 hex>`). */
|
|
42
|
+
const CHART_VERSION_COORDINATE = /-h[0-9a-f]{16}$/;
|
|
43
|
+
|
|
44
|
+
/** The chart-set lock schema every reader accepts; a lock of any other schema is re-resolved, never migrated. */
|
|
45
|
+
export const CHART_SET_LOCK_SCHEMA_VERSION = 4;
|
|
46
|
+
|
|
47
|
+
/** An OCI manifest digest: `sha256:` and 64 lowercase hex. */
|
|
48
|
+
export const OCI_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
49
|
+
|
|
50
|
+
/** The instruction every refusal of an outdated lock carries. */
|
|
51
|
+
export const RESOLVE_CHART_SET_INSTRUCTION = 're-resolve the chart set with `xema-edition chart-set resolve`';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A chart-set lock (schemaVersion 4) that pins every member by version and OCI
|
|
55
|
+
* manifest digest, with the `-h<16 hex>` content coordinate, and pins each
|
|
56
|
+
* publisher's index chart by `indexDigest` (always a sha256 digest).
|
|
57
|
+
*/
|
|
58
|
+
export function assertRosteredChartSetLock(lock, label) {
|
|
59
|
+
const fail = (why) => {
|
|
60
|
+
throw new Error(`${label} is not a rostered chart-set lock: ${why}`);
|
|
61
|
+
};
|
|
62
|
+
if (lock?.schemaVersion !== CHART_SET_LOCK_SCHEMA_VERSION) {
|
|
63
|
+
fail(`schemaVersion must be ${CHART_SET_LOCK_SCHEMA_VERSION} (got ${JSON.stringify(lock?.schemaVersion)}); ${RESOLVE_CHART_SET_INSTRUCTION}`);
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(lock.publishers) || lock.publishers.length === 0) {fail('publishers must be a non-empty array');}
|
|
66
|
+
for (const [position, pin] of lock.publishers.entries()) {
|
|
67
|
+
if (!pin?.charts || typeof pin.charts !== 'object' || Array.isArray(pin.charts) || Object.keys(pin.charts).length === 0) {
|
|
68
|
+
fail(`${pin?.index ?? '?'} carries no roster (charts: name -> member version) — run resolve-chart-set.mjs`);
|
|
69
|
+
}
|
|
70
|
+
for (const [name, member] of Object.entries(pin.charts)) {
|
|
71
|
+
if (!member || typeof member !== 'object' || Array.isArray(member)) {fail(`${pin.index}: ${name} must be {version, digest}`);}
|
|
72
|
+
if (!CHART_VERSION_COORDINATE.test(String(member.version))) {fail(`${pin.index}: ${name} has no content coordinate (${member.version}); a chart set carries ${CHART_VERSION_COORDINATE}`);}
|
|
73
|
+
if (!OCI_DIGEST_PATTERN.test(String(member.digest))) {fail(`${pin.index}: ${name} has no sha256 manifest digest`);}
|
|
74
|
+
}
|
|
75
|
+
assertIndexDigest(pin, position, fail);
|
|
76
|
+
}
|
|
77
|
+
return lock;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The `indexDigest` rule: the key is present and is a sha256 digest. */
|
|
81
|
+
function assertIndexDigest(pin, position, fail) {
|
|
82
|
+
const where = `publisher ${position} (${pin?.index ?? '?'})`;
|
|
83
|
+
if (!pin || typeof pin !== 'object' || !Object.hasOwn(pin, 'indexDigest')) {
|
|
84
|
+
fail(`${where} carries no indexDigest key (required: "sha256:<64 hex>")`);
|
|
85
|
+
}
|
|
86
|
+
if (!OCI_DIGEST_PATTERN.test(String(pin.indexDigest))) {fail(`${where} has a malformed indexDigest (${JSON.stringify(pin.indexDigest)}); expected "sha256:<64 lowercase hex>"`);}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Write the RELEASE edition's copy of the fleet pin — byte-identical to
|
|
91
|
+
* `pinText`. Only a rostered pin (schemaVersion 4) with release chart
|
|
92
|
+
* coordinates is written.
|
|
93
|
+
*/
|
|
94
|
+
export function writeEditionChartSet(editionDir, pinText) {
|
|
95
|
+
assertRosteredChartSetLock(JSON.parse(pinText), EDITION_CHART_SET_FILE);
|
|
96
|
+
writeFileSync(join(editionDir, EDITION_CHART_SET_FILE), pinText);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Does the edition carry the fleet pin, byte for byte? */
|
|
100
|
+
export function checkEditionChartSet(editionDir, pinText) {
|
|
101
|
+
const file = join(editionDir, EDITION_CHART_SET_FILE);
|
|
102
|
+
if (!existsSync(file)) {return EditionChartSetCheck.MISSING;}
|
|
103
|
+
return readFileSync(file, 'utf8') === pinText ? EditionChartSetCheck.GREEN : EditionChartSetCheck.STALE;
|
|
104
|
+
}
|