@xemahq/xema 0.53.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/README.md +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +66 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/edition.d.ts.map +1 -1
- package/dist/commands/edition.js +1 -0
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +2062 -1025
- package/dist/edition/verify-image-evidence.bundle.provenance.json +595 -505
- package/dist/runtime/edition/verb-map.d.ts +2 -1
- package/dist/runtime/edition/verb-map.d.ts.map +1 -1
- package/dist/runtime/edition/verb-map.js +2 -0
- package/dist/runtime/edition/verb-map.js.map +1 -1
- 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 +61 -4
- package/edition/adapter-bundled-versions.mjs +2 -1
- package/edition/authorization-base.mjs +9 -8
- package/edition/biome-content.mjs +23 -29
- package/edition/candidate-edition.mjs +692 -0
- package/edition/check-committed-image-evidence.mjs +44 -15
- package/edition/checkout-indexed-source-repositories.mjs +6 -8
- package/edition/collected-refusals.mjs +59 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +18 -8
- package/edition/customer-edition-verbs.mjs +43 -10
- package/edition/develop-chart-set.mjs +7 -6
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +2 -1
- package/edition/develop-edition-resolve.mjs +110 -31
- package/edition/develop-edition.mjs +73 -11
- package/edition/develop-engine-image.mjs +95 -0
- package/edition/develop-freeze.mjs +24 -1
- package/edition/develop-graph.mjs +5 -3
- package/edition/develop-image-probe.mjs +8 -5
- package/edition/develop-image-record.mjs +194 -0
- package/edition/develop-record-ancestry.mjs +23 -5
- package/edition/develop-resolve-report.mjs +159 -0
- 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/develop-snapshot-inputs.mjs +4 -3
- package/edition/develop-verify-image-record.mjs +23 -4
- package/edition/dist-check.mjs +45 -9
- package/edition/distribution-evidence-set.mjs +14 -9
- package/edition/edition-artifact.mjs +17 -7
- package/edition/edition-image-seal.mjs +2 -1
- package/edition/edition-protocol.mjs +8 -19
- 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 +298 -0
- package/edition/lib.mjs +25 -45
- package/edition/locked-bundle-signer.mjs +15 -3
- package/edition/operation-grant-admission.mjs +51 -9
- package/edition/resolve-customer-edition.mjs +1 -1
- package/edition/resolve-distribution.mjs +95 -82
- package/edition/resolve-edition.mjs +2 -1
- package/edition/resolve-images.mjs +233 -14
- package/edition/seal-image-digests.mjs +24 -13
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/verify-image-platforms.mjs +2 -0
- package/edition/workspace-sections.mjs +33 -3
- package/package.json +7 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// develop-edition-resolve — resolve and seal one DEVELOP edition from a snapshot.
|
|
2
2
|
//
|
|
3
|
-
// The whole pipeline, in the order the release runs it, over the snapshot's
|
|
3
|
+
// The whole pipeline, in the order the release runs it (every check reports its refusal; see develop-resolve-report.mjs), over the snapshot's
|
|
4
4
|
// injected inputs: distribution lock -> plane invariants (reservation table,
|
|
5
5
|
// confinement; develop-edition-invariants.mjs) -> authorization base (composed, never
|
|
6
6
|
// written) -> images lock -> develop seal, plus the chart-set lock: the frozen
|
|
@@ -12,15 +12,21 @@ import { biomeSourceTreeIds, readBiomeSourceFacts, sealLock } from './biome-cont
|
|
|
12
12
|
import { buildCatalogue } from './catalogue.mjs';
|
|
13
13
|
import { resolveDevelopChartSet } from './develop-chart-set.mjs';
|
|
14
14
|
import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
|
|
15
|
+
import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
|
|
15
16
|
import { createDevelopImageProbe } from './develop-image-probe.mjs';
|
|
17
|
+
import { BLOCKED, DevelopResolveCheck, RESOLVE_CHECK_DEPENDENCIES, createResolveReport } from './develop-resolve-report.mjs';
|
|
16
18
|
import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
|
|
17
19
|
import { EditionChannel } from './edition-constants.mjs';
|
|
18
20
|
import { sealImages } from './edition-image-seal.mjs';
|
|
21
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
19
22
|
import { dataRoot, kernelPackageDir } from './edition-workspace.mjs';
|
|
23
|
+
import { resolveImagesFromRecord, sealImagesFromRecord } from './images-from-record.mjs';
|
|
20
24
|
import { kernelPackageOfAuthority } from './kernel-package.mjs';
|
|
21
25
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
26
|
+
import { assertLockBundleSigners, isBundleSignerRefusal } from './locked-bundle-signer.mjs';
|
|
27
|
+
import { admitDistributionLock, ungrantedOptionalLinesOfRefusal } from './operation-grant-admission.mjs';
|
|
22
28
|
import { deployPinKey } from './pin-source.mjs';
|
|
23
|
-
import { composeAuthorizationBase,
|
|
29
|
+
import { admitResolvedLock, composeAuthorizationBase, prepareDistributionWithIndex } from './resolve-distribution.mjs';
|
|
24
30
|
import { externalSourceTrees, resolveImages, sourceTreeLocator } from './resolve-images.mjs';
|
|
25
31
|
import { createPinnedSourceAuthority } from './source-authority.mjs';
|
|
26
32
|
import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs';
|
|
@@ -32,12 +38,15 @@ import { loadTrustedThirdPartySourcePins } from './third-party-pins-trusted.mjs'
|
|
|
32
38
|
export const DEVELOP_EDITION_STEPS = Object.freeze({
|
|
33
39
|
assertFrozenCheckouts,
|
|
34
40
|
buildSnapshotInputs,
|
|
35
|
-
|
|
41
|
+
assertKernelEditionProtocol,
|
|
42
|
+
prepareDistributionWithIndex,
|
|
43
|
+
resolveDevelopEngineImage,
|
|
36
44
|
assertDevelopEditionInvariants,
|
|
37
45
|
assertAdapterBundlesMatchHost,
|
|
38
46
|
composeAuthorizationBase,
|
|
39
47
|
resolveImages,
|
|
40
48
|
sealImages,
|
|
49
|
+
sealImagesFromRecord,
|
|
41
50
|
resolveDevelopChartSet,
|
|
42
51
|
});
|
|
43
52
|
|
|
@@ -45,46 +54,118 @@ const stderrProgress = (line) => process.stderr.write(`${line}\n`);
|
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* The first half of every develop resolve: the frozen snapshot's inputs and the
|
|
48
|
-
* distribution lock composed over them. `resolveDevelopEdition`
|
|
49
|
-
*
|
|
50
|
-
* for a subject is the same by construction.
|
|
57
|
+
* distribution lock composed over them, NOT yet admitted. `resolveDevelopEdition`
|
|
58
|
+
* admits it as one of its checks; `resolveDevelopSnapshot` admits it at once. Both
|
|
59
|
+
* start here, so the content hash each computes for a subject is the same by construction.
|
|
51
60
|
*/
|
|
52
|
-
|
|
61
|
+
async function prepareDevelopSnapshot({ id, pins, steps, phase }) {
|
|
53
62
|
const schemas = await loadKernelDistributionSchemas();
|
|
54
63
|
steps.assertFrozenCheckouts(pins);
|
|
55
64
|
const inputs = await steps.buildSnapshotInputs({ id, pins, schemas });
|
|
56
65
|
phase('snapshot inputs built');
|
|
57
|
-
const
|
|
58
|
-
|
|
66
|
+
const prepared = await steps.prepareDistributionWithIndex(id, { inputs });
|
|
67
|
+
// The schemas the lock was composed with are the ones that admit it.
|
|
68
|
+
return { inputs, ...prepared };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The read-only `coverage` verb's snapshot: prepared as the resolve prepares it, then admitted (a refusal throws). */
|
|
72
|
+
export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION_STEPS, phase = () => {} }) {
|
|
73
|
+
const snapshot = await prepareDevelopSnapshot({ id, pins, steps, phase });
|
|
74
|
+
admitResolvedLock(snapshot.schemas, snapshot.lock);
|
|
75
|
+
return snapshot;
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
/**
|
|
79
|
+
* @param {object} request
|
|
80
|
+
* @param {object} [request.imagesFrom] a parsed image build record (images-from-record.mjs): the digests are the record's,
|
|
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)
|
|
83
|
+
*/
|
|
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 }) {
|
|
85
|
+
if ((imagesFrom === undefined) !== (verifySatellites === undefined)) {
|
|
86
|
+
throw new Error('resolveDevelopEdition takes an image build record and its satellite verifier together, or neither');
|
|
87
|
+
}
|
|
62
88
|
const startedAt = Date.now();
|
|
63
89
|
// One line per phase, with the time since the resolve began, so the step is never silent for long.
|
|
64
90
|
const phase = (name) => progress(`[develop-edition] ${name} (+${Date.now() - startedAt}ms)`);
|
|
65
|
-
|
|
91
|
+
// Every check runs unless a check it needs was refused; all refusals are reported together, and nothing is sealed or written then.
|
|
92
|
+
const report = createResolveReport();
|
|
93
|
+
const checked = (check, fn, options) => report.run(check, RESOLVE_CHECK_DEPENDENCIES[check], fn, options);
|
|
94
|
+
const chartSetLock = await checked(DevelopResolveCheck.ChartSet, async () => {
|
|
95
|
+
const chartSet = await steps.resolveDevelopChartSet({ xemaDeploySha: pins[deployPinKey()] });
|
|
96
|
+
phase('chart set resolved');
|
|
97
|
+
return chartSet;
|
|
98
|
+
});
|
|
99
|
+
const engineImage = await checked(DevelopResolveCheck.EngineImage, async () => {
|
|
100
|
+
const image = await steps.resolveDevelopEngineImage({ pins, engine, cosignBinary, env });
|
|
101
|
+
phase('engine image resolved');
|
|
102
|
+
return image;
|
|
103
|
+
});
|
|
104
|
+
await checked(DevelopResolveCheck.EditionProtocol, () => steps.assertKernelEditionProtocol(kernelPackageDir()));
|
|
105
|
+
const snapshot = await checked(DevelopResolveCheck.DistributionLock, () => prepareDevelopSnapshot({ id, pins, steps, phase }), {
|
|
106
|
+
classify: (error) => (isBundleSignerRefusal(error) ? DevelopResolveCheck.BundleSigner : DevelopResolveCheck.DistributionLock),
|
|
107
|
+
});
|
|
108
|
+
const lockProduced = snapshot !== BLOCKED;
|
|
109
|
+
const { schemas, inputs, lock, index } = lockProduced ? snapshot : {};
|
|
110
|
+
if (lockProduced) {
|
|
111
|
+
await checked(DevelopResolveCheck.BundleSigner, () => assertLockBundleSigners({ lock, schemas }));
|
|
112
|
+
}
|
|
66
113
|
// The plane invariants, against the graph this candidate froze (develop-edition-invariants.mjs).
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
await checked(DevelopResolveCheck.PlaneInvariants, () => {
|
|
115
|
+
const graded = steps.assertDevelopEditionInvariants({ id, lock, biomeIndex: inputs.biomeIndex, schemas });
|
|
116
|
+
phase(`reservation table asserted (${graded.reservedDomains} domains); confinement asserted (${graded.confinedBiomes} declaration(s))`);
|
|
117
|
+
});
|
|
118
|
+
await checked(DevelopResolveCheck.OperationAdmission, () => {
|
|
119
|
+
try {
|
|
120
|
+
admitDistributionLock(schemas, lock, { report: (line) => report.notice(line) });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
ungrantedOptionalLinesOfRefusal(error).forEach((line) => report.notice(line));
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
phase('operation requirements admitted');
|
|
126
|
+
});
|
|
127
|
+
await checked(DevelopResolveCheck.AdapterBundles, () => steps.assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(index), fetchManifest }));
|
|
128
|
+
const authorizationLock = await checked(DevelopResolveCheck.AuthorizationBase, async () => {
|
|
129
|
+
const composed = await steps.composeAuthorizationBase(id, lock, index);
|
|
130
|
+
phase('distribution lock resolved');
|
|
131
|
+
return composed;
|
|
132
|
+
});
|
|
72
133
|
// Every subject's develop image is probed the moment its hash is known, in
|
|
73
134
|
// parallel with the hashing of the rest; an absent image refuses here, naming
|
|
74
135
|
// all of them, before any later phase runs.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
136
|
+
let recorded;
|
|
137
|
+
const imageLock = await checked(DevelopResolveCheck.DevelopImages, async () => {
|
|
138
|
+
if (imagesFrom !== undefined) {
|
|
139
|
+
const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites, previousLock });
|
|
140
|
+
({ recorded } = fromRecord);
|
|
141
|
+
phase(`image subjects taken from the build record (${fromRecord.imageLock.subjects.length} subjects); satellites verified`);
|
|
142
|
+
return fromRecord.imageLock;
|
|
143
|
+
}
|
|
144
|
+
const probe = createDevelopImageProbe({
|
|
145
|
+
distributionId: id,
|
|
146
|
+
env,
|
|
147
|
+
...(sealOptions.distributionsRoot === undefined ? {} : { distributionsRoot: sealOptions.distributionsRoot }),
|
|
148
|
+
...(sealOptions.fetchImpl === undefined ? {} : { fetchImpl: sealOptions.fetchImpl }),
|
|
149
|
+
...(sealOptions.run === undefined ? {} : { run: sealOptions.run }),
|
|
150
|
+
progress,
|
|
151
|
+
});
|
|
152
|
+
const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe, previousLock });
|
|
153
|
+
phase(`image content hashes computed (${resolved.imageLock.subjects.length} subjects)`);
|
|
154
|
+
await probe.settle();
|
|
155
|
+
phase('every develop image is present');
|
|
156
|
+
return resolved.imageLock;
|
|
82
157
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
158
|
+
try {
|
|
159
|
+
report.settle();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
progress(error.message);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
if (report.noticeText() !== '') {progress(report.noticeText());}
|
|
165
|
+
const sealArguments = { imageLock, env, progress, thirdPartyPins: loadTrustedThirdPartySourcePins(dataRoot()), ...sealOptions };
|
|
166
|
+
const sealedImageLock = recorded === undefined
|
|
167
|
+
? await steps.sealImages({ channel: EditionChannel.Develop, ...sealArguments })
|
|
168
|
+
: await steps.sealImagesFromRecord({ ...sealArguments, recorded });
|
|
88
169
|
phase('images sealed');
|
|
89
170
|
// The unsealed lock becomes the one v5 grammar only here, once the images it
|
|
90
171
|
// owns are sealed: each biomeContentHash is recomputed from the frozen sources.
|
|
@@ -100,8 +181,6 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
100
181
|
phase(`biome sources read (${sourceFactsById.size} biomes)`);
|
|
101
182
|
const sealedLock = await sealLock({ ...biomeSources, unsealed: lock, sourceFactsById });
|
|
102
183
|
phase('distribution lock sealed');
|
|
103
|
-
const chartSetLock = await steps.resolveDevelopChartSet({ xemaDeploySha: pins[deployPinKey()] });
|
|
104
|
-
phase('chart set resolved');
|
|
105
184
|
const sourceTreeIds = await biomeSourceTreeIds({ ...biomeSources, lock, sourceFactsById });
|
|
106
185
|
const catalogue = buildCatalogue({
|
|
107
186
|
sourceTreeIds,
|
|
@@ -121,5 +200,5 @@ export async function resolveDevelopEdition({ id, pins, env = process.env, sealO
|
|
|
121
200
|
leaf: inputs.composed.leaf,
|
|
122
201
|
});
|
|
123
202
|
phase('catalogue built');
|
|
124
|
-
return { distributionLock: sealedLock, imageLock: sealedImageLock, authorizationLock, chartSetLock, catalogue };
|
|
203
|
+
return { distributionLock: sealedLock, imageLock: sealedImageLock, authorizationLock, chartSetLock, catalogue, engineImage };
|
|
125
204
|
}
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
// develop-edition — the develop edition's command line.
|
|
3
3
|
//
|
|
4
4
|
// resolve <id> --pins <file> --out <dir> --registry <root>
|
|
5
|
-
// --engine-
|
|
5
|
+
// --engine-repository <repository> --engine-signer <certificate identity> --cosign <absolute-path> [--local]
|
|
6
|
+
// [--images-from <image build record>]
|
|
6
7
|
// resolve the edition from the snapshot, seal it against the develop image
|
|
7
8
|
// repositories, write the descriptor and the four locks to <dir>, print the target.
|
|
9
|
+
// The engine is found and proven here (see `engine`), and every refusal of the run is reported
|
|
10
|
+
// in one table (develop-resolve-report.mjs): independent checks all run, a dependent one is
|
|
11
|
+
// reported `blocked by <check>`, and when anything is refused nothing is sealed or written.
|
|
12
|
+
// --images-from takes the digests from the everyday job's image build record instead of the registry probe:
|
|
13
|
+
// no probe and no rehash; satellite entries are cosign-verified against their producer's develop build
|
|
14
|
+
// records, and a subject missing, duplicated or extra in the record refuses (images-from-record.mjs).
|
|
8
15
|
// The workflow run is recorded from GITHUB_REPOSITORY / GITHUB_RUN_ID;
|
|
9
16
|
// --local stamps `local` instead, for a resolve outside GitHub Actions.
|
|
10
17
|
// resolve --definition <file> --pins <file> --out <dir> --registry <root>
|
|
@@ -16,10 +23,17 @@
|
|
|
16
23
|
// declares (`freeze-sources --definition` writes it); the catalogue digest is frozen into the descriptor's
|
|
17
24
|
// provenance pins as `catalogue`. The id is the definition's. Takes --data-root, --editions-root,
|
|
18
25
|
// --sources-root, --cache-dir (no --deploy-root, no --kernel: the kernel is the catalogue's, from npm).
|
|
19
|
-
//
|
|
26
|
+
// engine <id> --pins <file> --engine-repository <repository> --engine-signer <certificate identity> --cosign <absolute-path>
|
|
27
|
+
// print <repository>@sha256:<digest>: the engine xema-deploy CI published (tag sha-<12 hex>) for the
|
|
28
|
+
// snapshot's xema-deploy commit, its signature verified offline against the pinned trust root as made by
|
|
29
|
+
// --engine-signer with the signed annotations commit=<that commit> and branch=develop
|
|
30
|
+
// (develop-engine-image.mjs). The image `target` and `check-existing` take as --engine-image.
|
|
31
|
+
// target <id> --pins <file> --registry <root> --engine-image <repository>@sha256:<digest> [--images-from <image build record>]
|
|
20
32
|
// print <root>/<develop segment>/xema-edition-<id>:<tag>, the tag being sha256 of the canonical
|
|
21
33
|
// { cli, engine, pins, sourceImages } (snapshotEditionTag): every input of the bytes the artifact records,
|
|
22
|
-
// so the same pins with another engine, source-image map or CLI version are another artifact.
|
|
34
|
+
// so the same pins with another engine, source-image map or CLI version are another artifact. With
|
|
35
|
+
// --images-from the record's digest (imageBuildRecordDigest) is one more input, named imageBuildRecord; resolve
|
|
36
|
+
// --images-from prints the same target. The root is always the caller's
|
|
23
37
|
// registry root (e.g. registry.example.com/acme); the develop namespace segment and the repository
|
|
24
38
|
// prefix come from edition-contract.json, and no root is implied.
|
|
25
39
|
// target --definition <file> --pins <file> --registry <root> --engine-image <repository>@sha256:<digest>
|
|
@@ -27,10 +41,14 @@
|
|
|
27
41
|
// the same for a customer edition (id = the definition's; the tag covers the pinned catalogue digest).
|
|
28
42
|
// freeze-graph --data-sha <sha> --kernel-sha <sha>
|
|
29
43
|
// --deploy-sha <sha> [--editions-root-url <https url>]
|
|
30
|
-
// [--overrides <json>] --out <file>
|
|
44
|
+
// [--overrides <json> | --member-pins <selection file>] --out <file>
|
|
31
45
|
// resolve refs/heads/develop once per develop-graph repository (the data root's
|
|
32
46
|
// firstPartySources, at each declared URL) and the editions root; the three checked-out
|
|
33
|
-
// 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
|
|
34
52
|
// unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
|
|
35
53
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
36
54
|
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
@@ -57,7 +75,7 @@
|
|
|
57
75
|
// for every develop-graph member, take the newest build record the data root's record workflow signed for
|
|
58
76
|
// the branch (<root>/build-records/<name>, verified offline by digest against the pinned trusted root, as
|
|
59
77
|
// signed by <data root>/.github/workflows/record-build.yaml at refs/heads/<branch>, called by the member at the
|
|
60
|
-
// 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
|
|
61
79
|
// unreadable record refuses the whole run and writes nothing: a newer bad record is never skipped for an older
|
|
62
80
|
// one. --at takes the newest record completed at or before that time. Only `develop` has a declared signer; the
|
|
63
81
|
// data root must declare dataRootRepositoryUrl. Takes the roots `resolve` takes (no registry root is implied).
|
|
@@ -69,6 +87,14 @@
|
|
|
69
87
|
// part 07 passes the flag.
|
|
70
88
|
// Residual: anyone who can delete registry tags can remove newer records, and this then pins the newest remaining
|
|
71
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.
|
|
72
98
|
// choose-kernel [--kernel-pin <40-hex>] --out <file>
|
|
73
99
|
// choose the develop kernel commit in the --kernel checkout (full history; its refs/remotes/origin/develop is the develop head):
|
|
74
100
|
// the newest first-parent commit whose declared edition protocol this CLI supports
|
|
@@ -92,22 +118,28 @@
|
|
|
92
118
|
import { resolve } from 'node:path';
|
|
93
119
|
|
|
94
120
|
import { writeCatalogue } from './catalogue.mjs';
|
|
95
|
-
import { CUSTOMER_ROOTS, 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';
|
|
96
122
|
import { DEFINITION_FLAG } from './customer-flags.mjs';
|
|
97
123
|
import { runDevelopCoverage } from './develop-coverage-run.mjs';
|
|
98
124
|
import { CoverageExit } from './develop-coverage.mjs';
|
|
99
125
|
import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
126
|
+
import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
|
|
100
127
|
import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
|
|
128
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
129
|
+
import { runImageRecord } from './develop-image-record.mjs';
|
|
101
130
|
import { runSelectRecords } from './develop-select-records.mjs';
|
|
131
|
+
import { readSelectionPins } from './develop-selection-pins.mjs';
|
|
102
132
|
import { runVerifyImageRecord } from './develop-verify-image-record.mjs';
|
|
103
133
|
import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
104
134
|
import { EditionChannel, EditionKind } from './edition-constants.mjs';
|
|
105
135
|
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir } from './edition-workspace.mjs';
|
|
106
136
|
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
137
|
+
import { createSatelliteVerifier, imageBuildRecordDigest, readImageBuildRecord } from './images-from-record.mjs';
|
|
107
138
|
import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protocol-choice.mjs';
|
|
108
139
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
109
140
|
import { snapshotPinSource } from './pin-source.mjs';
|
|
110
141
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
142
|
+
import { resolvePreviousSeal } from './resolve-images.mjs';
|
|
111
143
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
112
144
|
|
|
113
145
|
// The roots a verb takes. A customer form (it names --definition) takes fewer, and never the kernel (the
|
|
@@ -150,21 +182,37 @@ function parseOverrides(text) {
|
|
|
150
182
|
return parsed;
|
|
151
183
|
}
|
|
152
184
|
|
|
185
|
+
/** The target-tag input of `--images-from`: the record's digest, or nothing on the probe path. */
|
|
186
|
+
async function imageBuildRecordTagInput(options) {
|
|
187
|
+
return options[IMAGES_FROM_FLAG] === undefined ? {} : { imageBuildRecordDigest: imageBuildRecordDigest(await readImageBuildRecord(options[IMAGES_FROM_FLAG])) };
|
|
188
|
+
}
|
|
189
|
+
|
|
153
190
|
async function run(argv, env) {
|
|
154
191
|
const { command, id, options, customer } = parseDevelopEditionArgs(argv);
|
|
155
192
|
switch (command) {
|
|
156
193
|
case 'target': {
|
|
157
194
|
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
158
|
-
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'] }));
|
|
195
|
+
console.log(developEditionTarget(id, readStrictJsonFile(options['--pins']), { registry: options['--registry'], engineImage: options['--engine-image'], ...(await imageBuildRecordTagInput(options)) }));
|
|
159
196
|
return;
|
|
160
197
|
}
|
|
161
198
|
case 'resolve': {
|
|
162
199
|
if (customer) {return runCustomerVerb({ channel: EditionChannel.Develop, command, options, env });}
|
|
163
200
|
const pins = readStrictJsonFile(options['--pins']);
|
|
164
201
|
const workflow = options['--local'] ? LOCAL_WORKFLOW : workflowFromEnv(env);
|
|
165
|
-
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage: options['--engine-image'] });
|
|
166
202
|
const evidenceTrustPolicy = await descriptorEvidenceTrust({ kind: EditionKind.Resolved, channel: EditionChannel.Develop });
|
|
167
|
-
const { catalogue, ...resolved } = await resolveDevelopEdition({
|
|
203
|
+
const { catalogue, engineImage, ...resolved } = await resolveDevelopEdition({
|
|
204
|
+
id,
|
|
205
|
+
pins,
|
|
206
|
+
env,
|
|
207
|
+
engine: { repository: options['--engine-repository'], signer: options['--engine-signer'] },
|
|
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]) }),
|
|
210
|
+
...(options[IMAGES_FROM_FLAG] === undefined ? {} : {
|
|
211
|
+
imagesFrom: await readImageBuildRecord(options[IMAGES_FROM_FLAG]),
|
|
212
|
+
verifySatellites: createSatelliteVerifier({ registry: options['--registry'], cosignBinary: options['--cosign'], env }),
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
const target = developEditionTarget(id, pins, { registry: options['--registry'], engineImage, ...(await imageBuildRecordTagInput(options)) });
|
|
168
216
|
const outDir = resolve(options['--out']);
|
|
169
217
|
writeEditionArtifact({
|
|
170
218
|
outDir,
|
|
@@ -173,13 +221,23 @@ async function run(argv, env) {
|
|
|
173
221
|
pins,
|
|
174
222
|
workflow,
|
|
175
223
|
evidenceTrustPolicy,
|
|
176
|
-
engineImage
|
|
224
|
+
engineImage,
|
|
177
225
|
...resolved,
|
|
178
226
|
});
|
|
179
227
|
writeCatalogue(outDir, catalogue);
|
|
180
228
|
console.log(target);
|
|
181
229
|
return;
|
|
182
230
|
}
|
|
231
|
+
case 'engine':
|
|
232
|
+
console.log(
|
|
233
|
+
await resolveDevelopEngineImage({
|
|
234
|
+
pins: readStrictJsonFile(options['--pins']),
|
|
235
|
+
engine: { repository: options['--engine-repository'], signer: options['--engine-signer'] },
|
|
236
|
+
cosignBinary: options['--cosign'],
|
|
237
|
+
env,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
183
241
|
case 'coverage':
|
|
184
242
|
return runDevelopCoverage({
|
|
185
243
|
id,
|
|
@@ -191,6 +249,9 @@ async function run(argv, env) {
|
|
|
191
249
|
case 'select-records':
|
|
192
250
|
await runSelectRecords({ options, env });
|
|
193
251
|
return;
|
|
252
|
+
case 'image-record':
|
|
253
|
+
await runImageRecord({ id, options, env });
|
|
254
|
+
return;
|
|
194
255
|
case 'verify-image-record':
|
|
195
256
|
await runVerifyImageRecord({ options, env });
|
|
196
257
|
return;
|
|
@@ -224,6 +285,7 @@ async function run(argv, env) {
|
|
|
224
285
|
deploySha: options['--deploy-sha'],
|
|
225
286
|
editionsRootUrl: options['--editions-root-url'],
|
|
226
287
|
overrides: parseOverrides(options['--overrides']),
|
|
288
|
+
...(options['--member-pins'] === undefined ? {} : { memberPins: readSelectionPins(options['--member-pins'], '--member-pins', developGraphMembers()) }),
|
|
227
289
|
...headResolver(EditionChannel.Develop, env),
|
|
228
290
|
});
|
|
229
291
|
writeOnce(options['--out'], pins);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// develop-engine-image — the engine image a develop edition is deployed with, found and proven.
|
|
2
|
+
//
|
|
3
|
+
// The edition's engine is the one the deploy repository's CI published for EXACTLY the deploy commit the snapshot pins
|
|
4
|
+
// (tag `sha-<12 hex>`, keyless signature by a `workflow_run` follower of a green push CI). The tag only finds the digest.
|
|
5
|
+
// The signature is checked offline against the pinned trust root through the CLI's one cosign path (pinned-cosign.mjs)
|
|
6
|
+
// with the publisher's EXACT identity, issuer, workflow repository and ref (cosign-certificate-constraints.mjs); never a
|
|
7
|
+
// pattern. A follower runs the default branch's copy of the workflow, so the certificate's own workflow commit is not the
|
|
8
|
+
// built one: the built commit is bound by the signed annotations commit=<40 hex> and branch=develop.
|
|
9
|
+
// The digest is what the edition records.
|
|
10
|
+
|
|
11
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
12
|
+
import { certificateIdentityArgs } from './cosign-certificate-constraints.mjs';
|
|
13
|
+
import { assertEngineImage } from './edition-artifact.mjs';
|
|
14
|
+
import { assertPublisher, publisherIdentity } from './edition-publisher.mjs';
|
|
15
|
+
import { deployPinKey } from './pin-source.mjs';
|
|
16
|
+
import { cosignEnvironment, withPinnedCosign } from './pinned-cosign.mjs';
|
|
17
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
18
|
+
import { isNotPublishedYet } from './seal-image-digests.mjs';
|
|
19
|
+
import { GITHUB_ACTIONS_OIDC_ISSUER } from './third-party-source-evidence.mjs';
|
|
20
|
+
|
|
21
|
+
const COMMIT_PATTERN = /^[a-f0-9]{40}$/;
|
|
22
|
+
const TAG_PREFIX = 'sha-';
|
|
23
|
+
const TAG_COMMIT_LENGTH = 12;
|
|
24
|
+
const DEVELOP_BRANCH = 'develop';
|
|
25
|
+
const STDERR_TAIL_BYTES = 800;
|
|
26
|
+
|
|
27
|
+
/** The tag xema-deploy CI gives the engine it publishes for `deploySha`. */
|
|
28
|
+
export const engineTagOf = (deploySha) => `${TAG_PREFIX}${deploySha.slice(0, TAG_COMMIT_LENGTH)}`;
|
|
29
|
+
|
|
30
|
+
const IDENTITY_PATTERN = /^https:\/\/github\.com\/([^/@]+\/[^/@]+)\/(\.github\/workflows\/[^/@]+)@(.+)$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The signer an exact certificate identity `https://github.com/<owner>/<name>/.github/workflows/<file>@<ref>` names, held to
|
|
34
|
+
* the publisher grammar of edition-contract.json (so no pattern, no wildcard).
|
|
35
|
+
*/
|
|
36
|
+
export function engineSignerOf(identity) {
|
|
37
|
+
const match = typeof identity === 'string' ? IDENTITY_PATTERN.exec(identity) : null;
|
|
38
|
+
if (match === null) {
|
|
39
|
+
throw new RefusalError(`--engine-signer must be the exact certificate identity https://github.com/<owner>/<name>/.github/workflows/<file>@<ref>; got ${JSON.stringify(identity)}`);
|
|
40
|
+
}
|
|
41
|
+
const publisher = assertPublisher({ repository: match[1], workflow: match[2], ref: match[3] }, '--engine-signer');
|
|
42
|
+
return { certificateIdentity: publisherIdentity(publisher), oidcIssuer: GITHUB_ACTIONS_OIDC_ISSUER, githubWorkflow: { repository: publisher.repository, ref: publisher.ref } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tail = (text) => String(text).trim().slice(-STDERR_TAIL_BYTES);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} request
|
|
49
|
+
* @param {Record<string, string>} request.pins the snapshot's pins; the xema-deploy commit is read from them
|
|
50
|
+
* @param {{repository: string, signer: string}} request.engine the engine repository and the signer's exact certificate identity
|
|
51
|
+
* @param {string} request.cosignBinary absolute path of cosign
|
|
52
|
+
* @param {object} request.env the environment the registry credentials are named in
|
|
53
|
+
* @param {object} [request.pinned] withPinnedCosign's injections (tests)
|
|
54
|
+
* @returns {Promise<string>} `<repository>@sha256:<digest>`
|
|
55
|
+
*/
|
|
56
|
+
export async function resolveDevelopEngineImage({ pins, engine, cosignBinary, env, pinned = {} }) {
|
|
57
|
+
const deploySha = pins[deployPinKey()];
|
|
58
|
+
if (typeof deploySha !== 'string' || !COMMIT_PATTERN.test(deploySha)) {
|
|
59
|
+
throw new RefusalError(`the pin ${deployPinKey()} is ${JSON.stringify(deploySha)}, not a 40-hex commit`);
|
|
60
|
+
}
|
|
61
|
+
const { repository } = engine;
|
|
62
|
+
const signer = engineSignerOf(engine.signer);
|
|
63
|
+
const tag = engineTagOf(deploySha);
|
|
64
|
+
const { authority, repository: path } = splitImageRef(repository);
|
|
65
|
+
return withPinnedCosign({ cosignBinary, env, ...pinned }, async ({ runCosign, cosignEnvFor, clientFor, trustedRootPath }) => {
|
|
66
|
+
let digest;
|
|
67
|
+
try {
|
|
68
|
+
digest = (await clientFor(authority).getManifestByTag(path, tag)).digest;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!isNotPublishedYet(error)) {throw error;}
|
|
71
|
+
throw new RefusalError(
|
|
72
|
+
`no engine ${repository}:${tag} for the ${deployPinKey()} commit ${deploySha}. The deploy repository's CI publishes one for a green push to develop or main, ` +
|
|
73
|
+
'or a CI dispatch at that branch; a commit pushed with GITHUB_TOKEN has none until CI is dispatched. ' +
|
|
74
|
+
`Dispatch that CI at develop, or pin ${deployPinKey()} to a commit that has an engine.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const image = `${repository}@${digest}`;
|
|
78
|
+
const result = await runCosign(
|
|
79
|
+
[
|
|
80
|
+
'verify', '--offline', '--trusted-root', trustedRootPath,
|
|
81
|
+
...certificateIdentityArgs(signer),
|
|
82
|
+
'-a', `commit=${deploySha}`, '-a', `branch=${DEVELOP_BRANCH}`,
|
|
83
|
+
image,
|
|
84
|
+
],
|
|
85
|
+
cosignEnvironment(cosignEnvFor, authority),
|
|
86
|
+
);
|
|
87
|
+
if (result.code !== 0) {
|
|
88
|
+
throw new RefusalError(
|
|
89
|
+
`${image} (${tag}) is not signed by the engine signer ${signer.certificateIdentity} with commit=${deploySha} branch=${DEVELOP_BRANCH}; ` +
|
|
90
|
+
`refusing it as this edition's engine (cosign exit ${result.code}): ${tail(result.stderr)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return assertEngineImage(image);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -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
|
}
|