@xemahq/xema 0.26.0 → 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/distribution-assets.manifest.json +1 -2
- 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 +5 -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/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 +2 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +18 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +2 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +31 -7
- 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 +2 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +1 -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 +14 -6
- 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 -156
- 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,227 @@
|
|
|
1
|
+
// pin-source — where the resolver reads each first-party repository's commit.
|
|
2
|
+
//
|
|
3
|
+
// PUBLISHED (the default, and the only behaviour before this module): the
|
|
4
|
+
// committed `runtime-image-source-index.json#repositoryPins`, untouched.
|
|
5
|
+
//
|
|
6
|
+
// SNAPSHOT: one frozen map of pins, replacing the published ones for ONE
|
|
7
|
+
// in-memory resolution (the develop pipeline resolves a whole edition from it).
|
|
8
|
+
// Nothing is written back to the index, and there is NO fallback: a repo in the
|
|
9
|
+
// graph that the snapshot does not name is refused, never read from the
|
|
10
|
+
// published pin. This module owns the SHAPE of that map: a flat object whose
|
|
11
|
+
// keys live in six closed namespaces and whose values are exact 40-hex SHAs.
|
|
12
|
+
//
|
|
13
|
+
// repos/<x> | submodules/<x> every `repositoryPins[].repo` of the runtime
|
|
14
|
+
// image source index (the graph), no more, no fewer
|
|
15
|
+
// <pinRoles.data> the commit of the data root checkout (editions and indices)
|
|
16
|
+
// <pinRoles.kernel> the commit of the kernel the authorization lock is sealed
|
|
17
|
+
// with (may match repos/<x>, but is NOT in the graph)
|
|
18
|
+
// <pinRoles.deploy> the commit of the chart tree the chart-set lock digests
|
|
19
|
+
// editions-root the commit of the editions root checkout; present
|
|
20
|
+
// only when that root is NOT this repository
|
|
21
|
+
// sources/<name> one per `sources[]` entry declared in the chain
|
|
22
|
+
|
|
23
|
+
import { workspaceSection } from './edition-config.mjs';
|
|
24
|
+
import { compareStrings, serializeJson, sha256Hex } from './lib.mjs';
|
|
25
|
+
import { PinRole } from './workspace-sections.mjs';
|
|
26
|
+
|
|
27
|
+
export const PinSourceKind = Object.freeze({
|
|
28
|
+
Published: 'published',
|
|
29
|
+
Snapshot: 'snapshot',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
33
|
+
|
|
34
|
+
export const SnapshotPinNamespace = Object.freeze({
|
|
35
|
+
Graph: 'graph',
|
|
36
|
+
Distributions: 'distributions',
|
|
37
|
+
Kernel: 'kernel',
|
|
38
|
+
ChartSource: 'chart-source',
|
|
39
|
+
EditionsRoot: 'editions-root',
|
|
40
|
+
Source: 'source',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// The three checked-out roles a snapshot pins (data, kernel, deploy) are a closed
|
|
44
|
+
// set the library defines; the repository key each is pinned under is the data
|
|
45
|
+
// root's (`pinRoles` in edition-workspace.json), so a repository rename never
|
|
46
|
+
// needs a library release. `editions-root` is not a repository key but a
|
|
47
|
+
// protocol key of its own.
|
|
48
|
+
export const EDITIONS_ROOT_PIN_KEY = 'editions-root';
|
|
49
|
+
|
|
50
|
+
const pinKeyOf = (role) => workspaceSection('pinRoles', 'a snapshot pin')[role];
|
|
51
|
+
|
|
52
|
+
export const dataPinKey = () => pinKeyOf(PinRole.Data);
|
|
53
|
+
export const kernelPinKey = () => pinKeyOf(PinRole.Kernel);
|
|
54
|
+
export const deployPinKey = () => pinKeyOf(PinRole.Deploy);
|
|
55
|
+
const SOURCE_PIN_PREFIX = 'sources/';
|
|
56
|
+
const GRAPH_PIN_PATTERN = /^(?:repos|submodules)\/[a-z0-9][a-z0-9-]*$/;
|
|
57
|
+
|
|
58
|
+
/** The snapshot key of one declared `sources[]` entry. */
|
|
59
|
+
export function sourcePinKey(name) {
|
|
60
|
+
return `${SOURCE_PIN_PREFIX}${name}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The `sources[].name` a Source-namespace key pins. */
|
|
64
|
+
export function sourceNameOfPinKey(key) {
|
|
65
|
+
return key.slice(SOURCE_PIN_PREFIX.length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Which closed namespace a snapshot key belongs to; refuses a key in none. */
|
|
69
|
+
export function snapshotPinNamespace(key) {
|
|
70
|
+
if (key === dataPinKey()) {return SnapshotPinNamespace.Distributions;}
|
|
71
|
+
if (key === kernelPinKey()) {return SnapshotPinNamespace.Kernel;}
|
|
72
|
+
if (key === deployPinKey()) {return SnapshotPinNamespace.ChartSource;}
|
|
73
|
+
if (key === EDITIONS_ROOT_PIN_KEY) {return SnapshotPinNamespace.EditionsRoot;}
|
|
74
|
+
if (key.startsWith(SOURCE_PIN_PREFIX) && key.length > SOURCE_PIN_PREFIX.length) {
|
|
75
|
+
return SnapshotPinNamespace.Source;
|
|
76
|
+
}
|
|
77
|
+
if (GRAPH_PIN_PATTERN.test(key)) {return SnapshotPinNamespace.Graph;}
|
|
78
|
+
throw new Error(
|
|
79
|
+
`snapshot pin key "${key}" is in no namespace; expected a graph repository (repos/<x> or ` +
|
|
80
|
+
`submodules/<x>), "${dataPinKey()}", "${kernelPinKey()}", "${deployPinKey()}", "${EDITIONS_ROOT_PIN_KEY}" or "${SOURCE_PIN_PREFIX}<name>"`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertPinsObject(pins) {
|
|
85
|
+
if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
|
|
86
|
+
throw new Error('snapshot pins must be an object mapping key -> 40-hex commit');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertExactShas(pins) {
|
|
91
|
+
for (const [key, sha] of Object.entries(pins)) {
|
|
92
|
+
if (typeof sha !== 'string' || !SHA_PATTERN.test(sha)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`snapshot pin for "${key}" is not a 40-hex commit SHA: ${JSON.stringify(sha)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function graphPinsOf(pins) {
|
|
101
|
+
return Object.fromEntries(
|
|
102
|
+
Object.entries(pins).filter(([key]) => snapshotPinNamespace(key) === SnapshotPinNamespace.Graph),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate a COMPLETE snapshot against the graph and the composed chain.
|
|
108
|
+
* `declaredSourceNames` are the `sources[].name` entries the chain declares;
|
|
109
|
+
* `editionsRootExternal` is whether the editions root is another repository.
|
|
110
|
+
* Every refusal names the offending key; nothing falls back to a published pin.
|
|
111
|
+
*/
|
|
112
|
+
export function validateSnapshotPins(
|
|
113
|
+
pins,
|
|
114
|
+
{ runtimeSourceIndex, declaredSourceNames, editionsRootExternal },
|
|
115
|
+
) {
|
|
116
|
+
assertPinsObject(pins);
|
|
117
|
+
assertExactShas(pins);
|
|
118
|
+
for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
|
|
119
|
+
checkGraphPins(runtimeSourceIndex.repositoryPins, pins);
|
|
120
|
+
for (const [key, what] of [
|
|
121
|
+
[dataPinKey(), 'the commit of the data root checkout'],
|
|
122
|
+
[kernelPinKey(), 'the commit of the kernel the authorization lock is sealed with'],
|
|
123
|
+
[deployPinKey(), 'the commit of the chart tree the chart-set lock digests'],
|
|
124
|
+
]) {
|
|
125
|
+
if (!(key in pins)) {throw new Error(`snapshot is missing "${key}", ${what}`);}
|
|
126
|
+
}
|
|
127
|
+
if (editionsRootExternal && !(EDITIONS_ROOT_PIN_KEY in pins)) {
|
|
128
|
+
throw new Error(`snapshot is missing "${EDITIONS_ROOT_PIN_KEY}": the editions root is not this repository`);
|
|
129
|
+
}
|
|
130
|
+
if (!editionsRootExternal && EDITIONS_ROOT_PIN_KEY in pins) {
|
|
131
|
+
throw new Error(`snapshot names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
|
|
132
|
+
}
|
|
133
|
+
const declared = new Set(declaredSourceNames);
|
|
134
|
+
for (const name of declared) {
|
|
135
|
+
if (!(sourcePinKey(name) in pins)) {
|
|
136
|
+
throw new Error(`snapshot is missing "${sourcePinKey(name)}": the chain declares source "${name}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const key of Object.keys(pins)) {
|
|
140
|
+
if (snapshotPinNamespace(key) === SnapshotPinNamespace.Source && !declared.has(sourceNameOfPinKey(key))) {
|
|
141
|
+
throw new Error(`snapshot names "${key}", but the chain declares no source "${sourceNameOfPinKey(key)}"`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The pins sorted by key: the one canonical order, shared by the hash and the artifact. */
|
|
147
|
+
export function sortedSnapshotPins(pins) {
|
|
148
|
+
assertPinsObject(pins);
|
|
149
|
+
return Object.fromEntries(Object.entries(pins).sort(([a], [b]) => compareStrings(a, b)));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** sha256 hex of the canonical JSON of the pins sorted by key: the snapshot's identity. */
|
|
153
|
+
export function snapshotHash(pins) {
|
|
154
|
+
return sha256Hex(serializeJson(sortedSnapshotPins(pins)));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A declared source with its ref replaced by the snapshot pin; identity under a published pin source. */
|
|
158
|
+
export function pinnedSource(source, pinSource = PUBLISHED_PIN_SOURCE) {
|
|
159
|
+
if (pinSource?.kind !== PinSourceKind.Snapshot) {return source;}
|
|
160
|
+
const key = sourcePinKey(source.name);
|
|
161
|
+
const ref = pinSource.pins[key];
|
|
162
|
+
if (ref === undefined) {
|
|
163
|
+
throw new Error(`snapshot has no pin "${key}" for the declared source "${source.name}"; there is no fallback to its declared ref`);
|
|
164
|
+
}
|
|
165
|
+
return { ...source, ref };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const PUBLISHED_PIN_SOURCE = Object.freeze({ kind: PinSourceKind.Published });
|
|
169
|
+
|
|
170
|
+
/** @param {Record<string,string>} pins repo -> 40-hex commit */
|
|
171
|
+
export function snapshotPinSource(pins) {
|
|
172
|
+
return Object.freeze({ kind: PinSourceKind.Snapshot, pins });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Apply a pin source to a parsed runtime image source index. Published returns
|
|
177
|
+
* the SAME object (inert, byte for byte). Snapshot returns a copy whose
|
|
178
|
+
* `repositoryPins` carry the snapshot SHAs.
|
|
179
|
+
*/
|
|
180
|
+
export function applyPinSource(runtimeSourceIndex, pinSource = PUBLISHED_PIN_SOURCE) {
|
|
181
|
+
switch (pinSource?.kind) {
|
|
182
|
+
case PinSourceKind.Published:
|
|
183
|
+
return runtimeSourceIndex;
|
|
184
|
+
case PinSourceKind.Snapshot:
|
|
185
|
+
return applySnapshot(runtimeSourceIndex, pinSource.pins);
|
|
186
|
+
default:
|
|
187
|
+
throw new Error(
|
|
188
|
+
`unknown pin source kind ${JSON.stringify(pinSource?.kind)}; expected ` +
|
|
189
|
+
`${Object.values(PinSourceKind).join(' | ')}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function checkGraphPins(graph, pins) {
|
|
195
|
+
const graphRepos = new Set(graph.map((pin) => pin.repo));
|
|
196
|
+
for (const repo of Object.keys(graphPinsOf(pins))) {
|
|
197
|
+
if (!graphRepos.has(repo)) {
|
|
198
|
+
throw new Error(`snapshot names "${repo}", which is not a repository in the graph`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const missing = [...graphRepos].filter((repo) => !(repo in pins));
|
|
202
|
+
if (missing.length > 0) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`snapshot is missing ${missing.length} repositor${missing.length === 1 ? 'y' : 'ies'} ` +
|
|
205
|
+
`in the graph: ${missing.join(', ')}. There is no fallback to the published pin.`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applySnapshot(runtimeSourceIndex, pins) {
|
|
211
|
+
assertPinsObject(pins);
|
|
212
|
+
assertExactShas(pins);
|
|
213
|
+
for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
|
|
214
|
+
const graph = runtimeSourceIndex.repositoryPins;
|
|
215
|
+
checkGraphPins(graph, pins);
|
|
216
|
+
return {
|
|
217
|
+
...runtimeSourceIndex,
|
|
218
|
+
repositoryPins: graph.map((pin) => ({
|
|
219
|
+
...pin,
|
|
220
|
+
// The IMAGE axis only. `vendoredSourceCommit` is the SOURCE axis (the
|
|
221
|
+
// published tip vendored bytes come from) and is deliberately NOT fused
|
|
222
|
+
// with it: see `build-runtime-image-source-index.mjs#repositoryIdentity`
|
|
223
|
+
// for the measured production failure that fusing the two caused.
|
|
224
|
+
sourceCommit: pins[pin.repo],
|
|
225
|
+
})),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
+
// WHICH PRODUCING REPOSITORIES MUST RE-SIGN BEFORE THE NEXT RELEASE?
|
|
4
|
+
//
|
|
5
|
+
// xema edition evidence plan-refresh \
|
|
6
|
+
// --data-root <dir> --editions-root <dir> --kernel <dir> \
|
|
7
|
+
// [--kernel-checkout exact|preview] \
|
|
8
|
+
// --distribution <id> [repeatable] \
|
|
9
|
+
// [--horizon-seconds <n>] [--now <epoch-millis>] [--github-output <path>]
|
|
10
|
+
//
|
|
11
|
+
// Producer evidence is read from ONE place: the OCI referrers of each locked
|
|
12
|
+
// image, in the registry the lock names.
|
|
13
|
+
//
|
|
14
|
+
// `--kernel-checkout exact` (the default) loads the kernel schemas from a clean
|
|
15
|
+
// kernel checkout at exactly the commit the data root's evidence-trust pin
|
|
16
|
+
// records. `preview` loads whatever kernel is built at `--kernel`: that build
|
|
17
|
+
// is held to no commit, so its plan is a LOCAL PREVIEW. It says so on every
|
|
18
|
+
// run and cannot write `--github-output`, so no workflow can act on it.
|
|
19
|
+
//
|
|
20
|
+
// WHY THIS EXISTS
|
|
21
|
+
//
|
|
22
|
+
// Producer evidence carries a hard `expiresAt` that the offline verifier
|
|
23
|
+
// enforces. Nothing was watching it. Evidence was produced only as a
|
|
24
|
+
// side-effect of an image BUILD (or a manual dispatch), so a subject whose
|
|
25
|
+
// source has not changed in a week has evidence that quietly ages out — and
|
|
26
|
+
// the failure surfaces at the worst possible moment: at a customer install, or
|
|
27
|
+
// as a release that cannot be delivered, with a green CI behind it.
|
|
28
|
+
//
|
|
29
|
+
// This is the standing question asked on a schedule instead: for every subject
|
|
30
|
+
// of every named edition, is there a published producer bundle, is it usable,
|
|
31
|
+
// and does it still have enough life to survive the release that will consume
|
|
32
|
+
// it? Anything else is a refresh that is DUE, reported per producing
|
|
33
|
+
// repository so the owner of that repository can act on one line.
|
|
34
|
+
//
|
|
35
|
+
// EXIT CODES — the distinction is deliberate
|
|
36
|
+
//
|
|
37
|
+
// 0 every subject is CURRENT.
|
|
38
|
+
// 2 a verdict was reached and a refresh IS due. This is the loud failure the
|
|
39
|
+
// scheduled workflow exists to produce.
|
|
40
|
+
// 1 NO verdict could be reached (bad arguments, unreadable locks, a lock and
|
|
41
|
+
// the runtime index disagreeing about a repository pin). A planner that
|
|
42
|
+
// cannot decide must never report "nothing to do".
|
|
43
|
+
//
|
|
44
|
+
// THE HORIZON IS THE REUSE FLOOR
|
|
45
|
+
//
|
|
46
|
+
// The default horizon is `MIN_REMAINING_TTL_SECONDS`, the same floor
|
|
47
|
+
// `check-committed-image-evidence.mjs` uses to decide a committed bundle is
|
|
48
|
+
// too close to expiry to hand forward. One number, one meaning: below it, the
|
|
49
|
+
// bundle is not something a release should be built on.
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
+
|
|
52
|
+
import { appendFileSync } from 'node:fs';
|
|
53
|
+
import { isAbsolute } from 'node:path';
|
|
54
|
+
|
|
55
|
+
import { MIN_REMAINING_TTL_SECONDS } from './check-committed-image-evidence.mjs';
|
|
56
|
+
import {
|
|
57
|
+
expectedProducerSource,
|
|
58
|
+
parseEvidenceCliFlags,
|
|
59
|
+
} from './distribution-evidence-set.mjs';
|
|
60
|
+
import {
|
|
61
|
+
WorkspaceRoot,
|
|
62
|
+
configureEditionWorkspaceFromArgv,
|
|
63
|
+
dataRoot,
|
|
64
|
+
editionsRoot,
|
|
65
|
+
isEntryModule,
|
|
66
|
+
} from './edition-workspace.mjs';
|
|
67
|
+
import {
|
|
68
|
+
canonicalJson,
|
|
69
|
+
loadValidatedLocks,
|
|
70
|
+
} from './image-evidence.mjs';
|
|
71
|
+
import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
|
|
72
|
+
import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
|
|
73
|
+
import {
|
|
74
|
+
createRegistryClientResolver,
|
|
75
|
+
resolveImageSourceEvidenceReferrer,
|
|
76
|
+
} from './oci-image-source-evidence.mjs';
|
|
77
|
+
import {
|
|
78
|
+
loadProducerKernelSchemas,
|
|
79
|
+
readPinnedContractAuthority,
|
|
80
|
+
} from './produce-image-source-evidence.mjs';
|
|
81
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
82
|
+
|
|
83
|
+
/** Closed set of per-subject refresh verdicts. */
|
|
84
|
+
export const EvidenceRefreshStatus = Object.freeze({
|
|
85
|
+
Current: 'CURRENT',
|
|
86
|
+
Absent: 'ABSENT',
|
|
87
|
+
Unusable: 'UNUSABLE',
|
|
88
|
+
Expired: 'EXPIRED',
|
|
89
|
+
Expiring: 'EXPIRING',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** Where the kernel schemas of a planning run come from. */
|
|
93
|
+
export const KernelCheckout = Object.freeze({
|
|
94
|
+
/** A clean kernel checkout at exactly the pinned authority commit. */
|
|
95
|
+
Exact: 'exact',
|
|
96
|
+
/** The kernel built at `--kernel`, held to no commit: a local preview only. */
|
|
97
|
+
Preview: 'preview',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const DUE_STATUSES = Object.freeze(
|
|
101
|
+
new Set([
|
|
102
|
+
EvidenceRefreshStatus.Absent,
|
|
103
|
+
EvidenceRefreshStatus.Unusable,
|
|
104
|
+
EvidenceRefreshStatus.Expired,
|
|
105
|
+
EvidenceRefreshStatus.Expiring,
|
|
106
|
+
]),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @returns {Promise<{horizonSeconds: number, refreshDue: boolean,
|
|
111
|
+
* subjects: Array<object>, groups: Array<object>}>}
|
|
112
|
+
*/
|
|
113
|
+
export async function planImageEvidenceRefresh({
|
|
114
|
+
distributionIds,
|
|
115
|
+
dataRoot: dataRootDirectory,
|
|
116
|
+
editionsRoot: editionsRootDirectory,
|
|
117
|
+
schemas,
|
|
118
|
+
expectedContractAuthority,
|
|
119
|
+
registryClientFactory,
|
|
120
|
+
horizonSeconds = MIN_REMAINING_TTL_SECONDS,
|
|
121
|
+
now = Date.now(),
|
|
122
|
+
}) {
|
|
123
|
+
if (!Array.isArray(distributionIds) || distributionIds.length === 0) {
|
|
124
|
+
throw new Error('at least one distribution id is required');
|
|
125
|
+
}
|
|
126
|
+
if (!expectedContractAuthority) {
|
|
127
|
+
throw new Error('the expected kernel contract authority is required');
|
|
128
|
+
}
|
|
129
|
+
if (!isAbsolute(dataRootDirectory) || !isAbsolute(editionsRootDirectory)) {
|
|
130
|
+
throw new Error('the data root and the editions root must be absolute paths');
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isSafeInteger(horizonSeconds) || horizonSeconds <= 0) {
|
|
133
|
+
throw new Error('the refresh horizon must be a positive whole number of seconds');
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isSafeInteger(now) || now <= 0) {
|
|
136
|
+
throw new Error('the evaluation instant must be positive epoch milliseconds');
|
|
137
|
+
}
|
|
138
|
+
const clientFor = createRegistryClientResolver({
|
|
139
|
+
dataRoot: dataRootDirectory,
|
|
140
|
+
factory: registryClientFactory,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const subjects = [];
|
|
144
|
+
for (const distributionId of [...distributionIds].sort(compareStrings)) {
|
|
145
|
+
const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedLocks(
|
|
146
|
+
distributionId,
|
|
147
|
+
dataRootDirectory,
|
|
148
|
+
schemas,
|
|
149
|
+
{ editionsRoot: editionsRootDirectory },
|
|
150
|
+
);
|
|
151
|
+
assertKernelContractAuthorityEqual(
|
|
152
|
+
runtimeSourceIndex.contractAuthority,
|
|
153
|
+
expectedContractAuthority,
|
|
154
|
+
'runtime-image-source-index.json',
|
|
155
|
+
);
|
|
156
|
+
for (const subject of imageLock.subjects) {
|
|
157
|
+
const repositoryPin = repositoryPins.get(subject.repo);
|
|
158
|
+
const expectedSource = expectedProducerSource({
|
|
159
|
+
subject,
|
|
160
|
+
repositoryPin,
|
|
161
|
+
sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
|
|
162
|
+
});
|
|
163
|
+
let matches = [];
|
|
164
|
+
// Set when the registry ANSWERS that the locked coordinate holds no
|
|
165
|
+
// image. That is a decided verdict for this subject, not an undecidable
|
|
166
|
+
// one, so it is reported rather than thrown — see the exit-code contract
|
|
167
|
+
// in this file's header. Until 2026-09-02 the throw escaped the whole
|
|
168
|
+
// planner: measured that day, ALL 68 locked coordinates were absent from
|
|
169
|
+
// ghcr, and the run died on the first of them and said nothing about the
|
|
170
|
+
// other 67.
|
|
171
|
+
let coordinateAbsence = null;
|
|
172
|
+
{
|
|
173
|
+
const { authority, repository } = splitImageRef(subject.imageRef);
|
|
174
|
+
const client = clientFor(authority);
|
|
175
|
+
let image = null;
|
|
176
|
+
try {
|
|
177
|
+
image = await client.getManifestByTag(repository, subject.contentHash);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// A registry that says "no such manifest" has decided this subject.
|
|
180
|
+
// Any other failure means the question went UNANSWERED, and a planner
|
|
181
|
+
// that cannot decide must never report "nothing to do" — so it still
|
|
182
|
+
// escapes and the run still exits 1.
|
|
183
|
+
if (error?.status !== 404) {throw error;}
|
|
184
|
+
coordinateAbsence =
|
|
185
|
+
`the registry has no image at the locked coordinate ` +
|
|
186
|
+
`${subject.imageRef}:${subject.contentHash} — nothing was ever ` +
|
|
187
|
+
`published there, so no producer evidence can attest it and the ` +
|
|
188
|
+
`lock must be re-resolved against a commit that was actually built`;
|
|
189
|
+
}
|
|
190
|
+
if (image !== null) {
|
|
191
|
+
const artifact = await resolveImageSourceEvidenceReferrer({
|
|
192
|
+
client,
|
|
193
|
+
imageRepository: subject.imageRef,
|
|
194
|
+
subjectDigest: image.digest,
|
|
195
|
+
schemas,
|
|
196
|
+
});
|
|
197
|
+
if (artifact !== null) {
|
|
198
|
+
matches = artifact.records.filter(
|
|
199
|
+
(record) =>
|
|
200
|
+
canonicalJson(record.statement.predicate.source) ===
|
|
201
|
+
canonicalJson(expectedSource),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const verdict =
|
|
207
|
+
coordinateAbsence !== null
|
|
208
|
+
? {
|
|
209
|
+
status: EvidenceRefreshStatus.Unusable,
|
|
210
|
+
remainingSeconds: null,
|
|
211
|
+
reason: coordinateAbsence,
|
|
212
|
+
}
|
|
213
|
+
: classifySubject({ matches, horizonSeconds, now });
|
|
214
|
+
subjects.push({
|
|
215
|
+
distributionId,
|
|
216
|
+
subjectKind: subject.kind,
|
|
217
|
+
subjectId: subject.id,
|
|
218
|
+
repo: subject.repo,
|
|
219
|
+
repositoryUrl: repositoryPin.repositoryUrl,
|
|
220
|
+
sourceCommit: subject.sourceCommit,
|
|
221
|
+
...verdict,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
subjects.sort(
|
|
227
|
+
(left, right) =>
|
|
228
|
+
compareStrings(left.repo, right.repo) ||
|
|
229
|
+
compareStrings(left.distributionId, right.distributionId) ||
|
|
230
|
+
compareStrings(left.subjectKind, right.subjectKind) ||
|
|
231
|
+
compareStrings(left.subjectId, right.subjectId),
|
|
232
|
+
);
|
|
233
|
+
const groups = [];
|
|
234
|
+
for (const subject of subjects) {
|
|
235
|
+
const incumbent = groups.at(-1);
|
|
236
|
+
if (incumbent?.repo === subject.repo) {
|
|
237
|
+
incumbent.subjects.push(subject);
|
|
238
|
+
incumbent.due ||= DUE_STATUSES.has(subject.status);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
groups.push({
|
|
242
|
+
repo: subject.repo,
|
|
243
|
+
repositoryUrl: subject.repositoryUrl,
|
|
244
|
+
subjects: [subject],
|
|
245
|
+
due: DUE_STATUSES.has(subject.status),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
horizonSeconds,
|
|
250
|
+
refreshDue: subjects.some((subject) => DUE_STATUSES.has(subject.status)),
|
|
251
|
+
subjects,
|
|
252
|
+
groups,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function classifySubject({ matches, horizonSeconds, now }) {
|
|
257
|
+
if (matches.length === 0) {
|
|
258
|
+
return {
|
|
259
|
+
status: EvidenceRefreshStatus.Absent,
|
|
260
|
+
remainingSeconds: null,
|
|
261
|
+
reason: 'no published producer evidence attests this source coordinate',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const [match] = matches;
|
|
265
|
+
const expiresAt = match.statement.predicate.producerRun.expiresAt;
|
|
266
|
+
const remainingSeconds = Math.floor((Date.parse(expiresAt) - now) / 1000);
|
|
267
|
+
if (!Number.isFinite(remainingSeconds)) {
|
|
268
|
+
return {
|
|
269
|
+
status: EvidenceRefreshStatus.Unusable,
|
|
270
|
+
remainingSeconds: null,
|
|
271
|
+
reason: `published statement has an unreadable expiresAt "${String(expiresAt)}"`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (remainingSeconds <= 0) {
|
|
275
|
+
return {
|
|
276
|
+
status: EvidenceRefreshStatus.Expired,
|
|
277
|
+
remainingSeconds,
|
|
278
|
+
reason: `evidence EXPIRED at ${expiresAt}; it can never verify again`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (remainingSeconds < horizonSeconds) {
|
|
282
|
+
return {
|
|
283
|
+
status: EvidenceRefreshStatus.Expiring,
|
|
284
|
+
remainingSeconds,
|
|
285
|
+
reason: `evidence expires in ${remainingSeconds}s, inside the ${horizonSeconds}s refresh horizon`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
status: EvidenceRefreshStatus.Current,
|
|
290
|
+
remainingSeconds,
|
|
291
|
+
reason: `evidence is usable for another ${remainingSeconds}s`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** One line per subject, plus GitHub annotations grouped by producing repo. */
|
|
296
|
+
export function renderRefreshPlan(plan) {
|
|
297
|
+
const lines = [];
|
|
298
|
+
for (const group of plan.groups) {
|
|
299
|
+
const due = group.subjects.filter((subject) =>
|
|
300
|
+
DUE_STATUSES.has(subject.status),
|
|
301
|
+
);
|
|
302
|
+
if (due.length === 0) {
|
|
303
|
+
lines.push(
|
|
304
|
+
`[image-evidence-refresh] ${group.repo}: all ${group.subjects.length} subject(s) CURRENT`,
|
|
305
|
+
);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
lines.push(
|
|
309
|
+
`::error::[image-evidence-refresh] ${group.repo} (${group.repositoryUrl}) must re-run its ` +
|
|
310
|
+
`image-source evidence producer for ${due.length} of ${group.subjects.length} subject(s): ` +
|
|
311
|
+
due
|
|
312
|
+
.map(
|
|
313
|
+
(subject) =>
|
|
314
|
+
`${subject.subjectKind}:${subject.subjectId} [${subject.distributionId}] ${subject.status} — ${subject.reason}`,
|
|
315
|
+
)
|
|
316
|
+
.join(' | '),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
lines.push(
|
|
320
|
+
plan.refreshDue
|
|
321
|
+
? `[image-evidence-refresh] REFRESH DUE for ${
|
|
322
|
+
plan.subjects.filter((subject) => DUE_STATUSES.has(subject.status))
|
|
323
|
+
.length
|
|
324
|
+
} of ${plan.subjects.length} subject(s) within a ${plan.horizonSeconds}s horizon`
|
|
325
|
+
: `[image-evidence-refresh] all ${plan.subjects.length} subject(s) hold evidence beyond the ${plan.horizonSeconds}s horizon`,
|
|
326
|
+
);
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The step outputs a workflow reads: whether a refresh is due, how many subjects, and which producing repositories. */
|
|
331
|
+
export function renderGithubOutputs(plan) {
|
|
332
|
+
const dueCount = plan.subjects.filter((subject) => DUE_STATUSES.has(subject.status)).length;
|
|
333
|
+
return (
|
|
334
|
+
`refresh-due=${plan.refreshDue}\n` +
|
|
335
|
+
`due-subjects=${dueCount}\n` +
|
|
336
|
+
`total-subjects=${plan.subjects.length}\n` +
|
|
337
|
+
`due-repositories=${plan.groups
|
|
338
|
+
.filter((group) => group.due)
|
|
339
|
+
.map((group) => group.repo)
|
|
340
|
+
.join(',')}\n`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function main() {
|
|
345
|
+
const argv = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
|
|
346
|
+
WorkspaceRoot.Data,
|
|
347
|
+
WorkspaceRoot.Editions,
|
|
348
|
+
WorkspaceRoot.Kernel,
|
|
349
|
+
]);
|
|
350
|
+
const flags = parseEvidenceCliFlags(argv, {
|
|
351
|
+
single: [
|
|
352
|
+
'horizon-seconds',
|
|
353
|
+
'now',
|
|
354
|
+
'github-output',
|
|
355
|
+
'kernel-checkout',
|
|
356
|
+
],
|
|
357
|
+
repeated: ['distribution'],
|
|
358
|
+
});
|
|
359
|
+
const horizonRaw = flags.optional('horizon-seconds');
|
|
360
|
+
const nowRaw = flags.optional('now');
|
|
361
|
+
const kernelCheckout = flags.optional('kernel-checkout') ?? KernelCheckout.Exact;
|
|
362
|
+
if (!Object.values(KernelCheckout).includes(kernelCheckout)) {
|
|
363
|
+
throw new Error(`--kernel-checkout must be one of ${Object.values(KernelCheckout).join(', ')}`);
|
|
364
|
+
}
|
|
365
|
+
const githubOutput = flags.optional('github-output');
|
|
366
|
+
const preview = kernelCheckout === KernelCheckout.Preview;
|
|
367
|
+
if (preview && githubOutput !== undefined) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
'--kernel-checkout preview plans with a kernel build that is held to no commit; ' +
|
|
370
|
+
'its plan is a local preview and cannot be written to --github-output',
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const schemas = preview
|
|
374
|
+
? await loadKernelDistributionSchemas()
|
|
375
|
+
: (await loadProducerKernelSchemas({ dataRoot: dataRoot() })).schemas;
|
|
376
|
+
const plan = await planImageEvidenceRefresh({
|
|
377
|
+
distributionIds: flags.requireMany('distribution'),
|
|
378
|
+
dataRoot: dataRoot(),
|
|
379
|
+
editionsRoot: editionsRoot(),
|
|
380
|
+
schemas,
|
|
381
|
+
expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
|
|
382
|
+
horizonSeconds:
|
|
383
|
+
horizonRaw === undefined ? MIN_REMAINING_TTL_SECONDS : Number(horizonRaw),
|
|
384
|
+
now: nowRaw === undefined ? Date.now() : Number(nowRaw),
|
|
385
|
+
});
|
|
386
|
+
if (preview) {
|
|
387
|
+
console.log(
|
|
388
|
+
'[image-evidence-refresh] LOCAL PREVIEW: planned with the kernel built at --kernel, which is held to no commit. ' +
|
|
389
|
+
'Nothing may be sealed or scheduled from this plan; run without --kernel-checkout preview for a verdict.',
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
for (const line of renderRefreshPlan(plan)) {
|
|
393
|
+
console.log(line);
|
|
394
|
+
}
|
|
395
|
+
if (githubOutput !== undefined) {
|
|
396
|
+
appendFileSync(githubOutput, renderGithubOutputs(plan), 'utf8');
|
|
397
|
+
}
|
|
398
|
+
if (plan.refreshDue) {
|
|
399
|
+
process.exitCode = 2;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (isEntryModule(import.meta.url)) {
|
|
404
|
+
main().catch((error) => {
|
|
405
|
+
console.error(`[image-evidence-refresh] ${error.message}`);
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
});
|
|
408
|
+
}
|