@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,89 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// A REMEDY THIS REPOSITORY CAN ACTUALLY RUN.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
//
|
|
6
|
+
// This repository's `package.json` has NO `scripts` block. Every `pnpm
|
|
7
|
+
// dist:*` command is declared one directory up, in the AGGREGATOR's
|
|
8
|
+
// `package.json`, as a bare `node repos-infra/xema-distributions/<file>.mjs`.
|
|
9
|
+
// So a diagnostic printing `pnpm dist:images <id>` names a command its reader
|
|
10
|
+
// cannot run — and the reader of a dist-check failure is standing in a FLAT
|
|
11
|
+
// checkout of this repository, which is exactly where the pnpm spelling does
|
|
12
|
+
// not resolve. A remedy that cannot be run where it is printed is a dead end
|
|
13
|
+
// (`.claude/rules/writing-boundary-checks.md`, and closure-ledger A25).
|
|
14
|
+
//
|
|
15
|
+
// Measured 2026-09-28 at `origin/develop`: 52 `pnpm dist:*` references in
|
|
16
|
+
// this repository, 19 such scripts declared in the aggregator, and ZERO
|
|
17
|
+
// declared here.
|
|
18
|
+
//
|
|
19
|
+
// WHY NOT A SECOND `scripts` BLOCK
|
|
20
|
+
//
|
|
21
|
+
// That was the obvious fix and it is the wrong one: it would put 19 name→file
|
|
22
|
+
// mappings in two places, free to drift, and drift between them is invisible
|
|
23
|
+
// — a new script reachable from one side and not the other, with nothing able
|
|
24
|
+
// to say so short of a new gate. Closure-ledger A26 forbids buying a fix with
|
|
25
|
+
// a permanent check.
|
|
26
|
+
//
|
|
27
|
+
// Deriving the command from the script's REAL PATH has one authority — the
|
|
28
|
+
// file system — and cannot go stale: rename the file and the command follows,
|
|
29
|
+
// because it IS the file. This is the treatment `seal-image-digests.mjs`
|
|
30
|
+
// already chose for itself and argued for in its own words; it is extracted
|
|
31
|
+
// here so the other ten diagnostics get it too rather than each re-deriving
|
|
32
|
+
// it, and so `sealCommandFor` stops being the only site that is right.
|
|
33
|
+
//
|
|
34
|
+
// WHAT IT DOES NOT DO
|
|
35
|
+
//
|
|
36
|
+
// It does not replace the aggregator's `pnpm dist:*` spelling, which is real,
|
|
37
|
+
// is what CI invokes, and is correct FROM THE AGGREGATOR. Documentation may
|
|
38
|
+
// name either; a RUNTIME diagnostic names this one, because the process
|
|
39
|
+
// printing it already knows where it is standing and the reader does not.
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
44
|
+
|
|
45
|
+
/** This module's directory — the distributions root in both layouts the fleet runs. */
|
|
46
|
+
const ROOT = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render an absolute script path as the SHORTEST form that still resolves from
|
|
50
|
+
* the caller's current directory.
|
|
51
|
+
*
|
|
52
|
+
* A relative path is preferred when the script is at or below `process.cwd()`
|
|
53
|
+
* — that is the form an operator can paste — and the absolute path is used
|
|
54
|
+
* otherwise, because a `../../..` chain is longer, harder to read, and no more
|
|
55
|
+
* runnable. Escaping to a parent (`..`) is never shown for that reason.
|
|
56
|
+
*/
|
|
57
|
+
function shortestRunnablePath(script) {
|
|
58
|
+
const fromCwd = relative(process.cwd(), script);
|
|
59
|
+
return !fromCwd.startsWith('..') && fromCwd.length < script.length
|
|
60
|
+
? fromCwd
|
|
61
|
+
: script;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The command that runs `scriptFileName` — a sibling of this module — with
|
|
66
|
+
* `args`, spelled so the reader can run it from wherever they are standing.
|
|
67
|
+
*
|
|
68
|
+
* Takes a FILE NAME rather than a script name on purpose: a file name is
|
|
69
|
+
* checked by the file system the moment anybody runs the remedy, whereas a
|
|
70
|
+
* `dist:*` name is checked by nothing and is what went wrong here.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} scriptFileName e.g. `'resolve-distribution.mjs'`
|
|
73
|
+
* @param {...string} args positional arguments, appended verbatim
|
|
74
|
+
*/
|
|
75
|
+
export function commandFor(scriptFileName, ...args) {
|
|
76
|
+
const shown = shortestRunnablePath(resolve(ROOT, scriptFileName));
|
|
77
|
+
return ['node', shown, ...args].join(' ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The command that runs the CALLING module itself.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} importMetaUrl the caller's `import.meta.url`
|
|
84
|
+
* @param {...string} args positional arguments, appended verbatim
|
|
85
|
+
*/
|
|
86
|
+
export function commandForSelf(importMetaUrl, ...args) {
|
|
87
|
+
const shown = shortestRunnablePath(fileURLToPath(importMetaUrl));
|
|
88
|
+
return ['node', shown, ...args].join(' ');
|
|
89
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `xema edition repin-evidence-trust` - write the data root's evidence trust
|
|
3
|
+
// pin: the digest of the declared policy, the digests of THIS CLI's built
|
|
4
|
+
// verifier bundle and provenance, the kernel contract authority the data
|
|
5
|
+
// root's runtime image source index states, and the oras binaries that may
|
|
6
|
+
// read a registry. The result is a data-root change to review; nothing here is
|
|
7
|
+
// generated into the library.
|
|
8
|
+
//
|
|
9
|
+
// [--trusted-oras-digest sha256:<64 hex>]… the oras binaries to trust. Without
|
|
10
|
+
// the flag the list of the existing pin is kept; a first pin requires it.
|
|
11
|
+
//
|
|
12
|
+
// Refuses when the data root declares no evidenceTrustPolicy.
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { locateBuiltVerifier, readBuiltVerifierDigests } from './built-verifier.mjs';
|
|
18
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
19
|
+
import {
|
|
20
|
+
WorkspaceRoot,
|
|
21
|
+
configureEditionWorkspaceFromProcess,
|
|
22
|
+
dataRoot,
|
|
23
|
+
isEntryModule,
|
|
24
|
+
} from './edition-workspace.mjs';
|
|
25
|
+
import {
|
|
26
|
+
loadDeclaredTrustPolicy,
|
|
27
|
+
readEvidenceTrustPin,
|
|
28
|
+
readStatedKernelContractAuthority,
|
|
29
|
+
requireOrasDigests,
|
|
30
|
+
writeEvidenceTrustPin,
|
|
31
|
+
} from './evidence-trust-pin.mjs';
|
|
32
|
+
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write the pin for the configured data root and return it.
|
|
36
|
+
* `locateVerifier` is a parameter so a test can supply a fixture bundle.
|
|
37
|
+
*/
|
|
38
|
+
export async function repinEvidenceTrust({
|
|
39
|
+
locateVerifier = locateBuiltVerifier,
|
|
40
|
+
trustedOrasBinaryDigests = undefined,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const root = dataRoot();
|
|
43
|
+
const declaration = editionWorkspaceConfig().evidenceTrustPolicy;
|
|
44
|
+
if (declaration.policy === null) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${root} declares no evidenceTrustPolicy in edition-workspace.json; there is nothing to pin`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
50
|
+
const { policyDigest } = loadDeclaredTrustPolicy({ dataRoot: root, declaration, schemas });
|
|
51
|
+
const verifier = readBuiltVerifierDigests(locateVerifier);
|
|
52
|
+
let orasDigests = trustedOrasBinaryDigests;
|
|
53
|
+
if (orasDigests === undefined) {
|
|
54
|
+
if (!existsSync(join(root, declaration.pin))) {
|
|
55
|
+
// Names the flag: a first pin has no list to keep.
|
|
56
|
+
requireOrasDigests([]);
|
|
57
|
+
}
|
|
58
|
+
orasDigests = readEvidenceTrustPin({ dataRoot: root, declaration }).trustedOrasBinaryDigests;
|
|
59
|
+
}
|
|
60
|
+
return writeEvidenceTrustPin({
|
|
61
|
+
dataRoot: root,
|
|
62
|
+
declaration,
|
|
63
|
+
policyDigest,
|
|
64
|
+
verifier,
|
|
65
|
+
kernelContractAuthority: readStatedKernelContractAuthority({ dataRoot: root, schemas }),
|
|
66
|
+
trustedOrasBinaryDigests: orasDigests,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `--trusted-oras-digest <digest>` and `--trusted-oras-digest=<digest>`, repeated; nothing else is an argument. */
|
|
71
|
+
export function parseRepinArguments(argv) {
|
|
72
|
+
const digests = [];
|
|
73
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
74
|
+
const argument = argv[index];
|
|
75
|
+
if (argument === '--trusted-oras-digest') {
|
|
76
|
+
index += 1;
|
|
77
|
+
if (argv[index] === undefined) {throw new Error('--trusted-oras-digest needs a value');}
|
|
78
|
+
digests.push(argv[index]);
|
|
79
|
+
} else if (argument.startsWith('--trusted-oras-digest=')) {
|
|
80
|
+
digests.push(argument.slice('--trusted-oras-digest='.length));
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(`unknown argument "${argument}"; usage: xema edition repin-evidence-trust [--trusted-oras-digest sha256:<64 hex>]…`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { trustedOrasBinaryDigests: digests.length === 0 ? undefined : digests };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isEntryModule(import.meta.url)) {
|
|
89
|
+
try {
|
|
90
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
|
|
91
|
+
const pin = await repinEvidenceTrust(parseRepinArguments(process.argv.slice(2)));
|
|
92
|
+
console.log(
|
|
93
|
+
`[repin-evidence-trust] pinned policy ${pin.policyDigest}, verifier ${pin.verifier.bundleDigest}, ` +
|
|
94
|
+
`kernel contract authority ${pin.kernelContractAuthority.sourceCommit} and ` +
|
|
95
|
+
`${pin.trustedOrasBinaryDigests.length} oras binary digest(s)`,
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(`[repin-evidence-trust] ${error.message}`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// A required resource requirement is a closure edge.
|
|
2
|
+
//
|
|
3
|
+
// A biome service that declares `inclusion: required` for a resource kind
|
|
4
|
+
// (postgres.schema, redis.namespace, ...) cannot run without a provider of that
|
|
5
|
+
// kind, exactly as it cannot run without a biome it `requires`. So the
|
|
6
|
+
// resolver follows the edge to the provider and ships it; naming the consumer
|
|
7
|
+
// is enough, no edition lists a provider by hand.
|
|
8
|
+
//
|
|
9
|
+
// The edge resolves only when exactly one deliverable biome provides the kind.
|
|
10
|
+
// Zero providers or several are refused with a typed error: with none the
|
|
11
|
+
// consumer can never boot, and with several no provider-selection policy
|
|
12
|
+
// exists, so choosing one would be a guess made on the consumer's behalf.
|
|
13
|
+
|
|
14
|
+
export const RequiredResourceProviderFailure = Object.freeze({
|
|
15
|
+
NoProvider: 'no-provider',
|
|
16
|
+
AmbiguousProvider: 'ambiguous-provider',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export class RequiredResourceProviderError extends Error {
|
|
20
|
+
constructor(failure, { distributionId, consumerId, requirement, providerIds }) {
|
|
21
|
+
const subject =
|
|
22
|
+
`biome "${consumerId}" service "${requirement.consumerService}" requires ` +
|
|
23
|
+
`${requirement.kind} "${requirement.logicalName}" (${requirement.inclusion})`;
|
|
24
|
+
super(
|
|
25
|
+
failure === RequiredResourceProviderFailure.NoProvider
|
|
26
|
+
? `distribution "${distributionId}": ${subject}, and no biome in the index provides ` +
|
|
27
|
+
`${requirement.kind}, so the consumer cannot run in any edition.`
|
|
28
|
+
: `distribution "${distributionId}": ${subject}, and ${providerIds.length} biomes provide ` +
|
|
29
|
+
`that kind: ${providerIds.join(', ')}. No provider-selection policy exists, so the ` +
|
|
30
|
+
`resolver refuses rather than choosing one.`,
|
|
31
|
+
);
|
|
32
|
+
this.name = 'RequiredResourceProviderError';
|
|
33
|
+
this.failure = failure;
|
|
34
|
+
this.distributionId = distributionId;
|
|
35
|
+
this.consumerId = consumerId;
|
|
36
|
+
this.requirement = requirement;
|
|
37
|
+
this.providerIds = providerIds;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The biome entry that is the sole provider of the requirement's kind among
|
|
43
|
+
* `candidates` (the deliverable, source-admissible index entries).
|
|
44
|
+
*/
|
|
45
|
+
export function soleResourceProvider({ distributionId, consumer, requirement, candidates }) {
|
|
46
|
+
const providers = candidates.filter((entry) =>
|
|
47
|
+
(entry.providesResources ?? []).some((provision) => provision.kind === requirement.kind),
|
|
48
|
+
);
|
|
49
|
+
const providerIds = [...new Set(providers.map((entry) => entry.id))].sort();
|
|
50
|
+
const context = { distributionId, consumerId: consumer.id, requirement, providerIds };
|
|
51
|
+
if (providers.length === 0) {
|
|
52
|
+
throw new RequiredResourceProviderError(RequiredResourceProviderFailure.NoProvider, context);
|
|
53
|
+
}
|
|
54
|
+
if (providerIds.length > 1) {
|
|
55
|
+
throw new RequiredResourceProviderError(RequiredResourceProviderFailure.AmbiguousProvider, context);
|
|
56
|
+
}
|
|
57
|
+
return providers[0];
|
|
58
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// resolve-chart-set — pin the Helm chart-set version an install resolves, so
|
|
3
|
+
// that no human has to.
|
|
4
|
+
//
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// WHY THIS EXISTS
|
|
7
|
+
//
|
|
8
|
+
// `xema-appliance provision` pulls charts from the configured chart registry
|
|
9
|
+
// at an explicitly pinned `XEMA_CHART_VERSION`, and REFUSES to run without
|
|
10
|
+
// one ("refusing to pull a floating chart version"). Nothing set it. The
|
|
11
|
+
// value was typed by an operator, which made every platform update a step
|
|
12
|
+
// somebody had to remember and get right — exactly the maintenance surface
|
|
13
|
+
// the images plane already removed for itself with images.lock.json.
|
|
14
|
+
//
|
|
15
|
+
// This resolves it instead, and writes the answer to a lock the appliance
|
|
16
|
+
// reads. The env var survives as an override, never as the only source.
|
|
17
|
+
//
|
|
18
|
+
// WHY IT IS RESOLVED AND NOT COMPUTED
|
|
19
|
+
//
|
|
20
|
+
// The version cannot be derived from this repository: it is minted by the two
|
|
21
|
+
// publish-charts workflows (xema-deploy and xema-infra-services) at publish
|
|
22
|
+
// time. So the only truthful way to pin it is to ask the registry which
|
|
23
|
+
// versions actually exist, and that is what this does.
|
|
24
|
+
//
|
|
25
|
+
// EACH PUBLISHER IS PINNED INDEPENDENTLY
|
|
26
|
+
//
|
|
27
|
+
// One OCI namespace, two publishers, and an install resolves charts from
|
|
28
|
+
// BOTH — a deployments/*/infra-plan.json names components from each in the
|
|
29
|
+
// same list. Each closes its own half with an index chart, written only after
|
|
30
|
+
// every chart of that half is pushed, signed and proved served.
|
|
31
|
+
//
|
|
32
|
+
// They used to be pinned by ONE shared version, which meant both had to mint
|
|
33
|
+
// the same string, which meant deriving it from the only input they could both
|
|
34
|
+
// reproduce — the UTC date — which capped publishing at once a day. The shared
|
|
35
|
+
// pin was the coupling and the day of latency was its price. Each half is now
|
|
36
|
+
// pinned separately: a publisher that has not run does not hold the other back.
|
|
37
|
+
//
|
|
38
|
+
// WHICH VERSION IS CURRENT, WHEN COORDINATES DO NOT SORT
|
|
39
|
+
//
|
|
40
|
+
// Index versions are content-derived (`0.0.0-h<hash>`), so semver precedence
|
|
41
|
+
// says nothing about which is newest — hashes have no order. The registry
|
|
42
|
+
// does: `helm push` stamps `org.opencontainers.image.created` into the
|
|
43
|
+
// manifest, so the current index is the one most recently pushed, read from
|
|
44
|
+
// the registry rather than guessed from the tag.
|
|
45
|
+
//
|
|
46
|
+
// Deliberately NOT a moving `latest` tag. Helm derives the OCI tag from the
|
|
47
|
+
// chart version and will not push a non-semver one, and a moving pointer is
|
|
48
|
+
// exactly what this plane's image rules refuse — a late run can reach it after
|
|
49
|
+
// a newer one moved it. Every coordinate here stays immutable.
|
|
50
|
+
//
|
|
51
|
+
// FAIL-CLOSED
|
|
52
|
+
//
|
|
53
|
+
// No eligible version is an ERROR, never an empty or stale pin. A lock that
|
|
54
|
+
// silently kept yesterday's value because today's read failed is the exact
|
|
55
|
+
// shape this plane has been bitten by: a claim that outlives its evidence.
|
|
56
|
+
|
|
57
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
58
|
+
import { basename, join } from 'node:path';
|
|
59
|
+
|
|
60
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
61
|
+
import {
|
|
62
|
+
EDITION_CHART_SET_FILE,
|
|
63
|
+
EditionChartSetCheck,
|
|
64
|
+
assertRosteredChartSetLock,
|
|
65
|
+
checkEditionChartSet,
|
|
66
|
+
writeEditionChartSet,
|
|
67
|
+
} from './edition-chart-set.mjs';
|
|
68
|
+
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
69
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, dataRoot, editionsRoot, isEntryModule } from './edition-workspace.mjs';
|
|
70
|
+
import { serializeJson } from './lib.mjs';
|
|
71
|
+
import { OrasRegistry } from './oras.mjs';
|
|
72
|
+
import { deployPinKey } from './pin-source.mjs';
|
|
73
|
+
import {
|
|
74
|
+
loadRegistryCredentialDeclarations,
|
|
75
|
+
resolveRegistryCredential,
|
|
76
|
+
} from './registry-credentials.mjs';
|
|
77
|
+
|
|
78
|
+
if (isEntryModule(import.meta.url)) {
|
|
79
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Deploy, WorkspaceRoot.Kernel]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const CHART_SET_LOCK_FILE = 'chart-set.lock.json';
|
|
83
|
+
export const chartSetLockPath = () => join(dataRoot(), CHART_SET_LOCK_FILE);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The chart registry and its publishers.
|
|
87
|
+
*
|
|
88
|
+
* ONE PIN for every edition, and EVERY EDITION CARRIES IT. The set that
|
|
89
|
+
* satisfies the widest edition satisfies the narrowest, so there is one answer
|
|
90
|
+
* to "which chart set do editions take now" — this lock. But the chart
|
|
91
|
+
* generation a deployment renders is part of its edition (owner ruling
|
|
92
|
+
* 2026-10-04), so each edition holds a byte-identical copy as
|
|
93
|
+
* `<id>/chart-set.lock.json`, hashed into its coordinate and published with it.
|
|
94
|
+
* This script is the one writer of the pin AND every copy, and dist-check
|
|
95
|
+
* refuses a copy that differs — so the copies cannot disagree
|
|
96
|
+
* (edition-chart-set.mjs).
|
|
97
|
+
*/
|
|
98
|
+
const chartRegistry = () => editionWorkspaceConfig().chartRegistry;
|
|
99
|
+
const chartSetPublishers = () => editionWorkspaceConfig().chartPublishers;
|
|
100
|
+
|
|
101
|
+
/** `<host>/<path>` -> the repository path the registry API addresses. */
|
|
102
|
+
function registryRepository(indexName) {
|
|
103
|
+
return `${chartRegistry().replace(/^oci:\/\/[^/]+\//, '')}/${indexName}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function registryAuthority() {
|
|
107
|
+
return chartRegistry().replace(/^oci:\/\//, '').split('/')[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* cosign publishes a signature as a TAG (`sha256-<digest>.sig`) beside the
|
|
112
|
+
* artifact it signs. Those are not versions and must never be pinned.
|
|
113
|
+
*/
|
|
114
|
+
export function isVersionTag(tag) {
|
|
115
|
+
return !tag.startsWith('sha256-');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The newest index version for ONE publisher, by registry creation time.
|
|
120
|
+
* `candidates` is `[{ tag, createdAt }]`; returns the tag or null.
|
|
121
|
+
*
|
|
122
|
+
* A tag whose manifest carries no creation stamp is REFUSED rather than sorted
|
|
123
|
+
* last: guessing an order for a coordinate whose age is unknown is how a stale
|
|
124
|
+
* set gets pinned as current.
|
|
125
|
+
*/
|
|
126
|
+
export function selectNewestIndex(allCandidates) {
|
|
127
|
+
if (!Array.isArray(allCandidates)) {return null;}
|
|
128
|
+
// A develop chart version (`0.0.0-dev.<sha12>`) is a frozen xema-deploy
|
|
129
|
+
// snapshot, never a release index: it is not a candidate for the release pin.
|
|
130
|
+
const candidates = allCandidates.filter((c) => !c.tag.startsWith(DEVELOP_CHART_VERSION_PREFIX));
|
|
131
|
+
if (candidates.length === 0) {return null;}
|
|
132
|
+
const undated = candidates.filter((c) => !c.createdAt);
|
|
133
|
+
if (undated.length > 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`index version(s) ${undated.map((c) => c.tag).join(', ')} carry no ` +
|
|
136
|
+
'org.opencontainers.image.created stamp, so "which is current" cannot be answered. ' +
|
|
137
|
+
'Refusing to guess.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return [...candidates].sort(
|
|
141
|
+
(a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt),
|
|
142
|
+
)[0].tag;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
146
|
+
|
|
147
|
+
/** The index chart annotation listing the charts the publisher closed the set with. */
|
|
148
|
+
export const INDEX_ROSTER_ANNOTATION = 'xema.dev/chart-set-charts';
|
|
149
|
+
|
|
150
|
+
export function buildLock(versionsByIndex, rostersByIndex, publishers = chartSetPublishers()) {
|
|
151
|
+
// Every publisher's VERSION first: a pin that resolved nothing for one half is
|
|
152
|
+
// the more fundamental refusal, whatever its roster.
|
|
153
|
+
for (const publisher of publishers) {
|
|
154
|
+
if (!versionsByIndex[publisher.index]) {
|
|
155
|
+
throw new Error(`no index version was resolved for '${publisher.index}'`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
// 3: every member is `{version, digest}` (its OCI manifest digest), so the
|
|
160
|
+
// lock itself is the trust anchor. 2 added `publishers[].version` (one each)
|
|
161
|
+
// in place of one shared version, which would couple the two halves.
|
|
162
|
+
schemaVersion: 3,
|
|
163
|
+
registry: chartRegistry(),
|
|
164
|
+
publishers: publishers.map((publisher) => {
|
|
165
|
+
const version = versionsByIndex[publisher.index];
|
|
166
|
+
if (!version) {
|
|
167
|
+
throw new Error(`no index version was resolved for '${publisher.index}'`);
|
|
168
|
+
}
|
|
169
|
+
// THE ROSTER, sealed: chart name → {version, digest}, so an edition that
|
|
170
|
+
// copies this lock selects and verifies every chart without the registry.
|
|
171
|
+
const roster = rostersByIndex?.[publisher.index];
|
|
172
|
+
if (!roster || Object.keys(roster).length === 0) {
|
|
173
|
+
throw new Error(`no roster was read for '${publisher.index}@${version}'`);
|
|
174
|
+
}
|
|
175
|
+
const charts = {};
|
|
176
|
+
for (const name of Object.keys(roster).sort()) {
|
|
177
|
+
const member = roster[name];
|
|
178
|
+
if (!DIGEST_PATTERN.test(String(member?.digest))) {
|
|
179
|
+
throw new Error(`${publisher.index}@${version}: ${name} has no sha256 manifest digest`);
|
|
180
|
+
}
|
|
181
|
+
charts[name] = { version: member.version, digest: member.digest };
|
|
182
|
+
}
|
|
183
|
+
return { ...publisher, version, charts };
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The roster an index chart carries: its `xema.dev/chart-set-charts`
|
|
190
|
+
* annotation, one `{"name","version"[,"digest"]}` JSON object per line, read from the
|
|
191
|
+
* chart's OCI config blob (helm stores Chart.yaml's metadata there). The blob is
|
|
192
|
+
* fetched by digest and hash-checked by the client. Returns name → {version,
|
|
193
|
+
* digest?}; `digest` is present only when the index carries one.
|
|
194
|
+
*/
|
|
195
|
+
export function parseIndexRoster(annotation, label) {
|
|
196
|
+
if (typeof annotation !== 'string' || annotation.trim().length === 0) {
|
|
197
|
+
throw new Error(`${label} carries no xema.dev/chart-set-charts roster`);
|
|
198
|
+
}
|
|
199
|
+
const charts = {};
|
|
200
|
+
for (const line of annotation.split('\n').map((l) => l.trim()).filter(Boolean)) {
|
|
201
|
+
const member = JSON.parse(line);
|
|
202
|
+
if (typeof member?.name !== 'string' || typeof member?.version !== 'string') {
|
|
203
|
+
throw new Error(`${label}: roster line is not {name, version}: ${line}`);
|
|
204
|
+
}
|
|
205
|
+
if (Object.hasOwn(charts, member.name)) {throw new Error(`${label}: roster names ${member.name} twice`);}
|
|
206
|
+
if (member.digest !== undefined && !DIGEST_PATTERN.test(String(member.digest))) {
|
|
207
|
+
throw new Error(`${label}: ${member.name} carries a malformed digest: ${member.digest}`);
|
|
208
|
+
}
|
|
209
|
+
charts[member.name] = member.digest === undefined
|
|
210
|
+
? { version: member.version }
|
|
211
|
+
: { version: member.version, digest: member.digest };
|
|
212
|
+
}
|
|
213
|
+
return Object.fromEntries(Object.entries(charts).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Give every member a manifest digest: the index's own when it carries one,
|
|
218
|
+
* else the registry's for `<name>:<version>` (`lookup`). Both present and
|
|
219
|
+
* different is refused.
|
|
220
|
+
*/
|
|
221
|
+
export async function sealRoster(roster, lookup, label) {
|
|
222
|
+
const sealed = {};
|
|
223
|
+
for (const [name, member] of Object.entries(roster)) {
|
|
224
|
+
const registryDigest = await lookup(name, member.version);
|
|
225
|
+
if (!DIGEST_PATTERN.test(String(registryDigest))) {
|
|
226
|
+
throw new Error(`${label}: registry returned no sha256 manifest digest for ${name}:${member.version}`);
|
|
227
|
+
}
|
|
228
|
+
if (member.digest !== undefined && member.digest !== registryDigest) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`${label}: ${name}:${member.version} index digest ${member.digest} != registry ${registryDigest}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
sealed[name] = { version: member.version, digest: member.digest ?? registryDigest };
|
|
234
|
+
}
|
|
235
|
+
return sealed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The annotations of the index chart `<indexName>:<version>`, read from its OCI config blob (helm stores Chart.yaml's metadata there). */
|
|
239
|
+
export async function indexAnnotations(client, indexName, version) {
|
|
240
|
+
const repository = registryRepository(indexName);
|
|
241
|
+
const manifest = JSON.parse((await client.getManifestByTag(repository, version)).bytes.toString('utf8'));
|
|
242
|
+
const configDigest = manifest?.config?.digest;
|
|
243
|
+
if (typeof configDigest !== 'string') {throw new Error(`${indexName}@${version}: manifest names no config blob`);}
|
|
244
|
+
const config = JSON.parse((await client.getBlob(repository, configDigest)).toString('utf8'));
|
|
245
|
+
return config?.annotations ?? {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function indexRoster(client, indexName, version) {
|
|
249
|
+
const annotations = await indexAnnotations(client, indexName, version);
|
|
250
|
+
return parseIndexRoster(annotations[INDEX_ROSTER_ANNOTATION], `${indexName}@${version}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Every edition directory (one holding a distribution.lock.json) under the editions root. */
|
|
254
|
+
function editionDirs() {
|
|
255
|
+
return readdirSync(editionsRoot(), { withFileTypes: true })
|
|
256
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(editionsRoot(), entry.name, 'distribution.lock.json')))
|
|
257
|
+
.map((entry) => join(editionsRoot(), entry.name))
|
|
258
|
+
.sort();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
262
|
+
const DEVELOP_CHART_VERSION_PREFIX = '0.0.0-dev.';
|
|
263
|
+
const DEVELOP_SHA_LENGTH = 12;
|
|
264
|
+
|
|
265
|
+
/** The develop chart version xema-deploy publishes for a frozen SHA: `0.0.0-dev.<sha12>`, also the OCI tag. */
|
|
266
|
+
export const developChartVersion = (sha) => `${DEVELOP_CHART_VERSION_PREFIX}${sha.slice(0, DEVELOP_SHA_LENGTH)}`;
|
|
267
|
+
|
|
268
|
+
/** Every chart (a directory with a Chart.yaml, library charts included) of an xema-deploy checkout, sorted. */
|
|
269
|
+
function deployChartNames(xemaDeployDir) {
|
|
270
|
+
const chartsDir = join(xemaDeployDir, 'charts');
|
|
271
|
+
if (!existsSync(chartsDir)) {throw new Error(`${xemaDeployDir} has no charts/ directory`);}
|
|
272
|
+
return readdirSync(chartsDir, { withFileTypes: true })
|
|
273
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(chartsDir, entry.name, 'Chart.yaml')))
|
|
274
|
+
.map((entry) => entry.name)
|
|
275
|
+
.sort();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The registry client for the chart registry, with the data root's declared credential. */
|
|
279
|
+
export function chartRegistryClient() {
|
|
280
|
+
const credential = resolveRegistryCredential({ authority: registryAuthority(), declarations: loadRegistryCredentialDeclarations() });
|
|
281
|
+
return new OrasRegistry({ credential });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The manifest digest the chart registry serves for `<chart>:<version>`. */
|
|
285
|
+
const registryManifestDigest = (client) => async (name, version) =>
|
|
286
|
+
(await client.getManifestByTag(registryRepository(name), version)).digest;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The develop chart-set lock: the SAME rostered lock a release pins, over the
|
|
290
|
+
* charts xema-deploy publishes for the frozen SHA. The deploy publisher (the
|
|
291
|
+
* workspace publisher whose repository is the deploy pin role's) publishes every
|
|
292
|
+
* chart directory of its tree at `0.0.0-dev.<sha12>`; the roster records each
|
|
293
|
+
* chart's registry manifest digest, so a consumer pulls by digest exactly as it
|
|
294
|
+
* does for a release. A chart the registry does not serve is refused by name.
|
|
295
|
+
* `lookup(name, version)` defaults to the chart registry and returns the digest.
|
|
296
|
+
*/
|
|
297
|
+
export async function resolveDevelopChartSet({ xemaDeployDir, xemaDeploySha, lookup }) {
|
|
298
|
+
if (!SHA_PATTERN.test(xemaDeploySha ?? '')) {
|
|
299
|
+
throw new Error(`the deploy checkout SHA is not a 40-hex commit: ${JSON.stringify(xemaDeploySha)}`);
|
|
300
|
+
}
|
|
301
|
+
const names = deployChartNames(xemaDeployDir);
|
|
302
|
+
if (names.length === 0) {throw new Error(`the deploy checkout at ${xemaDeploySha} has no chart (charts/<name>/Chart.yaml)`);}
|
|
303
|
+
const deployRepo = basename(deployPinKey());
|
|
304
|
+
const publishers = chartSetPublishers().filter((publisher) => basename(publisher.repo) === deployRepo);
|
|
305
|
+
if (publishers.length !== 1) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`${publishers.length} chart publishers have repository ${deployRepo} (the deploy pin role); exactly one must, ` +
|
|
308
|
+
'since it is the publisher whose develop charts this lock pins',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const version = developChartVersion(xemaDeploySha);
|
|
312
|
+
const digestOf = lookup ?? registryManifestDigest(chartRegistryClient());
|
|
313
|
+
const roster = Object.fromEntries(names.map((name) => [name, { version }]));
|
|
314
|
+
const sealed = await sealRoster(roster, digestOf, `${publishers[0].index}@${version}`);
|
|
315
|
+
const lock = buildLock({ [publishers[0].index]: version }, { [publishers[0].index]: sealed }, publishers);
|
|
316
|
+
return assertRosteredChartSetLock(lock, EDITION_CHART_SET_FILE, EditionChannel.Develop);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function readLock(path = chartSetLockPath()) {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Every index tag of one publisher, with the creation stamp the registry
|
|
329
|
+
* recorded for it.
|
|
330
|
+
*/
|
|
331
|
+
async function indexCandidates(client, indexName) {
|
|
332
|
+
const repository = registryRepository(indexName);
|
|
333
|
+
const tags = (await client.listTags(repository)).filter(isVersionTag);
|
|
334
|
+
const candidates = [];
|
|
335
|
+
for (const tag of tags) {
|
|
336
|
+
const manifest = await client.getManifestByTag(repository, tag);
|
|
337
|
+
const document = JSON.parse(manifest.bytes.toString('utf8'));
|
|
338
|
+
candidates.push({
|
|
339
|
+
tag,
|
|
340
|
+
createdAt: document?.annotations?.['org.opencontainers.image.created'] ?? null,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return candidates;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveFromRegistry() {
|
|
347
|
+
const client = chartRegistryClient();
|
|
348
|
+
|
|
349
|
+
const versionsByIndex = {};
|
|
350
|
+
const rostersByIndex = {};
|
|
351
|
+
for (const { index, repo } of chartSetPublishers()) {
|
|
352
|
+
const candidates = await indexCandidates(client, index);
|
|
353
|
+
const version = selectNewestIndex(candidates);
|
|
354
|
+
if (!version) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`${index} has no published version, so ${repo} has never closed a chart set. ` +
|
|
357
|
+
'An index is written only after its publisher pushes, signs and proves served every ' +
|
|
358
|
+
'chart of its half — its absence means that half has never completed.',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
versionsByIndex[index] = version;
|
|
362
|
+
const roster = await indexRoster(client, index, version);
|
|
363
|
+
// The registry is asked for every member, so an index digest is checked
|
|
364
|
+
// against it and a member without one still gets sealed.
|
|
365
|
+
rostersByIndex[index] = await sealRoster(
|
|
366
|
+
roster,
|
|
367
|
+
async (name, memberVersion) => (await client.getManifestByTag(registryRepository(name), memberVersion)).digest,
|
|
368
|
+
`${index}@${version}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return { versionsByIndex, rostersByIndex };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function main(argv) {
|
|
375
|
+
const check = argv.includes('--check');
|
|
376
|
+
const { versionsByIndex, rostersByIndex } = await resolveFromRegistry();
|
|
377
|
+
const next = buildLock(versionsByIndex, rostersByIndex);
|
|
378
|
+
assertRosteredChartSetLock(next, CHART_SET_LOCK_FILE, EditionChannel.Release);
|
|
379
|
+
const pinText = serializeJson(next);
|
|
380
|
+
const current = readLock();
|
|
381
|
+
// The pin AND every edition's copy of it: one writer, one run, so the N copies
|
|
382
|
+
// of one fact cannot disagree (edition-chart-set.mjs).
|
|
383
|
+
const staleEditions = editionDirs().filter((dir) => checkEditionChartSet(dir, pinText) !== EditionChartSetCheck.GREEN);
|
|
384
|
+
const changed = serializeJson(current) !== pinText || staleEditions.length > 0;
|
|
385
|
+
|
|
386
|
+
const describe = next.publishers
|
|
387
|
+
.map((publisher) => `${publisher.index}=${publisher.version}`)
|
|
388
|
+
.join(' ');
|
|
389
|
+
|
|
390
|
+
if (check) {
|
|
391
|
+
if (changed) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`${CHART_SET_LOCK_FILE} is stale${staleEditions.length ? ` (and ${staleEditions.length} edition copy/copies differ from it)` : ''}. The registry's current indexes are ${describe}. ` +
|
|
394
|
+
"Run 'node resolve-chart-set.mjs' and commit the result.",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
process.stdout.write(`chart-set: ${describe} (lock is current)\n`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!changed) {
|
|
402
|
+
process.stdout.write(`chart-set: ${describe} (unchanged)\n`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
writeFileSync(chartSetLockPath(), pinText);
|
|
406
|
+
for (const dir of editionDirs()) {writeEditionChartSet(dir, pinText);}
|
|
407
|
+
process.stdout.write(`chart-set: ${describe} (pin + ${editionDirs().length} edition ${EDITION_CHART_SET_FILE})\n`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (isEntryModule(import.meta.url)) {
|
|
411
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
412
|
+
process.stderr.write(`resolve-chart-set: ${error.message}\n`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
});
|
|
415
|
+
}
|