@xemahq/edition-tooling 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// develop-engine-image — the engine image the catalogue-free customer resolve's vendor step records, found and proven
|
|
2
|
+
// (develop-catalogue-free-vendor.mjs, the official lane). The channel lane's vendor resolve is handed its engine by
|
|
3
|
+
// digest (`--engine-image`) and proves nothing.
|
|
4
|
+
//
|
|
5
|
+
// The edition's engine is the one the deploy repository's CI published for EXACTLY the deploy commit the snapshot pins
|
|
6
|
+
// (tag `sha-<12 hex>`, keyless signature by a `workflow_run` follower of a green push CI). The tag only finds the digest.
|
|
7
|
+
// The signature is checked offline against the pinned trust root through the CLI's one cosign path (pinned-cosign.mjs)
|
|
8
|
+
// with the publisher's EXACT identity, issuer, workflow repository and ref (cosign-certificate-constraints.mjs); never a
|
|
9
|
+
// pattern. A follower runs the default branch's copy of the workflow, so the certificate's own workflow commit is not the
|
|
10
|
+
// built one: the built commit is bound by the signed annotations commit=<40 hex> and branch=develop.
|
|
11
|
+
// The digest is what the edition records.
|
|
12
|
+
|
|
13
|
+
import { RefusalError } from './collected-refusals.mjs';
|
|
14
|
+
import { certificateIdentityArgs } from './cosign-certificate-constraints.mjs';
|
|
15
|
+
import { assertEngineImage } from './edition-artifact.mjs';
|
|
16
|
+
import { assertPublisher, publisherIdentity } from './edition-publisher.mjs';
|
|
17
|
+
import { deployPinKey } from './pin-source.mjs';
|
|
18
|
+
import { cosignEnvironment, withPinnedCosign } from './pinned-cosign.mjs';
|
|
19
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
20
|
+
import { isNotPublishedYet } from './seal-image-digests.mjs';
|
|
21
|
+
import { GITHUB_ACTIONS_OIDC_ISSUER } from './third-party-source-evidence.mjs';
|
|
22
|
+
|
|
23
|
+
const COMMIT_PATTERN = /^[a-f0-9]{40}$/;
|
|
24
|
+
const TAG_PREFIX = 'sha-';
|
|
25
|
+
const TAG_COMMIT_LENGTH = 12;
|
|
26
|
+
const DEVELOP_BRANCH = 'develop';
|
|
27
|
+
const STDERR_TAIL_BYTES = 800;
|
|
28
|
+
|
|
29
|
+
/** The tag xema-deploy CI gives the engine it publishes for `deploySha`. */
|
|
30
|
+
export const engineTagOf = (deploySha) => `${TAG_PREFIX}${deploySha.slice(0, TAG_COMMIT_LENGTH)}`;
|
|
31
|
+
|
|
32
|
+
const IDENTITY_PATTERN = /^https:\/\/github\.com\/([^/@]+\/[^/@]+)\/(\.github\/workflows\/[^/@]+)@(.+)$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The signer an exact certificate identity `https://github.com/<owner>/<name>/.github/workflows/<file>@<ref>` names, held to
|
|
36
|
+
* the publisher grammar of edition-contract.json (so no pattern, no wildcard).
|
|
37
|
+
*/
|
|
38
|
+
export function engineSignerOf(identity) {
|
|
39
|
+
const match = typeof identity === 'string' ? IDENTITY_PATTERN.exec(identity) : null;
|
|
40
|
+
if (match === null) {
|
|
41
|
+
throw new RefusalError(`--engine-signer must be the exact certificate identity https://github.com/<owner>/<name>/.github/workflows/<file>@<ref>; got ${JSON.stringify(identity)}`);
|
|
42
|
+
}
|
|
43
|
+
const publisher = assertPublisher({ repository: match[1], workflow: match[2], ref: match[3] }, '--engine-signer');
|
|
44
|
+
return { certificateIdentity: publisherIdentity(publisher), oidcIssuer: GITHUB_ACTIONS_OIDC_ISSUER, githubWorkflow: { repository: publisher.repository, ref: publisher.ref } };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tail = (text) => String(text).trim().slice(-STDERR_TAIL_BYTES);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} request
|
|
51
|
+
* @param {Record<string, string>} request.pins the snapshot's pins; the xema-deploy commit is read from them
|
|
52
|
+
* @param {{repository: string, signer: string}} request.engine the engine repository and the signer's exact certificate identity
|
|
53
|
+
* @param {string} request.cosignBinary absolute path of cosign
|
|
54
|
+
* @param {object} request.env the environment the registry credentials are named in
|
|
55
|
+
* @param {object} [request.pinned] withPinnedCosign's injections (tests)
|
|
56
|
+
* @returns {Promise<string>} `<repository>@sha256:<digest>`
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveDevelopEngineImage({ pins, engine, cosignBinary, env, pinned = {} }) {
|
|
59
|
+
const deploySha = pins[deployPinKey()];
|
|
60
|
+
if (typeof deploySha !== 'string' || !COMMIT_PATTERN.test(deploySha)) {
|
|
61
|
+
throw new RefusalError(`the pin ${deployPinKey()} is ${JSON.stringify(deploySha)}, not a 40-hex commit`);
|
|
62
|
+
}
|
|
63
|
+
const { repository } = engine;
|
|
64
|
+
const signer = engineSignerOf(engine.signer);
|
|
65
|
+
const tag = engineTagOf(deploySha);
|
|
66
|
+
const { authority, repository: path } = splitImageRef(repository);
|
|
67
|
+
return withPinnedCosign({ cosignBinary, env, ...pinned }, async ({ runCosign, cosignEnvFor, clientFor, trustedRootPath }) => {
|
|
68
|
+
let digest;
|
|
69
|
+
try {
|
|
70
|
+
digest = (await clientFor(authority).getManifestByTag(path, tag)).digest;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!isNotPublishedYet(error)) {throw error;}
|
|
73
|
+
throw new RefusalError(
|
|
74
|
+
`no engine ${repository}:${tag} for the ${deployPinKey()} commit ${deploySha}. The deploy repository's CI publishes one for a green push to develop or main, ` +
|
|
75
|
+
'or a CI dispatch at that branch; a commit pushed with GITHUB_TOKEN has none until CI is dispatched. ' +
|
|
76
|
+
`Dispatch that CI at develop, or pin ${deployPinKey()} to a commit that has an engine.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const image = `${repository}@${digest}`;
|
|
80
|
+
const result = await runCosign(
|
|
81
|
+
[
|
|
82
|
+
'verify', '--offline', '--trusted-root', trustedRootPath,
|
|
83
|
+
...certificateIdentityArgs(signer),
|
|
84
|
+
'-a', `commit=${deploySha}`, '-a', `branch=${DEVELOP_BRANCH}`,
|
|
85
|
+
image,
|
|
86
|
+
],
|
|
87
|
+
cosignEnvironment(cosignEnvFor, authority),
|
|
88
|
+
);
|
|
89
|
+
if (result.code !== 0) {
|
|
90
|
+
throw new RefusalError(
|
|
91
|
+
`${image} (${tag}) is not signed by the engine signer ${signer.certificateIdentity} with commit=${deploySha} branch=${DEVELOP_BRANCH}; ` +
|
|
92
|
+
`refusing it as this edition's engine (cosign exit ${result.code}): ${tail(result.stderr)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return assertEngineImage(image);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// develop-freeze — freeze the develop branches into one snapshot of pins.
|
|
2
|
+
//
|
|
3
|
+
// Resolves `refs/heads/develop` ONCE per distinct repository URL, applies the
|
|
4
|
+
// caller's per-key overrides (an override wins and is never looked up), and
|
|
5
|
+
// returns the pins. After this, NOTHING reads a branch: every later step takes
|
|
6
|
+
// the pins. Two phases, because the declared sources are only known once the
|
|
7
|
+
// edition's chain can be read from the checked-out editions root:
|
|
8
|
+
//
|
|
9
|
+
// freezeGraph the graph repositories, the data root, the kernel, the deploy checkout, the editions root
|
|
10
|
+
// freezeSources one pin per `sources[]` entry the chain declares
|
|
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
|
+
//
|
|
15
|
+
// `resolveHead({ name, url })` is the one network seam (default: `git ls-remote` through
|
|
16
|
+
// git-source-auth: the data root's token only for a repository under its
|
|
17
|
+
// `sourceRepositoryUrlPrefix`, no credential for any other, never argv).
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
|
|
22
|
+
import { currentResolutionOf } from './develop-current-resolution.mjs';
|
|
23
|
+
import { developGraphMembers } from './develop-graph.mjs';
|
|
24
|
+
import { EditionChannel } from './edition-channel.mjs';
|
|
25
|
+
import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
|
|
26
|
+
import { assertKernelShaAllowed, isKernelFromMember, KernelPinSourceError } from './kernel-pin-source.mjs';
|
|
27
|
+
import {
|
|
28
|
+
dataPinKey,
|
|
29
|
+
EDITIONS_ROOT_PIN_KEY,
|
|
30
|
+
kernelPinKey,
|
|
31
|
+
SnapshotPinNamespace,
|
|
32
|
+
deployPinKey,
|
|
33
|
+
snapshotPinNamespace,
|
|
34
|
+
sortedSnapshotPins,
|
|
35
|
+
sourceNameOfPinKey,
|
|
36
|
+
sourcePinKey,
|
|
37
|
+
} from './pin-source.mjs';
|
|
38
|
+
|
|
39
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
40
|
+
|
|
41
|
+
/** The ref namespaces a freeze may resolve a ref in. */
|
|
42
|
+
export const RefKind = Object.freeze({ Branch: 'branch', Tag: 'tag' });
|
|
43
|
+
|
|
44
|
+
const REF_PREFIX = Object.freeze({ [RefKind.Branch]: 'refs/heads/', [RefKind.Tag]: 'refs/tags/' });
|
|
45
|
+
const PEELED_SUFFIX = '^{}';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What each channel freezes a source at. A develop edition freezes the head of the `develop` BRANCH of every
|
|
49
|
+
* repository; a release edition freezes each source at the ref its definition declares, a branch or a tag.
|
|
50
|
+
*/
|
|
51
|
+
const FREEZE_REF_KINDS = Object.freeze({
|
|
52
|
+
[EditionChannel.Develop]: Object.freeze([RefKind.Branch]),
|
|
53
|
+
[EditionChannel.Release]: Object.freeze([RefKind.Branch, RefKind.Tag]),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The commit `ref` names in one source `{ name, url }`, by `git ls-remote <url> <refs/heads/ref | refs/tags/ref |
|
|
58
|
+
* refs/tags/ref^{}>` (only the namespaces the channel freezes at). A tag resolves to its peeled commit when it is annotated.
|
|
59
|
+
* Throws naming the repository when the ref is absent, or when it is both a branch and a tag. The source is held and its
|
|
60
|
+
* git environment built by `withSourceGitEnv`: the workspace credential reaches only a repository under `sourceRepositoryUrlPrefix`.
|
|
61
|
+
*/
|
|
62
|
+
export function lsRemoteRef(source, ref, { channel, workspace, env = process.env, runGit = execFileSync }) {
|
|
63
|
+
const kinds = FREEZE_REF_KINDS[channel];
|
|
64
|
+
if (kinds === undefined) {throw new Error(`unknown edition channel ${JSON.stringify(channel)}`);}
|
|
65
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
66
|
+
throw new Error(`source "${source.name}" declares no ref; a ${channel} edition cannot be frozen without one`);
|
|
67
|
+
}
|
|
68
|
+
const names = kinds.flatMap((kind) => (kind === RefKind.Tag ? [`${REF_PREFIX[kind]}${ref}`, `${REF_PREFIX[kind]}${ref}${PEELED_SUFFIX}`] : [`${REF_PREFIX[kind]}${ref}`]));
|
|
69
|
+
const output = withSourceGitEnv(source, { workspace, env }, (gitEnv) =>
|
|
70
|
+
runGit('git', ['ls-remote', source.url, ...names], {
|
|
71
|
+
env: gitEnv,
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
}),
|
|
75
|
+
).trim();
|
|
76
|
+
const served = new Map();
|
|
77
|
+
for (const line of output.split('\n')) {
|
|
78
|
+
const [sha, name] = line.trim().split(/\s+/);
|
|
79
|
+
if (names.includes(name)) {
|
|
80
|
+
if (!SHA_PATTERN.test(sha ?? '')) {throw new Error(`${source.url}: git ls-remote answered ${JSON.stringify(line)} for ${name}`);}
|
|
81
|
+
served.set(name, sha);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const branch = served.get(`${REF_PREFIX[RefKind.Branch]}${ref}`);
|
|
85
|
+
const tagName = `${REF_PREFIX[RefKind.Tag]}${ref}`;
|
|
86
|
+
const tag = served.get(`${tagName}${PEELED_SUFFIX}`) ?? served.get(tagName);
|
|
87
|
+
if (branch !== undefined && tag !== undefined) {
|
|
88
|
+
throw new Error(`${source.url}: ${JSON.stringify(ref)} is both a branch and a tag; a ${channel} edition cannot be frozen at an ambiguous ref`);
|
|
89
|
+
}
|
|
90
|
+
const commit = branch ?? tag;
|
|
91
|
+
if (commit === undefined) {
|
|
92
|
+
throw new Error(`${source.url} has no ${kinds.map((kind) => `${REF_PREFIX[kind]}${ref}`).join(' or ')}; a ${channel} edition cannot be frozen without it`);
|
|
93
|
+
}
|
|
94
|
+
return commit;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A resolver that asks `resolveHead` at most once per distinct URL and ref; it takes the source `{ name, url, ref? }`. */
|
|
98
|
+
function memoizedHeads(resolveHead) {
|
|
99
|
+
const heads = new Map();
|
|
100
|
+
return (source) => {
|
|
101
|
+
const key = JSON.stringify([source.url, source.ref]);
|
|
102
|
+
if (!heads.has(key)) {heads.set(key, resolveHead(source));}
|
|
103
|
+
return heads.get(key);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The pins input selects WHICH inputs a snapshot resolves, never which tooling
|
|
109
|
+
* code runs: the distributions plane that executes is the dispatching ref's own
|
|
110
|
+
* checkout, so an override for its pin key is refused outright (even one equal
|
|
111
|
+
* to that checkout), naming the key.
|
|
112
|
+
*/
|
|
113
|
+
export function assertNoToolingOverride(overrides) {
|
|
114
|
+
if (overrides[dataPinKey()] !== undefined) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`pins override "${dataPinKey()}" is refused: it would select the tooling code this run executes. ` +
|
|
117
|
+
'The distributions plane is always the dispatching ref\'s own checkout; pins choose inputs, never tooling.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertOverrides(overrides, allowed) {
|
|
123
|
+
for (const [key, sha] of Object.entries(overrides)) {
|
|
124
|
+
const namespace = snapshotPinNamespace(key);
|
|
125
|
+
if (!allowed.has(namespace)) {continue;} // another phase's pin
|
|
126
|
+
if (typeof sha !== 'string' || !SHA_PATTERN.test(sha)) {
|
|
127
|
+
throw new Error(`override for "${key}" is not a 40-hex commit SHA: ${JSON.stringify(sha)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The member pins name exactly the graph minus the current-resolution members; a missing or extra member is refused by name. */
|
|
133
|
+
function assertMemberPinsAreTheGraph(memberPins, members) {
|
|
134
|
+
const current = new Set(Object.keys(currentResolutionOf(members)));
|
|
135
|
+
const expected = members.map(({ repo }) => repo).filter((repo) => !current.has(repo));
|
|
136
|
+
const missing = expected.filter((repo) => !Object.hasOwn(memberPins, repo));
|
|
137
|
+
const extra = Object.keys(memberPins).filter((repo) => !expected.includes(repo));
|
|
138
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
139
|
+
throw new Error(`--member-pins must pin exactly the develop graph minus ${JSON.stringify([...current])}: missing ${JSON.stringify(missing)}, extra ${JSON.stringify(extra)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const GRAPH_PHASE = new Set([
|
|
144
|
+
SnapshotPinNamespace.Graph,
|
|
145
|
+
SnapshotPinNamespace.Distributions,
|
|
146
|
+
SnapshotPinNamespace.Kernel,
|
|
147
|
+
SnapshotPinNamespace.ChartSource,
|
|
148
|
+
SnapshotPinNamespace.EditionsRoot,
|
|
149
|
+
]);
|
|
150
|
+
const SOURCE_PHASE = new Set([SnapshotPinNamespace.Source]);
|
|
151
|
+
|
|
152
|
+
/** A repository the workflow checked out is pinned at what it checked out; an override may only agree. */
|
|
153
|
+
function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
|
|
154
|
+
if (!SHA_PATTERN.test(sha ?? '')) {
|
|
155
|
+
throw new Error(`${flag} is not a 40-hex commit SHA: ${JSON.stringify(sha)}`);
|
|
156
|
+
}
|
|
157
|
+
if (overrides[key] !== undefined && overrides[key] !== sha) {
|
|
158
|
+
throw new Error(`override for "${key}" is ${overrides[key]}, but ${what} is checked out at ${sha}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {object} request
|
|
164
|
+
* @param {string} request.dataSha the commit the data root checkout is at
|
|
165
|
+
* @param {string} request.channel the edition channel; it decides where the kernel pin comes from (kernel-pin-source.mjs)
|
|
166
|
+
* @param {string} [request.kernelSha] release only: the commit the kernel checkout is at (refused on develop with KernelPinSourceError)
|
|
167
|
+
* @param {string} [request.kernelMember] develop only: the repo handle of the member whose checkout holds the kernel; the kernel pin is that member's pin
|
|
168
|
+
* @param {string} request.deploySha the commit the deploy checkout is at
|
|
169
|
+
* @param {string} [request.editionsRootUrl] https URL of the editions root; absent = this repository
|
|
170
|
+
* @param {Record<string, string>} [request.memberPins] the graph members' pins from a selection (`--member-pins`): exactly the
|
|
171
|
+
* graph minus the members that keep their current resolution (develop-current-resolution.mjs), which are pinned at their
|
|
172
|
+
* develop head; refused together with overrides
|
|
173
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
|
|
174
|
+
*/
|
|
175
|
+
export function freezeGraph({
|
|
176
|
+
channel,
|
|
177
|
+
dataSha,
|
|
178
|
+
kernelSha,
|
|
179
|
+
kernelMember,
|
|
180
|
+
deploySha,
|
|
181
|
+
editionsRootUrl,
|
|
182
|
+
overrides = {},
|
|
183
|
+
memberPins,
|
|
184
|
+
resolveHead,
|
|
185
|
+
holdSource,
|
|
186
|
+
}) {
|
|
187
|
+
if (memberPins !== undefined && Object.keys(overrides).length > 0) {
|
|
188
|
+
throw new Error('--member-pins and --overrides are exclusive: the member pins are the selected build records, never overridden');
|
|
189
|
+
}
|
|
190
|
+
assertNoToolingOverride(overrides);
|
|
191
|
+
assertOverrides(overrides, GRAPH_PHASE);
|
|
192
|
+
const head = memoizedHeads(resolveHead);
|
|
193
|
+
// The develop graph is the data root's first-party repositories (develop-graph.mjs), never the release pins.
|
|
194
|
+
const members = developGraphMembers();
|
|
195
|
+
const graph = new Set(members.map((member) => member.repo));
|
|
196
|
+
for (const [key] of Object.entries(overrides)) {
|
|
197
|
+
if (snapshotPinNamespace(key) === SnapshotPinNamespace.Graph && !graph.has(key)) {
|
|
198
|
+
throw new Error(`override names "${key}", which is not a repository in the graph`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (memberPins !== undefined) {assertMemberPinsAreTheGraph(memberPins, members);}
|
|
202
|
+
const fromMember = isKernelFromMember(channel);
|
|
203
|
+
assertKernelShaAllowed(channel, kernelSha);
|
|
204
|
+
if (fromMember && !graph.has(kernelMember)) {
|
|
205
|
+
throw new KernelPinSourceError(`the kernel member ${JSON.stringify(kernelMember)} is not a repository in the graph`);
|
|
206
|
+
}
|
|
207
|
+
const checkedOut = [
|
|
208
|
+
{ key: dataPinKey(), sha: dataSha, flag: '--data-sha', what: 'the data root checkout' },
|
|
209
|
+
...(fromMember ? [] : [{ key: kernelPinKey(), sha: kernelSha, flag: '--kernel-sha', what: 'the kernel checkout' }]),
|
|
210
|
+
{ key: deployPinKey(), sha: deploySha, flag: '--deploy-sha', what: 'the deploy checkout' },
|
|
211
|
+
];
|
|
212
|
+
for (const { key, sha, flag, what } of checkedOut) {assertCheckedOutPin({ key, sha, flag, what, overrides });}
|
|
213
|
+
const toResolve = members.map((member) => ({ name: member.repo, url: member.repositoryUrl }));
|
|
214
|
+
if (editionsRootUrl !== undefined) {
|
|
215
|
+
toResolve.push({ name: EDITIONS_ROOT_PIN_KEY, url: canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url') });
|
|
216
|
+
}
|
|
217
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
218
|
+
for (const source of toResolve) {holdSource(source);}
|
|
219
|
+
if (editionsRootUrl === undefined && overrides[EDITIONS_ROOT_PIN_KEY] !== undefined) {
|
|
220
|
+
throw new Error(`override names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
|
|
221
|
+
}
|
|
222
|
+
const pins = Object.fromEntries(checkedOut.map(({ key, sha }) => [key, sha]));
|
|
223
|
+
for (const member of members) {
|
|
224
|
+
pins[member.repo] = overrides[member.repo] ?? memberPins?.[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
|
|
225
|
+
}
|
|
226
|
+
if (fromMember) {
|
|
227
|
+
// The kernel is built from the member's checkout, so its pin is the member's: equal by construction.
|
|
228
|
+
assertCheckedOutPin({ key: kernelPinKey(), sha: pins[kernelMember], flag: kernelMember, what: `the kernel member ${kernelMember} checkout`, overrides });
|
|
229
|
+
pins[kernelPinKey()] = pins[kernelMember];
|
|
230
|
+
}
|
|
231
|
+
if (editionsRootUrl !== undefined) {
|
|
232
|
+
const url = canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url');
|
|
233
|
+
pins[EDITIONS_ROOT_PIN_KEY] = overrides[EDITIONS_ROOT_PIN_KEY] ?? head({ name: EDITIONS_ROOT_PIN_KEY, url });
|
|
234
|
+
}
|
|
235
|
+
return sortedSnapshotPins(pins);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @param {object} request
|
|
240
|
+
* @param {Record<string,string>} request.pins the graph phase's pins
|
|
241
|
+
* @param {object[]} request.declaredSources every `sources[]` entry in the chain (`{ name, url, ... }`)
|
|
242
|
+
* @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source (`holdSource` of git-source-auth with the workspace and env); runs for ALL sources before the first head
|
|
243
|
+
*/
|
|
244
|
+
export function freezeSources({ pins, declaredSources, overrides = {}, resolveHead, holdSource }) {
|
|
245
|
+
assertOverrides(overrides, SOURCE_PHASE);
|
|
246
|
+
const head = memoizedHeads(resolveHead);
|
|
247
|
+
const sourceByName = new Map();
|
|
248
|
+
for (const source of declaredSources) {
|
|
249
|
+
const known = sourceByName.get(source.name);
|
|
250
|
+
if (known !== undefined && known.url !== source.url) {
|
|
251
|
+
throw new Error(`source "${source.name}" is declared at two URLs (${known.url}, ${source.url})`);
|
|
252
|
+
}
|
|
253
|
+
sourceByName.set(source.name, source);
|
|
254
|
+
}
|
|
255
|
+
for (const key of Object.keys(overrides)) {
|
|
256
|
+
if (snapshotPinNamespace(key) === SnapshotPinNamespace.Source && !sourceByName.has(sourceNameOfPinKey(key))) {
|
|
257
|
+
throw new Error(`override names "${key}", but the chain declares no such source`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Every source is held before the first head is asked: a refusal of the last leaves no git call behind.
|
|
261
|
+
for (const source of sourceByName.values()) {holdSource(source);}
|
|
262
|
+
const frozen = { ...pins };
|
|
263
|
+
for (const [name, source] of sourceByName) {
|
|
264
|
+
frozen[sourcePinKey(name)] = overrides[sourcePinKey(name)] ?? head(source);
|
|
265
|
+
}
|
|
266
|
+
return sortedSnapshotPins(frozen);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The declared source URLs a customer freeze may ask, held BEFORE any network call: the token of the
|
|
271
|
+
* run (EVIDENCE_REPOSITORIES_TOKEN) is sent to the origin of each URL, so a URL is refused unless it is
|
|
272
|
+
* a canonical public HTTPS repository URL written in its canonical form and lies under the workspace's
|
|
273
|
+
* `sourceRepositoryUrlPrefix`. A definition that merely names another origin never receives the token.
|
|
274
|
+
*/
|
|
275
|
+
export function assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix) {
|
|
276
|
+
if (typeof sourceRepositoryUrlPrefix !== 'string' || sourceRepositoryUrlPrefix.length === 0) {
|
|
277
|
+
throw new Error('a customer freeze takes the workspace\'s sourceRepositoryUrlPrefix');
|
|
278
|
+
}
|
|
279
|
+
for (const source of sources) {
|
|
280
|
+
assertSourceDeclaresNoCredential(source);
|
|
281
|
+
const canonical = assertCanonicalSourceUrl(source);
|
|
282
|
+
const context = `source "${source.name}" url`;
|
|
283
|
+
if (!isUnderSourcePrefix(canonical, sourceRepositoryUrlPrefix)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`${context} ${JSON.stringify(source.url)} is not under the workspace's sourceRepositoryUrlPrefix ${sourceRepositoryUrlPrefix}: ` +
|
|
286
|
+
'the source token is sent only to repositories under that prefix',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The pins of a CUSTOMER edition: exactly one `sources/<name>` commit per source its definition declares,
|
|
294
|
+
* each the head of refs/heads/develop of that source's repository, resolved once. Every URL is held by
|
|
295
|
+
* `assertCustomerSourceUrls` before the first head is asked.
|
|
296
|
+
*/
|
|
297
|
+
export function freezeCustomerSources({ definition, resolveHead, holdSource, sourceRepositoryUrlPrefix }) {
|
|
298
|
+
const sources = definition?.sources ?? [];
|
|
299
|
+
if (!Array.isArray(sources) || sources.some((source) => typeof source?.name !== 'string' || typeof source?.url !== 'string')) {
|
|
300
|
+
throw new Error('the definition\'s sources must be an array of {name, url}');
|
|
301
|
+
}
|
|
302
|
+
assertCustomerSourceUrls(sources, sourceRepositoryUrlPrefix);
|
|
303
|
+
return freezeSources({ pins: {}, declaredSources: sources, resolveHead, holdSource });
|
|
304
|
+
}
|