@xemahq/xema 0.54.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edition/verify-image-evidence.bundle.mjs +1989 -1008
- package/dist/edition/verify-image-evidence.bundle.provenance.json +583 -499
- package/dist/runtime/lock-loader.d.ts +2 -1
- package/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +15 -5
- package/dist/runtime/lock-loader.js.map +1 -1
- package/edition/README.md +8 -3
- package/edition/check-committed-image-evidence.mjs +44 -15
- package/edition/customer-edition-verbs.mjs +15 -4
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-resolve.mjs +4 -3
- package/edition/develop-edition.mjs +30 -9
- package/edition/develop-freeze.mjs +24 -1
- package/edition/develop-graph.mjs +5 -3
- package/edition/develop-image-probe.mjs +6 -4
- package/edition/develop-image-record.mjs +194 -0
- package/edition/develop-record-ancestry.mjs +23 -5
- package/edition/develop-select-records.mjs +29 -21
- package/edition/develop-selection-line.mjs +30 -0
- package/edition/develop-selection-pins.mjs +30 -0
- package/edition/dist-check.mjs +45 -9
- package/edition/distribution-evidence-set.mjs +14 -9
- package/edition/edition-image-seal.mjs +2 -1
- package/edition/edition-protocol.mjs +6 -20
- package/edition/image-evidence.mjs +6 -2
- package/edition/image-lock-service-rows.mjs +4 -4
- package/edition/image-subject-identity.mjs +24 -0
- package/edition/images-from-record.mjs +46 -110
- package/edition/lib.mjs +24 -26
- package/edition/resolve-customer-edition.mjs +1 -1
- package/edition/resolve-edition.mjs +2 -1
- package/edition/resolve-images.mjs +224 -12
- package/edition/seal-image-digests.mjs +24 -13
- package/edition/verify-image-platforms.mjs +2 -0
- package/edition/workspace-sections.mjs +11 -1
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DistributionLockReadView, type RuntimeImagePlatform } from '@xemahq/contracts/distribution';
|
|
1
|
+
import { type DistributionLockReadView, type RuntimeImagePlatform, type RuntimeImageSubject } from '@xemahq/contracts/distribution';
|
|
2
2
|
import { type ArtifactResolution } from './paths';
|
|
3
3
|
export interface LoadedDistributionLock {
|
|
4
4
|
readonly lock: DistributionLockReadView;
|
|
@@ -20,4 +20,5 @@ export interface LoadedImageLock {
|
|
|
20
20
|
readonly resolution: ArtifactResolution;
|
|
21
21
|
}
|
|
22
22
|
export declare function loadImageLock(opts: LoadLockOptions): LoadedImageLock;
|
|
23
|
+
export declare function subjectIdentity(subject: RuntimeImageSubject): string;
|
|
23
24
|
//# sourceMappingURL=lock-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,
|
|
1
|
+
{"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAqD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAarG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CASlF;AAQD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAQ7B,QAAQ,CAAC,iBAAiB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,eAAe;IAQ9B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AA0BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CA+BpE;AASD,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAIpE"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadDistributionLock = loadDistributionLock;
|
|
4
4
|
exports.loadImageLock = loadImageLock;
|
|
5
|
+
exports.subjectIdentity = subjectIdentity;
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
6
7
|
const distribution_1 = require("@xemahq/contracts/distribution");
|
|
7
8
|
const paths_1 = require("./paths");
|
|
@@ -20,14 +21,21 @@ function loadImageLock(opts) {
|
|
|
20
21
|
const lock = (0, distribution_1.parseImageLock)(readJson(resolution.path));
|
|
21
22
|
const subjectsByKey = new Map();
|
|
22
23
|
const imagesByService = new Map();
|
|
24
|
+
const subjectsByName = new Map();
|
|
23
25
|
for (const subject of lock.subjects) {
|
|
24
|
-
subjectsByKey.set(
|
|
26
|
+
subjectsByKey.set(subjectIdentity(subject), subject);
|
|
25
27
|
}
|
|
26
|
-
for (const
|
|
28
|
+
for (const subject of subjectsByKey.values()) {
|
|
29
|
+
const named = subjectsByName.get(`${subject.kind}:${subject.id}`);
|
|
30
|
+
if (named === undefined || (subject.generation ?? 0) >= (named.generation ?? 0)) {
|
|
31
|
+
subjectsByName.set(`${subject.kind}:${subject.id}`, subject);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const [service, subject] of (0, distribution_1.dedicatedServiceImageSubjects)(lock)) {
|
|
27
35
|
imagesByService.set(service, toImageEntry(subject));
|
|
28
36
|
}
|
|
29
37
|
for (const binding of lock.platformServiceBindings) {
|
|
30
|
-
const subject =
|
|
38
|
+
const subject = subjectsByName.get(`${binding.subjectKind}:${binding.subjectId}`);
|
|
31
39
|
if (subject === undefined) {
|
|
32
40
|
throw new Error(`Invariant violated reading ${resolution.path}: platform service ` +
|
|
33
41
|
`"${binding.platformService}" binds the ${binding.subjectKind} subject ` +
|
|
@@ -37,8 +45,10 @@ function loadImageLock(opts) {
|
|
|
37
45
|
}
|
|
38
46
|
return { imagesByService, resolution };
|
|
39
47
|
}
|
|
40
|
-
function
|
|
41
|
-
return
|
|
48
|
+
function subjectIdentity(subject) {
|
|
49
|
+
return subject.kind === distribution_1.RuntimeImageSubjectKind.ComposedProcess
|
|
50
|
+
? `${subject.kind}:${subject.imageRef}:${subject.contentHash}`
|
|
51
|
+
: `${subject.kind}:${subject.id}`;
|
|
42
52
|
}
|
|
43
53
|
function toImageEntry(subject) {
|
|
44
54
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":";;AAoCA,oDASC;AAyDD,
|
|
1
|
+
{"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"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"}
|
package/edition/README.md
CHANGED
|
@@ -82,7 +82,8 @@ xema edition artifact <id> --channel release --registry <ns> --coordinate <tag>
|
|
|
82
82
|
xema edition develop <freeze-sources|resolve|target|check-existing> ...
|
|
83
83
|
# develop tag = sha256 of { cli, engine, pins, sourceImages }; target and check-existing take --engine-image, and a customer's --source-image
|
|
84
84
|
xema edition develop coverage <id> --pins <file> --cosign <abs path> [--json] # read-only forecast (policy held to its pin, this CLI's own verifier code): is each develop image producer-signed? exit 0 covered, 1 not, 2 error
|
|
85
|
-
xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, images }, written once; any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
|
|
85
|
+
xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, behind, images, currentResolution }, written once (a member whose firstPartySources entry declares `currentResolution: "<ticket>"`, xema-docs until XEMA-2373, is skipped with no registry read, listed in currentResolution and printed as `xema-docs: current resolution (leaves the edition, XEMA-2373)`) (behind = { <repo>: { head, commits } }: develop's head and the commits the pinned one trails it by; stdout gets one `<short name>: record <commit>, <N> commits behind HEAD` line per member, always `commits`, short name = the repo's last path segment and unambiguous; images[] entries carry `member`); any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
|
|
86
|
+
xema edition develop image-record <id> --selection <file> --pins <file> --out <file> # the edition's image build record (what `resolve --images-from` reads) from a select-records selection: subjects are the checked-out snapshot's first-party image subjects; a current-resolution member's (xema-docs) is probed and goes in `images`, every other takes the selection's image of its develop repository (selected commit must equal the snapshot's) and goes in `satellites`; platforms read by digest; any gap refuses and writes nothing; written once
|
|
86
87
|
xema edition develop verify-image-record --image <name>@sha256:<digest> --producer <owner/repo> --registry <root> --cosign <abs path> # is the digest bound to the producer by an authenticated develop build record (shared verify core with select-records). Records are read newest run first (listBuildRecordRuns sorts by compareBuildRecordRuns: run, then attempt, highest first), a verified record of a workflow other than the producer's `.github/workflows/ci.yaml` is skipped, and the answer is the newest ci.yaml record listing the exact image -> { image, digest, producer, commit }. `commit` is the commit of the newest verified ci.yaml record (by run number, then attempt) that lists the digest; it can be an older commit when a later run targeted it, and for an image the producer reused by content hash it is NOT the commit that built it. This is safe because the digest is what binds the bytes. Refuses when no ci.yaml record lists the image, the record names another producer, or cosign refuses the identity
|
|
87
88
|
xema edition develop check-overrides --overrides <json> --data-root <dir> # data root only
|
|
88
89
|
xema edition ceiling --distribution <id> --at <distributions sha> --producer-commit <owner/repo>=<sha> [...] --data-root <dir> --sources-root <dir> --kernel <dir> # the dev ceiling (DevelopCeilingSchema from @xemahq/contracts/develop-ceiling): selection, origin, repo and trustCeiling from the definition and indexes at --at; version, services, runtimeComponents and serviceProcessFacts from each producer's xema-biome.json and xema-biome.index.json at its --producer-commit; a missing commit or manifest refuses, with no fallback
|
|
@@ -126,7 +127,7 @@ The develop graph is the data root's first-party repositories, declared in `edit
|
|
|
126
127
|
|
|
127
128
|
- Each key is a repository handle (`repos/<x>` or `submodules/<x>`); `sourceId` is its kebab-case source id; `repositoryUrl` is canonical HTTPS under `sourceRepositoryUrlPrefix`. Both are unique. The retired string form (`"repos/x": "x"`) is refused with `RetiredFirstPartySourceShapeError`.
|
|
128
129
|
- `imageOrigin` says where the source's images come from on the everyday lane: `workspace` (built by the job in the aggregator workspace) or `satellite` (a producer outside it, such as `submodules/xema-host-web`, that publishes its own signed develop digest). The parse accepts a source without it and fills in no default; `--images-from` refuses such a source (see below).
|
|
129
|
-
- `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
|
|
130
|
+
- `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. With `--member-pins <selection file>` (a select-records selection; only `.pins`, 40-hex, is read) each member is pinned at its selected record commit instead: the selection must pin exactly the graph minus its `currentResolution` members (equal to the graph members' declared `currentResolution`; a missing or extra member is refused by name), a `currentResolution` member (xema-docs, XEMA-2373) is pinned at its develop head, and `--member-pins` with `--overrides` is refused. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
|
|
130
131
|
- A first-party repository is in the develop graph before any release pins it. The converse is checked offline: every `repositoryPins` entry must be declared first-party at the same `repositoryUrl`.
|
|
131
132
|
|
|
132
133
|
## Develop images from a build record
|
|
@@ -177,9 +178,13 @@ The edition library builds, validates and verifies evidence statements; it never
|
|
|
177
178
|
- `xema edition evidence statement catalogue stage|accept` (vendor) derives the E4 statement from `--artifact oci://<repository>@sha256:<digest>` and `--offering <id>`, and `accept` attaches the signed statement and bundle to the artifact as a referrer. A release catalogue is verified before it is read, by the data root's pinned built verifier (`catalogue-evidence arming|verify` mode of the bundle, which uses its own embedded kernel because the catalogue's kernel is not adopted yet); `xema edition release resolve` and `check-existing` take `--cosign <absolute path>` for it.
|
|
178
179
|
- `oras` is located with `XEMA_ORAS_BIN` or `PATH` and must hash to a pinned digest. It runs with a scrubbed environment, so proxy and CA variables do not reach it.
|
|
179
180
|
|
|
181
|
+
## Previous seal
|
|
182
|
+
|
|
183
|
+
A write of `images.lock.json` (`resolve-images`, `xema edition develop resolve`) takes exactly one of `--previous-lock <lock> --previous-lock-sha256 <hex>` or `--first-seal`; the CLI refuses unless the lock's bytes hash to the digest, which the caller takes from the images.lock layer of the signed, published edition (the latest, under the per-edition concurrency group). The in-repo resolve and pin/unpin take the committed `images.lock.json` at the target path as the previous lock (first seal when absent). The the composed artifacts' generations carry forward from it.
|
|
184
|
+
|
|
180
185
|
## Kernel protocol and pins
|
|
181
186
|
|
|
182
|
-
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39 and
|
|
187
|
+
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40 and v41 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
|
|
183
188
|
|
|
184
189
|
A develop edition does not take the kernel's develop head blindly: `xema edition develop choose-kernel --kernel <package dir> [--kernel-pin <40-hex>] --out <file>` reads the declared protocol of each commit on the checkout's first-parent history (from source, unbuilt) and chooses the newest one this CLI supports. It writes `{chosenSha, headSha, held, reason}` (`reason`: `head-supported` | `head-ahead` | `explicit-pin`; `held` is `head-ahead`), and when held prints `kernel held at v<N> (<sha12>), head at v<M> (<sha12>): raise the CLI` (stdout only, never persisted). The head is the checkout's `refs/remotes/origin/develop` (a full-history checkout). An explicit pin wins but is still checked, and must be on that head's first-parent history (`not-on-develop` otherwise); when no commit is supported it refuses (`EditionProtocolMismatchError`, `no-supported-commit`; the walk ends at the first commit with no declaration file) and never falls back to the head; a declaration that exists but does not parse, at the head or any walked commit, is refused by name (`unparseable`), never skipped. `freeze-graph` applies the same check to `--kernel-sha`.
|
|
185
190
|
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
evidenceDirectoryForDigest,
|
|
67
67
|
sha256Digest,
|
|
68
68
|
} from './image-evidence.mjs';
|
|
69
|
+
import { evidenceSubjectKey } from './image-subject-identity.mjs';
|
|
69
70
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
70
71
|
import {
|
|
71
72
|
assertProducerInput,
|
|
@@ -124,6 +125,29 @@ function assertContainedRelativePath(value, label) {
|
|
|
124
125
|
*
|
|
125
126
|
* @returns {{ evidenceDirectory: string, reusable: boolean, reason: string }}
|
|
126
127
|
*/
|
|
128
|
+
/**
|
|
129
|
+
* The key of an index entry: its subject's evidence identity, which for a composed artifact names the generation, so it is read
|
|
130
|
+
* from the statement the entry points at (the index states only kind and id). An entry whose statement cannot be read keeps
|
|
131
|
+
* `kind\0id`; the unusable statement is reported below.
|
|
132
|
+
*/
|
|
133
|
+
export function entryKey(entry, absoluteDirectory) {
|
|
134
|
+
try {
|
|
135
|
+
const { predicate } = JSON.parse(
|
|
136
|
+
readBoundedFile(join(absoluteDirectory, assertContainedRelativePath(entry.evidence.statementPath, 'statementPath'))).toString('utf8'),
|
|
137
|
+
);
|
|
138
|
+
return evidenceSubjectKey({
|
|
139
|
+
kind: entry.subjectKind,
|
|
140
|
+
id: entry.subjectId,
|
|
141
|
+
imageRef: predicate.artifact.imageRepository,
|
|
142
|
+
contentHash: predicate.source.sourceTreeHash,
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
145
|
+
return `${entry.subjectKind}\0${entry.subjectId}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const printable = (key) => key.replaceAll('\0', ':');
|
|
150
|
+
|
|
127
151
|
export async function assessCommittedEvidence({
|
|
128
152
|
input,
|
|
129
153
|
repositoryRoot,
|
|
@@ -190,7 +214,12 @@ export async function assessCommittedEvidence({
|
|
|
190
214
|
}
|
|
191
215
|
const expectedByKey = new Map(
|
|
192
216
|
expectedStatements.map((statement) => [
|
|
193
|
-
|
|
217
|
+
evidenceSubjectKey({
|
|
218
|
+
kind: statement.predicate.source.kind,
|
|
219
|
+
id: statement.predicate.source.id,
|
|
220
|
+
imageRef: statement.predicate.artifact.imageRepository,
|
|
221
|
+
contentHash: statement.predicate.source.sourceTreeHash,
|
|
222
|
+
}),
|
|
194
223
|
statement,
|
|
195
224
|
]),
|
|
196
225
|
);
|
|
@@ -200,20 +229,20 @@ export async function assessCommittedEvidence({
|
|
|
200
229
|
if (entry === null || typeof entry !== 'object') {
|
|
201
230
|
return unusable('producer-evidence.json carries a non-object entry');
|
|
202
231
|
}
|
|
203
|
-
const key =
|
|
232
|
+
const key = entryKey(entry, absoluteDirectory);
|
|
204
233
|
if (seen.has(key)) {
|
|
205
|
-
return unusable(`producer-evidence.json duplicates subject ${key}`);
|
|
234
|
+
return unusable(`producer-evidence.json duplicates subject ${printable(key)}`);
|
|
206
235
|
}
|
|
207
236
|
seen.add(key);
|
|
208
237
|
const expected = expectedByKey.get(key);
|
|
209
238
|
if (expected === undefined) {
|
|
210
239
|
return unusable(
|
|
211
|
-
`committed evidence carries subject ${key}, which this input does not evidence`,
|
|
240
|
+
`committed evidence carries subject ${printable(key)}, which this input does not evidence`,
|
|
212
241
|
);
|
|
213
242
|
}
|
|
214
243
|
const descriptor = entry.evidence;
|
|
215
244
|
if (descriptor === null || typeof descriptor !== 'object') {
|
|
216
|
-
return unusable(`subject ${key} has no evidence descriptor`);
|
|
245
|
+
return unusable(`subject ${printable(key)} has no evidence descriptor`);
|
|
217
246
|
}
|
|
218
247
|
let statementBytes;
|
|
219
248
|
let bundleBytes;
|
|
@@ -223,7 +252,7 @@ export async function assessCommittedEvidence({
|
|
|
223
252
|
absoluteDirectory,
|
|
224
253
|
assertContainedRelativePath(
|
|
225
254
|
descriptor.statementPath,
|
|
226
|
-
`subject ${key} statementPath`,
|
|
255
|
+
`subject ${printable(key)} statementPath`,
|
|
227
256
|
),
|
|
228
257
|
),
|
|
229
258
|
);
|
|
@@ -232,25 +261,25 @@ export async function assessCommittedEvidence({
|
|
|
232
261
|
absoluteDirectory,
|
|
233
262
|
assertContainedRelativePath(
|
|
234
263
|
descriptor.bundlePath,
|
|
235
|
-
`subject ${key} bundlePath`,
|
|
264
|
+
`subject ${printable(key)} bundlePath`,
|
|
236
265
|
),
|
|
237
266
|
),
|
|
238
267
|
);
|
|
239
268
|
} catch (error) {
|
|
240
|
-
return unusable(`subject ${key} artifacts are unreadable: ${error.message}`);
|
|
269
|
+
return unusable(`subject ${printable(key)} artifacts are unreadable: ${error.message}`);
|
|
241
270
|
}
|
|
242
271
|
if (
|
|
243
272
|
sha256Digest(statementBytes) !== descriptor.statementDigest ||
|
|
244
273
|
sha256Digest(bundleBytes) !== descriptor.bundleDigest
|
|
245
274
|
) {
|
|
246
275
|
return unusable(
|
|
247
|
-
`subject ${key} artifacts do not match the digests producer-evidence.json records`,
|
|
276
|
+
`subject ${printable(key)} artifacts do not match the digests producer-evidence.json records`,
|
|
248
277
|
);
|
|
249
278
|
}
|
|
250
279
|
try {
|
|
251
280
|
validateProducedBundle(statementBytes, bundleBytes);
|
|
252
281
|
} catch (error) {
|
|
253
|
-
return unusable(`subject ${key} bundle is malformed: ${error.message}`);
|
|
282
|
+
return unusable(`subject ${printable(key)} bundle is malformed: ${error.message}`);
|
|
254
283
|
}
|
|
255
284
|
let committed;
|
|
256
285
|
try {
|
|
@@ -259,7 +288,7 @@ export async function assessCommittedEvidence({
|
|
|
259
288
|
);
|
|
260
289
|
} catch (error) {
|
|
261
290
|
return unusable(
|
|
262
|
-
`subject ${key} statement does not parse under the kernel schema: ${error.message}`,
|
|
291
|
+
`subject ${printable(key)} statement does not parse under the kernel schema: ${error.message}`,
|
|
263
292
|
);
|
|
264
293
|
}
|
|
265
294
|
// producerRun is deliberately fresh on every execution (run id, attempt,
|
|
@@ -269,24 +298,24 @@ export async function assessCommittedEvidence({
|
|
|
269
298
|
// validate the existing run's own signature and lifetime independently.
|
|
270
299
|
if (!producerStatementMatchesInput(committed, expected)) {
|
|
271
300
|
return unusable(
|
|
272
|
-
`subject ${key} committed statement is not bound to this producer input ` +
|
|
301
|
+
`subject ${printable(key)} committed statement is not bound to this producer input ` +
|
|
273
302
|
'(authority, source, build, artifact or platform set changed)',
|
|
274
303
|
);
|
|
275
304
|
}
|
|
276
305
|
const expiresAt = Date.parse(committed.predicate.producerRun.expiresAt);
|
|
277
306
|
if (!Number.isFinite(expiresAt)) {
|
|
278
|
-
return unusable(`subject ${key} statement has an unreadable expiresAt`);
|
|
307
|
+
return unusable(`subject ${printable(key)} statement has an unreadable expiresAt`);
|
|
279
308
|
}
|
|
280
309
|
const remainingSeconds = Math.floor((expiresAt - now) / 1000);
|
|
281
310
|
if (remainingSeconds <= 0) {
|
|
282
311
|
return unusable(
|
|
283
|
-
`subject ${key} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
|
|
312
|
+
`subject ${printable(key)} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
|
|
284
313
|
'it can never verify again and must be re-produced',
|
|
285
314
|
);
|
|
286
315
|
}
|
|
287
316
|
if (remainingSeconds < MIN_REMAINING_TTL_SECONDS) {
|
|
288
317
|
return unusable(
|
|
289
|
-
`subject ${key} evidence expires in ${remainingSeconds}s, under the ` +
|
|
318
|
+
`subject ${printable(key)} evidence expires in ${remainingSeconds}s, under the ` +
|
|
290
319
|
`${MIN_REMAINING_TTL_SECONDS}s reuse floor — refreshing it now`,
|
|
291
320
|
);
|
|
292
321
|
}
|
|
@@ -183,15 +183,20 @@ const ENGINE_IMAGE_FLAG = '--engine-image';
|
|
|
183
183
|
const ENGINE_REPOSITORY_FLAG = '--engine-repository';
|
|
184
184
|
const ENGINE_SIGNER_FLAG = '--engine-signer';
|
|
185
185
|
export const IMAGES_FROM_FLAG = '--images-from';
|
|
186
|
+
export const PREVIOUS_LOCK_FLAG = '--previous-lock';
|
|
187
|
+
export const PREVIOUS_LOCK_SHA256_FLAG = '--previous-lock-sha256';
|
|
188
|
+
export const FIRST_SEAL_FLAG = '--first-seal';
|
|
186
189
|
|
|
187
190
|
const COMMANDS = Object.freeze({
|
|
188
191
|
resolve: {
|
|
189
192
|
positional: true,
|
|
190
193
|
// --images-from: the everyday job's image build record (images-from-record.mjs) instead of the registry probe; vendor only.
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
// --previous-lock with --previous-lock-sha256 (the digest of the latest signed, published edition's images.lock layer, passed by the workflow under
|
|
195
|
+
// the per-edition concurrency group) or --first-seal: the previous seal whose composed artifacts carry their generation forward; vendor only.
|
|
196
|
+
values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG],
|
|
197
|
+
switches: ['--local', FIRST_SEAL_FLAG],
|
|
193
198
|
required: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
|
|
194
|
-
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG], cosign: true },
|
|
199
|
+
customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, FIRST_SEAL_FLAG], cosign: true },
|
|
195
200
|
// A vendor resolve finds its engine itself; the old flag is refused by name, never read as a customer flag.
|
|
196
201
|
removedFromVendor: { [ENGINE_IMAGE_FLAG]: `use ${ENGINE_REPOSITORY_FLAG} and ${ENGINE_SIGNER_FLAG}` },
|
|
197
202
|
},
|
|
@@ -210,7 +215,7 @@ const COMMANDS = Object.freeze({
|
|
|
210
215
|
},
|
|
211
216
|
'freeze-graph': {
|
|
212
217
|
positional: false,
|
|
213
|
-
values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
|
|
218
|
+
values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--member-pins', '--out'],
|
|
214
219
|
switches: [],
|
|
215
220
|
required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
|
|
216
221
|
},
|
|
@@ -233,6 +238,12 @@ const COMMANDS = Object.freeze({
|
|
|
233
238
|
switches: [],
|
|
234
239
|
required: ['--branch', '--registry', '--cosign', '--out'],
|
|
235
240
|
},
|
|
241
|
+
'image-record': {
|
|
242
|
+
positional: true,
|
|
243
|
+
values: ['--selection', '--pins', '--out'],
|
|
244
|
+
switches: [],
|
|
245
|
+
required: ['--selection', '--pins', '--out'],
|
|
246
|
+
},
|
|
236
247
|
'verify-image-record': {
|
|
237
248
|
positional: false,
|
|
238
249
|
values: ['--image', '--producer', '--registry', '--cosign'],
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// develop-current-resolution — the members that keep the CURRENT develop resolution instead of a signed build record.
|
|
2
|
+
//
|
|
3
|
+
// A member keeps its current resolution (develop HEAD freeze plus the registry probe) only when the data root declares
|
|
4
|
+
// it: `currentResolution: "<ticket>"` on its `firstPartySources` entry in edition-workspace.json, the ticket that
|
|
5
|
+
// removes the exception (today xema-docs, whose develop lane is unsigned by owner decision and which leaves the
|
|
6
|
+
// edition). It is a declared exception, not a fallback: absent means none, and every other member without a record
|
|
7
|
+
// still refuses.
|
|
8
|
+
|
|
9
|
+
import { compareStrings } from './lib.mjs';
|
|
10
|
+
|
|
11
|
+
/** Whether a member (or its `firstPartySources` entry) keeps its current resolution. */
|
|
12
|
+
export const keepsCurrentResolution = (member) => Object.hasOwn(member, 'currentResolution');
|
|
13
|
+
|
|
14
|
+
/** The graph `members` that declare a current resolution, repo -> ticket, keys sorted: what a selection records. */
|
|
15
|
+
export function currentResolutionOf(members) {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
members
|
|
18
|
+
.filter(keepsCurrentResolution)
|
|
19
|
+
.map(({ repo, currentResolution }) => [repo, currentResolution])
|
|
20
|
+
.sort(([a], [b]) => compareStrings(a, b)),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Why a selection's `currentResolution` (absent = none) is not exactly the one the graph `members` declare; null when it is. */
|
|
25
|
+
export function currentResolutionProblem(declared, members) {
|
|
26
|
+
const expected = currentResolutionOf(members);
|
|
27
|
+
if (JSON.stringify(declared ?? {}) === JSON.stringify(expected)) {return null;}
|
|
28
|
+
return `"currentResolution" is ${JSON.stringify(declared)}, not ${JSON.stringify(expected)}, the members the data root declares keep their current resolution`;
|
|
29
|
+
}
|
|
@@ -79,8 +79,9 @@ export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION
|
|
|
79
79
|
* @param {object} request
|
|
80
80
|
* @param {object} [request.imagesFrom] a parsed image build record (images-from-record.mjs): the digests are the record's,
|
|
81
81
|
* with no probe and no rehash, and `verifySatellites` (createSatelliteVerifier) admits its satellite entries
|
|
82
|
+
* @param {object|null} [request.previousLock] the previous edition's parsed images lock: its composed artifacts carry their generation forward (null: generation 1)
|
|
82
83
|
*/
|
|
83
|
-
export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites }) {
|
|
84
|
+
export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites, previousLock = null }) {
|
|
84
85
|
if ((imagesFrom === undefined) !== (verifySatellites === undefined)) {
|
|
85
86
|
throw new Error('resolveDevelopEdition takes an image build record and its satellite verifier together, or neither');
|
|
86
87
|
}
|
|
@@ -135,7 +136,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
|
|
|
135
136
|
let recorded;
|
|
136
137
|
const imageLock = await checked(DevelopResolveCheck.DevelopImages, async () => {
|
|
137
138
|
if (imagesFrom !== undefined) {
|
|
138
|
-
const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites });
|
|
139
|
+
const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites, previousLock });
|
|
139
140
|
({ recorded } = fromRecord);
|
|
140
141
|
phase(`image subjects taken from the build record (${fromRecord.imageLock.subjects.length} subjects); satellites verified`);
|
|
141
142
|
return fromRecord.imageLock;
|
|
@@ -148,7 +149,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
|
|
|
148
149
|
...(sealOptions.run === undefined ? {} : { run: sealOptions.run }),
|
|
149
150
|
progress,
|
|
150
151
|
});
|
|
151
|
-
const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe });
|
|
152
|
+
const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe, previousLock });
|
|
152
153
|
phase(`image content hashes computed (${resolved.imageLock.subjects.length} subjects)`);
|
|
153
154
|
await probe.settle();
|
|
154
155
|
phase('every develop image is present');
|
|
@@ -41,10 +41,14 @@
|
|
|
41
41
|
// the same for a customer edition (id = the definition's; the tag covers the pinned catalogue digest).
|
|
42
42
|
// freeze-graph --data-sha <sha> --kernel-sha <sha>
|
|
43
43
|
// --deploy-sha <sha> [--editions-root-url <https url>]
|
|
44
|
-
// [--overrides <json>] --out <file>
|
|
44
|
+
// [--overrides <json> | --member-pins <selection file>] --out <file>
|
|
45
45
|
// resolve refs/heads/develop once per develop-graph repository (the data root's
|
|
46
46
|
// firstPartySources, at each declared URL) and the editions root; the three checked-out
|
|
47
|
-
// repositories are pinned at what the workflow checked out; write the pins. --
|
|
47
|
+
// repositories are pinned at what the workflow checked out; write the pins. --member-pins takes a select-records
|
|
48
|
+
// selection (only .pins, 40-hex, is read) and pins each graph member at its selected record commit instead of its
|
|
49
|
+
// develop head: the selection must pin exactly the graph minus its currentResolution members (which must equal
|
|
50
|
+
// the graph members' declared currentResolution), a missing or extra member is refused by name, and a currentResolution
|
|
51
|
+
// member is pinned at its develop head. --member-pins together with --overrides is refused. --kernel-sha is refused
|
|
48
52
|
// unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
|
|
49
53
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
50
54
|
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
// for every develop-graph member, take the newest build record the data root's record workflow signed for
|
|
72
76
|
// the branch (<root>/build-records/<name>, verified offline by digest against the pinned trusted root, as
|
|
73
77
|
// signed by <data root>/.github/workflows/record-build.yaml at refs/heads/<branch>, called by the member at the
|
|
74
|
-
// record's own commit) and write { pins, snapshot, images }, once (wx). Any missing, unsigned, mismatched or
|
|
78
|
+
// record's own commit) and write { pins, snapshot, behind, images, currentResolution }, once (wx); a member that keeps its current resolution (a declared currentResolution, develop-current-resolution.mjs) is skipped with no registry read, listed in currentResolution, and printed as `xema-docs: current resolution (leaves the edition, XEMA-2373)`; behind is { <repo>: { head, commits } }, develop's head and how many commits the selected one trails it by, and one line per member `<short name>: record <commit>, <N> commits behind HEAD` goes to stdout (always `commits`; short name = the repo's last path segment, refused when two members share one); images[] entries carry `member`, the owning repo handle. Any missing, unsigned, mismatched or
|
|
75
79
|
// unreadable record refuses the whole run and writes nothing: a newer bad record is never skipped for an older
|
|
76
80
|
// one. --at takes the newest record completed at or before that time. Only `develop` has a declared signer; the
|
|
77
81
|
// data root must declare dataRootRepositoryUrl. Takes the roots `resolve` takes (no registry root is implied).
|
|
@@ -83,6 +87,14 @@
|
|
|
83
87
|
// part 07 passes the flag.
|
|
84
88
|
// Residual: anyone who can delete registry tags can remove newer records, and this then pins the newest remaining
|
|
85
89
|
// valid one; the guard is part 07's, deploy-latest's owner refuses a snapshot that goes backwards.
|
|
90
|
+
// image-record <id> --selection <file> --pins <file> --out <file>
|
|
91
|
+
// write the edition's image build record (the file `resolve --images-from` reads) from a select-records selection:
|
|
92
|
+
// the subjects are the checked-out snapshot's first-party image subjects (enumerated as `coverage` does). A subject of
|
|
93
|
+
// a member that keeps its current resolution (a declared currentResolution, develop-current-resolution.mjs) is probed as resolve probes it
|
|
94
|
+
// and goes in `images`; every other first-party subject takes the selection's image of its develop repository, whose
|
|
95
|
+
// selected commit must be the snapshot's, and goes in `satellites`. Platforms are read from the registry by digest.
|
|
96
|
+
// Any subject without an image, any commit mismatch refuses the whole run and writes nothing, once (wx). Third-party
|
|
97
|
+
// subjects are ignored. Takes the roots `resolve` takes.
|
|
86
98
|
// choose-kernel [--kernel-pin <40-hex>] --out <file>
|
|
87
99
|
// choose the develop kernel commit in the --kernel checkout (full history; its refs/remotes/origin/develop is the develop head):
|
|
88
100
|
// the newest first-parent commit whose declared edition protocol this CLI supports
|
|
@@ -106,14 +118,17 @@
|
|
|
106
118
|
import { resolve } from 'node:path';
|
|
107
119
|
|
|
108
120
|
import { writeCatalogue } from './catalogue.mjs';
|
|
109
|
-
import { CUSTOMER_ROOTS, IMAGES_FROM_FLAG, headResolver, parseEditionVerbArgs, runCustomerVerb, writeOnce } from './customer-edition-verbs.mjs';
|
|
121
|
+
import { CUSTOMER_ROOTS, FIRST_SEAL_FLAG, IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, headResolver, parseEditionVerbArgs, runCustomerVerb, writeOnce } from './customer-edition-verbs.mjs';
|
|
110
122
|
import { DEFINITION_FLAG } from './customer-flags.mjs';
|
|
111
123
|
import { runDevelopCoverage } from './develop-coverage-run.mjs';
|
|
112
124
|
import { CoverageExit } from './develop-coverage.mjs';
|
|
113
125
|
import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
114
126
|
import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
|
|
115
127
|
import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
|
|
128
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
129
|
+
import { runImageRecord } from './develop-image-record.mjs';
|
|
116
130
|
import { runSelectRecords } from './develop-select-records.mjs';
|
|
131
|
+
import { readSelectionPins } from './develop-selection-pins.mjs';
|
|
117
132
|
import { runVerifyImageRecord } from './develop-verify-image-record.mjs';
|
|
118
133
|
import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
119
134
|
import { EditionChannel, EditionKind } from './edition-constants.mjs';
|
|
@@ -124,6 +139,7 @@ import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protoc
|
|
|
124
139
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
125
140
|
import { snapshotPinSource } from './pin-source.mjs';
|
|
126
141
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
142
|
+
import { resolvePreviousSeal } from './resolve-images.mjs';
|
|
127
143
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
128
144
|
|
|
129
145
|
// The roots a verb takes. A customer form (it names --definition) takes fewer, and never the kernel (the
|
|
@@ -167,8 +183,8 @@ function parseOverrides(text) {
|
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
/** The target-tag input of `--images-from`: the record's digest, or nothing on the probe path. */
|
|
170
|
-
function imageBuildRecordTagInput(options) {
|
|
171
|
-
return options[IMAGES_FROM_FLAG] === undefined ? {} : { imageBuildRecordDigest: imageBuildRecordDigest(readImageBuildRecord(options[IMAGES_FROM_FLAG])) };
|
|
186
|
+
async function imageBuildRecordTagInput(options) {
|
|
187
|
+
return options[IMAGES_FROM_FLAG] === undefined ? {} : { imageBuildRecordDigest: imageBuildRecordDigest(await readImageBuildRecord(options[IMAGES_FROM_FLAG])) };
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
async function run(argv, env) {
|
|
@@ -176,7 +192,7 @@ async function run(argv, env) {
|
|
|
176
192
|
switch (command) {
|
|
177
193
|
case 'target': {
|
|
178
194
|
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
179
|
-
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'], ...imageBuildRecordTagInput(options) }));
|
|
195
|
+
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'], ...(await imageBuildRecordTagInput(options)) }));
|
|
180
196
|
return;
|
|
181
197
|
}
|
|
182
198
|
case 'resolve': {
|
|
@@ -190,12 +206,13 @@ async function run(argv, env) {
|
|
|
190
206
|
env,
|
|
191
207
|
engine: { repository: options['--engine-repository'], signer: options['--engine-signer'] },
|
|
192
208
|
cosignBinary: options['--cosign'],
|
|
209
|
+
previousLock: await resolvePreviousSeal({ distributionId: id, previousLockPath: options[PREVIOUS_LOCK_FLAG], previousLockSha256: options[PREVIOUS_LOCK_SHA256_FLAG], firstSeal: Boolean(options[FIRST_SEAL_FLAG]) }),
|
|
193
210
|
...(options[IMAGES_FROM_FLAG] === undefined ? {} : {
|
|
194
|
-
imagesFrom: readImageBuildRecord(options[IMAGES_FROM_FLAG]),
|
|
211
|
+
imagesFrom: await readImageBuildRecord(options[IMAGES_FROM_FLAG]),
|
|
195
212
|
verifySatellites: createSatelliteVerifier({ registry: options['--registry'], cosignBinary: options['--cosign'], env }),
|
|
196
213
|
}),
|
|
197
214
|
});
|
|
198
|
-
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage, ...imageBuildRecordTagInput(options) });
|
|
215
|
+
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage, ...(await imageBuildRecordTagInput(options)) });
|
|
199
216
|
const outDir = resolve(options['--out']);
|
|
200
217
|
writeEditionArtifact({
|
|
201
218
|
outDir,
|
|
@@ -232,6 +249,9 @@ async function run(argv, env) {
|
|
|
232
249
|
case 'select-records':
|
|
233
250
|
await runSelectRecords({ options, env });
|
|
234
251
|
return;
|
|
252
|
+
case 'image-record':
|
|
253
|
+
await runImageRecord({ id, options, env });
|
|
254
|
+
return;
|
|
235
255
|
case 'verify-image-record':
|
|
236
256
|
await runVerifyImageRecord({ options, env });
|
|
237
257
|
return;
|
|
@@ -265,6 +285,7 @@ async function run(argv, env) {
|
|
|
265
285
|
deploySha: options['--deploy-sha'],
|
|
266
286
|
editionsRootUrl: options['--editions-root-url'],
|
|
267
287
|
overrides: parseOverrides(options['--overrides']),
|
|
288
|
+
...(options['--member-pins'] === undefined ? {} : { memberPins: readSelectionPins(options['--member-pins'], '--member-pins', developGraphMembers()) }),
|
|
268
289
|
...headResolver(EditionChannel.Develop, env),
|
|
269
290
|
});
|
|
270
291
|
writeOnce(options['--out'], pins);
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// freezeGraph the graph repositories, the data root, the kernel, the deploy checkout, the editions root
|
|
10
10
|
// freezeSources one pin per `sources[]` entry the chain declares
|
|
11
11
|
//
|
|
12
|
+
// With `memberPins` (a select-records selection) the members are pinned at the selected build records instead; the one
|
|
13
|
+
// declared exception, a member that keeps its current resolution, is pinned at its develop head.
|
|
14
|
+
//
|
|
12
15
|
// `resolveHead({ name, url })` is the one network seam (default: `git ls-remote` through
|
|
13
16
|
// git-source-auth: the data root's token only for a repository under its
|
|
14
17
|
// `sourceRepositoryUrlPrefix`, no credential for any other, never argv).
|
|
@@ -16,6 +19,7 @@
|
|
|
16
19
|
import { execFileSync } from 'node:child_process';
|
|
17
20
|
|
|
18
21
|
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
22
|
+
import { currentResolutionOf } from './develop-current-resolution.mjs';
|
|
19
23
|
import { developGraphMembers } from './develop-graph.mjs';
|
|
20
24
|
import { EditionChannel } from './edition-channel.mjs';
|
|
21
25
|
import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
|
|
@@ -124,6 +128,17 @@ function assertOverrides(overrides, allowed) {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
/** The member pins name exactly the graph minus the current-resolution members; a missing or extra member is refused by name. */
|
|
132
|
+
function assertMemberPinsAreTheGraph(memberPins, members) {
|
|
133
|
+
const current = new Set(Object.keys(currentResolutionOf(members)));
|
|
134
|
+
const expected = members.map(({ repo }) => repo).filter((repo) => !current.has(repo));
|
|
135
|
+
const missing = expected.filter((repo) => !Object.hasOwn(memberPins, repo));
|
|
136
|
+
const extra = Object.keys(memberPins).filter((repo) => !expected.includes(repo));
|
|
137
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
138
|
+
throw new Error(`--member-pins must pin exactly the develop graph minus ${JSON.stringify([...current])}: missing ${JSON.stringify(missing)}, extra ${JSON.stringify(extra)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
127
142
|
const GRAPH_PHASE = new Set([
|
|
128
143
|
SnapshotPinNamespace.Graph,
|
|
129
144
|
SnapshotPinNamespace.Distributions,
|
|
@@ -149,6 +164,9 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
|
149
164
|
* @param {string} request.kernelSha the commit the kernel checkout is at
|
|
150
165
|
* @param {string} request.deploySha the commit the deploy checkout is at
|
|
151
166
|
* @param {string} [request.editionsRootUrl] https URL of the editions root; absent = this repository
|
|
167
|
+
* @param {Record<string, string>} [request.memberPins] the graph members' pins from a selection (`--member-pins`): exactly the
|
|
168
|
+
* graph minus the members that keep their current resolution (develop-current-resolution.mjs), which are pinned at their
|
|
169
|
+
* develop head; refused together with overrides
|
|
152
170
|
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
|
|
153
171
|
*/
|
|
154
172
|
export function freezeGraph({
|
|
@@ -157,9 +175,13 @@ export function freezeGraph({
|
|
|
157
175
|
deploySha,
|
|
158
176
|
editionsRootUrl,
|
|
159
177
|
overrides = {},
|
|
178
|
+
memberPins,
|
|
160
179
|
resolveHead,
|
|
161
180
|
holdSource,
|
|
162
181
|
}) {
|
|
182
|
+
if (memberPins !== undefined && Object.keys(overrides).length > 0) {
|
|
183
|
+
throw new Error('--member-pins and --overrides are exclusive: the member pins are the selected build records, never overridden');
|
|
184
|
+
}
|
|
163
185
|
assertNoToolingOverride(overrides);
|
|
164
186
|
assertOverrides(overrides, GRAPH_PHASE);
|
|
165
187
|
const head = memoizedHeads(resolveHead);
|
|
@@ -171,6 +193,7 @@ export function freezeGraph({
|
|
|
171
193
|
throw new Error(`override names "${key}", which is not a repository in the graph`);
|
|
172
194
|
}
|
|
173
195
|
}
|
|
196
|
+
if (memberPins !== undefined) {assertMemberPinsAreTheGraph(memberPins, members);}
|
|
174
197
|
const checkedOut = [
|
|
175
198
|
{ key: dataPinKey(), sha: dataSha, flag: '--data-sha', what: 'the data root checkout' },
|
|
176
199
|
{ key: kernelPinKey(), sha: kernelSha, flag: '--kernel-sha', what: 'the kernel checkout' },
|
|
@@ -188,7 +211,7 @@ export function freezeGraph({
|
|
|
188
211
|
}
|
|
189
212
|
const pins = Object.fromEntries(checkedOut.map(({ key, sha }) => [key, sha]));
|
|
190
213
|
for (const member of members) {
|
|
191
|
-
pins[member.repo] = overrides[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
|
|
214
|
+
pins[member.repo] = overrides[member.repo] ?? memberPins?.[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
|
|
192
215
|
}
|
|
193
216
|
if (editionsRootUrl !== undefined) {
|
|
194
217
|
const url = canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url');
|
|
@@ -13,14 +13,16 @@ import { compareStrings } from './lib.mjs';
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The develop graph: `{ repo, repositoryUrl }` per declared first-party
|
|
16
|
-
* repository, sorted by repo handle
|
|
16
|
+
* repository, sorted by repo handle, plus `currentResolution` (its ticket) on a
|
|
17
|
+
* member the data root declares one for (develop-current-resolution.mjs).
|
|
17
18
|
*
|
|
18
|
-
* @returns {readonly { repo: string, repositoryUrl: string }[]}
|
|
19
|
+
* @returns {readonly { repo: string, repositoryUrl: string, currentResolution?: string }[]}
|
|
19
20
|
*/
|
|
20
21
|
export function developGraphMembers() {
|
|
21
22
|
return Object.freeze(
|
|
22
23
|
Object.entries(firstPartySources('the develop graph'))
|
|
23
|
-
.map(([repo, { repositoryUrl }]) =>
|
|
24
|
+
.map(([repo, { repositoryUrl, currentResolution }]) =>
|
|
25
|
+
Object.freeze({ repo, repositoryUrl, ...(currentResolution === undefined ? {} : { currentResolution }) }))
|
|
24
26
|
.sort((a, b) => compareStrings(a.repo, b.repo)),
|
|
25
27
|
);
|
|
26
28
|
}
|