@xemahq/xema 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -37
- package/dist/assets/appliance-base/images.lock.json +126 -126
- package/dist/assets/appliance-lean/images.lock.json +123 -123
- package/dist/assets/cloud/images.lock.json +126 -126
- package/dist/assets/core/images.lock.json +120 -120
- package/dist/assets/distribution-assets.manifest.json +15 -16
- package/dist/assets/minimal/images.lock.json +108 -108
- package/dist/assets/oss/images.lock.json +126 -126
- package/dist/assets/starter/images.lock.json +123 -123
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +6 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/backing-registration.d.ts.map +1 -1
- package/dist/runtime/backing-registration.js +7 -2
- package/dist/runtime/backing-registration.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +3 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +20 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +3 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +37 -8
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +3 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +2 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +18 -7
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -154
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { dataRoot, sourcesRoot } from './edition-workspace.mjs';
|
|
13
|
+
|
|
14
|
+
export const PUBLISHED_SOURCE_REF = 'refs/remotes/origin/main';
|
|
15
|
+
|
|
16
|
+
export const DistributionSourceMode = Object.freeze({
|
|
17
|
+
Published: 'published',
|
|
18
|
+
Workspace: 'workspace',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function git(directory, args, options = {}) {
|
|
22
|
+
return execFileSync('git', ['-C', directory, ...args], {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
25
|
+
...options,
|
|
26
|
+
}).trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exactCommit(directory, ref, label) {
|
|
30
|
+
try {
|
|
31
|
+
return git(directory, ['rev-parse', '--verify', `${ref}^{commit}`]);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(`${label} has no exact Git commit at ${ref}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How far back to look for a built commit before refusing. Generous on purpose:
|
|
39
|
+
* the fleet commits 150-353 times a day and builds 4-12 times, so the newest
|
|
40
|
+
* built commit is routinely dozens of commits behind the tip.
|
|
41
|
+
*/
|
|
42
|
+
const BUILT_COMMIT_SEARCH_LIMIT = 500;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every commit this repository has PUBLISHED AN IMAGE FROM, read out of the
|
|
46
|
+
* committed evidence tree.
|
|
47
|
+
*
|
|
48
|
+
* `predicate.source.sourceCommit` in each signed in-toto statement records the
|
|
49
|
+
* exact commit its image was built from. That is a fact in git, at the ref being
|
|
50
|
+
* resolved — no network, no registry credential, no clock. Keeping this resolver
|
|
51
|
+
* pure-git is deliberate: it decides what every distribution lock names, so a
|
|
52
|
+
* network dependency here would make the whole release plane fail differently
|
|
53
|
+
* depending on whether ghcr answered.
|
|
54
|
+
*
|
|
55
|
+
* A repository with NO evidence tree publishes no images (it contributes
|
|
56
|
+
* vendored content only, like xema-connectors). For those the tip is the right
|
|
57
|
+
* answer and always was.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* The committed projection of "which commits of this repository have a
|
|
61
|
+
* published image", written by `project-build-ledger.mjs` from the registry's
|
|
62
|
+
* own tags.
|
|
63
|
+
*
|
|
64
|
+
* WHY A LEDGER AND NOT THE REGISTRY DIRECTLY. This resolver decides what every
|
|
65
|
+
* distribution lock names, and the README states the property that depends on
|
|
66
|
+
* it: locks contain only immutable inputs, so two runs over the same sources
|
|
67
|
+
* produce byte-identical CONTENT and an identical `inputHash`.
|
|
68
|
+
*
|
|
69
|
+
* That sentence used to read "byte-identical files", and measured over two full
|
|
70
|
+
* producer runs on 2026-09-06 it is false by exactly one field: every
|
|
71
|
+
* `distribution.lock.json` carries a wall-clock `resolvedAt`, so all eight
|
|
72
|
+
* differ on every run while their content and `inputHash` are stable, and
|
|
73
|
+
* `images.lock.json` and the three indexes are byte-identical. The property
|
|
74
|
+
* this paragraph is defending is unaffected — `resolvedAt` is provenance and
|
|
75
|
+
* feeds no hash — but a determinism claim that a single `diff` disproves is
|
|
76
|
+
* one nobody trusts the second time, so it is stated precisely instead.
|
|
77
|
+
*
|
|
78
|
+
* A live registry read would make lock
|
|
79
|
+
* generation depend on registry state at that instant and on the token's
|
|
80
|
+
* package visibility — the same sources would resolve differently on Tuesday
|
|
81
|
+
* and Thursday, and differently again for a different credential. A committed
|
|
82
|
+
* ledger is addressed by a ref, so it is part of "the same sources". It also
|
|
83
|
+
* keeps this path pure-git, which is what lets it run with no network at all.
|
|
84
|
+
*
|
|
85
|
+
* WHY NOT THE EVIDENCE TREE ALONE. It was the only source until 2026-09-02, and
|
|
86
|
+
* four of the five producing repositories had stopped writing it on 2026-08-25
|
|
87
|
+
* when they moved to publishing OCI referrers — a change whose commit message
|
|
88
|
+
* says nothing about evidence. So the reader and the producers spoke different
|
|
89
|
+
* media, silently: measured that day, `xema-cultivars` resolved to an 2026-08-25
|
|
90
|
+
* commit 153 behind its tip, and the tip WAS built and tagged in the registry.
|
|
91
|
+
* A pin frozen eight days in the past is what kept a fix for a production
|
|
92
|
+
* crashloop out of every image built from it.
|
|
93
|
+
*/
|
|
94
|
+
export const BUILD_LEDGER_FILE = 'build-ledger.json';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read the committed ledger. A caller that resolves many repositories should
|
|
98
|
+
* read it ONCE and pass it in — the parameter exists for that, not for tests.
|
|
99
|
+
*
|
|
100
|
+
* A malformed ledger THROWS rather than degrading to "no builds recorded":
|
|
101
|
+
* degrading would silently re-pin every repository to its bare tip, which is
|
|
102
|
+
* precisely the defect the ledger exists to prevent. An ABSENT ledger is a
|
|
103
|
+
* different fact and is allowed — it means this checkout has not projected one
|
|
104
|
+
* yet, and the evidence tree still answers.
|
|
105
|
+
*/
|
|
106
|
+
export function loadBuildLedger(
|
|
107
|
+
distributionsDir = dataRoot(),
|
|
108
|
+
) {
|
|
109
|
+
const path = join(distributionsDir, BUILD_LEDGER_FILE);
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code === 'ENOENT') {return null;}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (
|
|
118
|
+
raw === null ||
|
|
119
|
+
typeof raw !== 'object' ||
|
|
120
|
+
raw.schemaVersion !== 1 ||
|
|
121
|
+
typeof raw.repositories !== 'object' ||
|
|
122
|
+
raw.repositories === null ||
|
|
123
|
+
Array.isArray(raw.repositories)
|
|
124
|
+
) {
|
|
125
|
+
throw new Error(`${BUILD_LEDGER_FILE} is not strict schema v1`);
|
|
126
|
+
}
|
|
127
|
+
return raw.repositories;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The committed record of which commit each source repository is pinned at.
|
|
132
|
+
*
|
|
133
|
+
* WHY THIS IS THE AUTHORITY AND NOT A CACHE — the argument is
|
|
134
|
+
* {@link createPinnedSourceAuthority}'s and is not repeated here. What this
|
|
135
|
+
* function adds is that the record is reachable WITHOUT `lib.mjs`, so a builder
|
|
136
|
+
* can read it before anything that needs the kernel dist or the aggregator
|
|
137
|
+
* layout, and so this module keeps its one dependency rule: `node:` builtins
|
|
138
|
+
* only, resolvable from a bare checkout with no install.
|
|
139
|
+
*
|
|
140
|
+
* `runtime-image-source-index.json` OWNS these pins — it is the one artifact
|
|
141
|
+
* whose job is to decide them. Every other derived artifact READS them. That is
|
|
142
|
+
* the whole of "one authority": the decider resolves the world, and the
|
|
143
|
+
* consumers resolve the decider.
|
|
144
|
+
*
|
|
145
|
+
* A MISSING or MALFORMED index THROWS rather than degrading to "no pins
|
|
146
|
+
* recorded". Degrading would silently re-resolve every repository from its tip,
|
|
147
|
+
* which is precisely the defect the pins exist to prevent — and it would do it
|
|
148
|
+
* in the one situation nobody is watching, a checkout where the file failed to
|
|
149
|
+
* read. `loadBuildLedger` takes the same posture for the same reason, and
|
|
150
|
+
* differs on the ABSENT case only because an absent ledger is a real state
|
|
151
|
+
* (this checkout has not projected one) while an absent pin index is not: no
|
|
152
|
+
* edition can resolve without it.
|
|
153
|
+
*/
|
|
154
|
+
export const RUNTIME_IMAGE_SOURCE_INDEX_FILE = 'runtime-image-source-index.json';
|
|
155
|
+
|
|
156
|
+
export function committedRepositoryPins(
|
|
157
|
+
distributionsDir = dataRoot(),
|
|
158
|
+
) {
|
|
159
|
+
const path = join(distributionsDir, RUNTIME_IMAGE_SOURCE_INDEX_FILE);
|
|
160
|
+
let raw;
|
|
161
|
+
try {
|
|
162
|
+
raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Cannot read the committed source pins from ${path}: ${error.message}. ` +
|
|
166
|
+
`Every derived artifact in this plane resolves each repository's commit ` +
|
|
167
|
+
`through that record; there is no fallback to a live resolution, because ` +
|
|
168
|
+
`falling back is how one repository's refresh silently re-pins every other.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const pins = raw?.repositoryPins;
|
|
172
|
+
if (!Array.isArray(pins) || pins.length === 0) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${RUNTIME_IMAGE_SOURCE_INDEX_FILE} declares no repositoryPins`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return pins;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let defaultLedger;
|
|
181
|
+
|
|
182
|
+
function ledgerCommits(repo, ledger) {
|
|
183
|
+
let repositories = ledger;
|
|
184
|
+
if (repositories === undefined) {
|
|
185
|
+
if (defaultLedger === undefined) {defaultLedger = loadBuildLedger();}
|
|
186
|
+
repositories = defaultLedger;
|
|
187
|
+
}
|
|
188
|
+
if (repositories === null) {return null;}
|
|
189
|
+
const commits = repositories[repo];
|
|
190
|
+
return Array.isArray(commits) && commits.length > 0 ? commits : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Every commit this repository has published an image from, as the UNION of the
|
|
195
|
+
* two records of that one fact: the committed evidence tree and the build
|
|
196
|
+
* ledger. Union rather than precedence, because neither can produce a false
|
|
197
|
+
* positive — each is a record that a build actually happened — while either
|
|
198
|
+
* alone has been measured missing whole repositories.
|
|
199
|
+
*/
|
|
200
|
+
function builtCommits(directory, ref, repo, ledger) {
|
|
201
|
+
const fromTree = evidenceTreeCommits(directory, ref);
|
|
202
|
+
const fromLedger = ledgerCommits(repo, ledger);
|
|
203
|
+
// Null from BOTH means neither record has ever heard of this repository, so
|
|
204
|
+
// it publishes no images and its tip is right. An EMPTY union is a different
|
|
205
|
+
// fact — something recorded this repository and named no usable commit — and
|
|
206
|
+
// it must reach the refusal below rather than be read as "publishes nothing".
|
|
207
|
+
// Collapsing the two would silently re-pin to the bare tip, which is the one
|
|
208
|
+
// outcome this whole function exists to prevent.
|
|
209
|
+
if (fromTree === null && fromLedger === null) {return null;}
|
|
210
|
+
const union = new Set(fromTree ?? []);
|
|
211
|
+
for (const commit of fromLedger ?? []) {
|
|
212
|
+
if (/^[0-9a-f]{40}$/u.test(commit)) {union.add(commit);}
|
|
213
|
+
}
|
|
214
|
+
return union;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function evidenceTreeCommits(directory, ref) {
|
|
218
|
+
let listing;
|
|
219
|
+
try {
|
|
220
|
+
listing = git(directory, ['ls-tree', '-r', '--name-only', ref, 'evidence/'], {
|
|
221
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
222
|
+
});
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const statements = listing
|
|
227
|
+
.split('\n')
|
|
228
|
+
.filter((file) => /statements\/[^/]+\.json$/u.test(file));
|
|
229
|
+
if (statements.length === 0) {return null;}
|
|
230
|
+
|
|
231
|
+
const commits = new Set();
|
|
232
|
+
for (const file of statements) {
|
|
233
|
+
let statement;
|
|
234
|
+
try {
|
|
235
|
+
statement = JSON.parse(
|
|
236
|
+
git(directory, ['show', `${ref}:${file}`], { maxBuffer: 16 * 1024 * 1024 }),
|
|
237
|
+
);
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const commit = statement?.predicate?.source?.sourceCommit;
|
|
242
|
+
if (typeof commit === 'string' && /^[0-9a-f]{40}$/u.test(commit)) {
|
|
243
|
+
commits.add(commit);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return commits.size > 0 ? commits : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolve the immutable release source without consulting local HEAD.
|
|
251
|
+
*
|
|
252
|
+
* THIS RETURNS THE NEWEST *BUILT* COMMIT, NOT THE TIP, and the difference is the
|
|
253
|
+
* whole point.
|
|
254
|
+
*
|
|
255
|
+
* It returned `refs/remotes/origin/main` verbatim until 2026-09-01, with nothing
|
|
256
|
+
* checking that anything had ever been built there. Measured that day against
|
|
257
|
+
* the last twelve `xema-base` commits, probing ghcr for a published `:<sha>`
|
|
258
|
+
* tag: ONE had an image, and that one only because it had been hand-dispatched.
|
|
259
|
+
* Builds run 4-12 a day against 150-353 commits, so an arbitrary tip is almost
|
|
260
|
+
* never buildable — most of `main` is `evidence(base)` receipts, which are
|
|
261
|
+
* authored by `github-actions[bot]` and therefore get no CI run under GitHub's
|
|
262
|
+
* anti-recursion rule, hence no `workflow_run`, hence no build.
|
|
263
|
+
*
|
|
264
|
+
* The consequence was not subtle. A re-pin that landed on an unbuilt commit made
|
|
265
|
+
* every distribution lock declare coordinates the registry answers
|
|
266
|
+
* MANIFEST_UNKNOWN for — 41 of 43 after one such re-pin — and `Verify image
|
|
267
|
+
* platforms` went red across every edition. The producer and the resolver held
|
|
268
|
+
* two different definitions of "the release commit": a commit that was BUILT,
|
|
269
|
+
* versus whatever is at the tip.
|
|
270
|
+
*
|
|
271
|
+
* Building the missing coordinate by hand clears the symptom at one commit and
|
|
272
|
+
* leaves this mechanism intact, so the next re-pin reproduces it. This is the
|
|
273
|
+
* mechanism.
|
|
274
|
+
*/
|
|
275
|
+
/**
|
|
276
|
+
* The published TIP of a source repository — the SOURCE-provenance axis.
|
|
277
|
+
*
|
|
278
|
+
* ── TWO AXES, ONE PUBLISHED FACT ──
|
|
279
|
+
*
|
|
280
|
+
* This and {@link publishedSourceCommit} read the SAME ref and differ only in
|
|
281
|
+
* what they are allowed to SELECT from it, which is the whole distinction:
|
|
282
|
+
*
|
|
283
|
+
* - **image provenance** asks *"which commit did we actually build an image
|
|
284
|
+
* from?"* and must therefore walk back to published image evidence;
|
|
285
|
+
* - **source provenance** asks *"where do these vendored bytes come from?"*
|
|
286
|
+
* and needs only a commit that is published and immutable.
|
|
287
|
+
*
|
|
288
|
+
* Answering the second question with the first answer is not conservative, it
|
|
289
|
+
* is WRONG, and the way it fails is structural rather than occasional:
|
|
290
|
+
*
|
|
291
|
+
* a CONTENT-ONLY component — one with no `api/` directory, shipping no
|
|
292
|
+
* service — can never produce an image, therefore never earns image
|
|
293
|
+
* evidence, therefore can never be named by the image axis, therefore its
|
|
294
|
+
* source changes CANNOT REACH A CONSUMER'S IMAGE AT ALL.
|
|
295
|
+
*
|
|
296
|
+
* Measured 2026-09-14: `biomes/platform/memory-maintenance` in `xema-cultivars`
|
|
297
|
+
* is exactly that shape. Its agent frontmatter had migrated `modelClass` ->
|
|
298
|
+
* `modelLane`; the fix sat on that repository's `main`, and the image-evidence
|
|
299
|
+
* pin could only name a commit 130 behind it that still carried the retired
|
|
300
|
+
* key. `biome-host-api` baked the stale file, its own strict parser refused it,
|
|
301
|
+
* and the new ReplicaSet crashlooped in production while the previous one kept
|
|
302
|
+
* serving — so the defect was invisible to every health signal.
|
|
303
|
+
*
|
|
304
|
+
* `publishedSourceCommit` already contained half of this distinction and could
|
|
305
|
+
* not express it: its `built === null` arm returns the tip for a repository
|
|
306
|
+
* that publishes no images at all. That arm is right, and it was keyed on the
|
|
307
|
+
* REPOSITORY when the question belongs to the SUBJECT — a repository that
|
|
308
|
+
* publishes some images still vendors content-only source out of it.
|
|
309
|
+
*
|
|
310
|
+
* ── THIS IS NOT A RETURN TO "WHATEVER HEAD HAPPENED TO BE" ──
|
|
311
|
+
*
|
|
312
|
+
* Nothing here reads local `HEAD` or a working tree, and the value is resolved
|
|
313
|
+
* ONCE into a committed pin that the build then reads. The revision stays
|
|
314
|
+
* DECLARED; only the menu it is declared from stops being restricted to
|
|
315
|
+
* commits that happen to have shipped an image.
|
|
316
|
+
*/
|
|
317
|
+
export function publishedSourceTip(directory, repo) {
|
|
318
|
+
try {
|
|
319
|
+
return exactCommit(
|
|
320
|
+
directory,
|
|
321
|
+
PUBLISHED_SOURCE_REF,
|
|
322
|
+
`source repository "${repo}"`,
|
|
323
|
+
);
|
|
324
|
+
} catch {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`source repository "${repo}" has no ${PUBLISHED_SOURCE_REF} — a distribution ` +
|
|
327
|
+
`pin must name a published commit. Fetch the repository before regenerating.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function publishedSourceCommit(directory, repo, { ledger } = {}) {
|
|
333
|
+
const tip = publishedSourceTip(directory, repo);
|
|
334
|
+
|
|
335
|
+
const built = builtCommits(directory, PUBLISHED_SOURCE_REF, repo, ledger);
|
|
336
|
+
// Neither record knows this repository, so it publishes no images at all
|
|
337
|
+
// (xema-connectors contributes vendored content only). Nothing about it can
|
|
338
|
+
// be unbuildable and the tip is exactly right — as it always was.
|
|
339
|
+
if (built === null) {return tip;}
|
|
340
|
+
|
|
341
|
+
let walked;
|
|
342
|
+
try {
|
|
343
|
+
walked = git(directory, [
|
|
344
|
+
'rev-list',
|
|
345
|
+
`--max-count=${BUILT_COMMIT_SEARCH_LIMIT}`,
|
|
346
|
+
tip,
|
|
347
|
+
]).split('\n');
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`source repository "${repo}" could not be walked back from ${PUBLISHED_SOURCE_REF}`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const commit of walked) {
|
|
355
|
+
if (built.has(commit)) {return commit;}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Fail fast and loudly. Silently falling back to the tip is the defect this
|
|
359
|
+
// function exists to remove, and a pin nobody can build is worse than a
|
|
360
|
+
// refusal somebody can read.
|
|
361
|
+
throw new Error(
|
|
362
|
+
`source repository "${repo}" has no commit with published image evidence within ` +
|
|
363
|
+
`${BUILT_COMMIT_SEARCH_LIMIT} commits of ${PUBLISHED_SOURCE_REF}. A distribution ` +
|
|
364
|
+
`pin must name a commit an image was actually built from — pinning the bare tip ` +
|
|
365
|
+
`is what made every lock declare coordinates the registry answers ` +
|
|
366
|
+
`MANIFEST_UNKNOWN for. Build that repository at a recent commit, or widen ` +
|
|
367
|
+
`BUILT_COMMIT_SEARCH_LIMIT if its release cadence is genuinely slower than that.`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function validateRepository(repoRoot, repo, knownRepositories) {
|
|
372
|
+
if (!knownRepositories.has(repo)) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`source repository "${repo}" is not in the explicit distribution source repository set`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const directory = resolve(repoRoot, repo);
|
|
378
|
+
const rel = relative(repoRoot, directory);
|
|
379
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`)) {
|
|
380
|
+
throw new Error(`source repository "${repo}" resolves outside the aggregator`);
|
|
381
|
+
}
|
|
382
|
+
if (!existsSync(directory)) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`source repository "${repo}" is missing at ${directory}; source resolution never falls back to another authority`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const actualTopLevel = realpathSync(
|
|
388
|
+
git(directory, ['rev-parse', '--show-toplevel']),
|
|
389
|
+
);
|
|
390
|
+
if (actualTopLevel !== realpathSync(directory)) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`source repository "${repo}" resolves to ${directory}, but Git reports top-level ${actualTopLevel}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return directory;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function workspaceSnapshotCommit(directory, repo) {
|
|
399
|
+
const head = exactCommit(directory, 'HEAD', `workspace repository "${repo}"`);
|
|
400
|
+
const temporary = mkdtempSync(join(tmpdir(), 'xema-distribution-source-'));
|
|
401
|
+
const indexPath = join(temporary, 'index');
|
|
402
|
+
const env = { ...process.env, GIT_INDEX_FILE: indexPath };
|
|
403
|
+
try {
|
|
404
|
+
git(directory, ['read-tree', head], { env });
|
|
405
|
+
git(directory, ['add', '-A', '--', '.'], { env });
|
|
406
|
+
const tree = git(directory, ['write-tree'], { env });
|
|
407
|
+
const headTree = git(directory, ['rev-parse', `${head}^{tree}`]);
|
|
408
|
+
if (tree === headTree) {return { commit: head, head, dirty: false };}
|
|
409
|
+
const commit = execFileSync(
|
|
410
|
+
'git',
|
|
411
|
+
['-C', directory, 'commit-tree', tree, '-p', head],
|
|
412
|
+
{
|
|
413
|
+
encoding: 'utf8',
|
|
414
|
+
input: 'xema local workspace source snapshot\n',
|
|
415
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
416
|
+
env: {
|
|
417
|
+
...process.env,
|
|
418
|
+
GIT_AUTHOR_NAME: 'Xema Workspace Source',
|
|
419
|
+
GIT_AUTHOR_EMAIL: 'workspace-source@xema.invalid',
|
|
420
|
+
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
|
|
421
|
+
GIT_COMMITTER_NAME: 'Xema Workspace Source',
|
|
422
|
+
GIT_COMMITTER_EMAIL: 'workspace-source@xema.invalid',
|
|
423
|
+
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
).trim();
|
|
427
|
+
return { commit, head, dirty: true };
|
|
428
|
+
} finally {
|
|
429
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function parseSourceModeArgs(argv) {
|
|
434
|
+
const remaining = [];
|
|
435
|
+
let mode = DistributionSourceMode.Published;
|
|
436
|
+
let declared = false;
|
|
437
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
438
|
+
if (argv[index] !== '--source') {
|
|
439
|
+
remaining.push(argv[index]);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (declared) {throw new Error('--source may be declared only once');}
|
|
443
|
+
const value = argv[index + 1];
|
|
444
|
+
if (!Object.values(DistributionSourceMode).includes(value)) {
|
|
445
|
+
throw new Error('--source requires exactly "published" or "workspace"');
|
|
446
|
+
}
|
|
447
|
+
mode = value;
|
|
448
|
+
declared = true;
|
|
449
|
+
index += 1;
|
|
450
|
+
}
|
|
451
|
+
return { mode, remaining };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The delegate a pure-record authority is given: one that cannot answer.
|
|
456
|
+
*
|
|
457
|
+
* Its message names the repository AND says what asking would have cost, so a
|
|
458
|
+
* reader meeting it in a stack trace does not have to find this file to learn
|
|
459
|
+
* why a resolution that works everywhere else is refused here.
|
|
460
|
+
*/
|
|
461
|
+
function refusingDelegate() {
|
|
462
|
+
const refuse = (repo) => {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`source repository "${repo}" reached a LIVE resolution from an artifact ` +
|
|
465
|
+
`whose commits are decided by the committed pins. Nothing downstream of ` +
|
|
466
|
+
`runtime-image-source-index.json may ask the world what a repository's ` +
|
|
467
|
+
`commit is: that is a second authority, and it moves pins nobody asked ` +
|
|
468
|
+
`to move. Refresh the pin itself (build-runtime-image-source-index.mjs ` +
|
|
469
|
+
`--trusted-write --repo ${repo}), or declare the live resolution ` +
|
|
470
|
+
`explicitly with --unpinned.`,
|
|
471
|
+
);
|
|
472
|
+
};
|
|
473
|
+
return Object.freeze({
|
|
474
|
+
mode: DistributionSourceMode.Published,
|
|
475
|
+
commitForRepository: refuse,
|
|
476
|
+
vendoredSourceCommitForRepository: refuse,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* A source authority that RESOLVES exactly one repository and answers every
|
|
482
|
+
* other from a set of pins already decided.
|
|
483
|
+
*
|
|
484
|
+
* ── WHY THE PIN SET IS THE AUTHORITY AND NOT A CACHE ──
|
|
485
|
+
*
|
|
486
|
+
* `commitForRepository` asks the world a live question — "what is the newest
|
|
487
|
+
* commit of this repository with published image evidence?" — and the answer
|
|
488
|
+
* moves whenever a build lands. Every derived artifact that calls it therefore
|
|
489
|
+
* re-decides every repository's pin on every run, and two artifacts derived
|
|
490
|
+
* minutes apart can disagree. `resolve-images` already refuses that
|
|
491
|
+
* disagreement: it re-reads each biome manifest at the RUNTIME INDEX's pin and
|
|
492
|
+
* fails when the biome index was built from a different commit.
|
|
493
|
+
*
|
|
494
|
+
* So the pins are not an optimisation. They are the DECISION, and a refresh
|
|
495
|
+
* that means to move one of them must leave the rest exactly where they are —
|
|
496
|
+
* otherwise shipping a fix in one repository silently re-pins every other,
|
|
497
|
+
* and every subject derived from a moved pin becomes a coordinate the registry
|
|
498
|
+
* must already be serving.
|
|
499
|
+
*
|
|
500
|
+
* A repository with neither a pin nor the refresh scope is a REFUSAL. Falling
|
|
501
|
+
* through to a live resolution would reintroduce the drift by the back door,
|
|
502
|
+
* in the one case nobody is looking at — a repository that JOINED the index
|
|
503
|
+
* since the pins were written.
|
|
504
|
+
*/
|
|
505
|
+
export function createPinnedSourceAuthority({
|
|
506
|
+
delegate,
|
|
507
|
+
refreshRepository,
|
|
508
|
+
pins,
|
|
509
|
+
}) {
|
|
510
|
+
// A refresh scope is the ONLY thing that can reach the live resolver, so it is
|
|
511
|
+
// the only thing that needs one. Omitting the delegate is the ordinary case
|
|
512
|
+
// for a CONSUMER artifact — one whose commits are all decided already — and it
|
|
513
|
+
// is substituted with a delegate that THROWS rather than left undefined.
|
|
514
|
+
//
|
|
515
|
+
// That substitution is the point rather than a convenience. "No repository was
|
|
516
|
+
// resolved live" is otherwise a property of the CALL SITE, provable only by
|
|
517
|
+
// reading it and re-provable after every edit; with a refusing delegate it is
|
|
518
|
+
// a property of the OBJECT, and a future edit that reaches for a live answer
|
|
519
|
+
// fails loudly at the moment it does. It is the positive control that proved
|
|
520
|
+
// `biome-index.json` is already pin-derived, kept rather than thrown away.
|
|
521
|
+
if (refreshRepository !== undefined && !delegate) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
'a pinned source authority with a refresh scope needs a delegate to resolve it',
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const live = delegate ?? refusingDelegate();
|
|
527
|
+
const byRepo = new Map();
|
|
528
|
+
for (const pin of pins ?? []) {byRepo.set(pin.repo, pin);}
|
|
529
|
+
if (byRepo.size === 0) {
|
|
530
|
+
throw new Error('a pinned source authority needs at least one pin');
|
|
531
|
+
}
|
|
532
|
+
if (refreshRepository !== undefined && !byRepo.has(refreshRepository)) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`refresh scope "${refreshRepository}" has no pin to refresh. A repository ` +
|
|
535
|
+
`joining the index is a full rebuild, not a scoped refresh.`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
const pinned = (repo, field) => {
|
|
539
|
+
const pin = byRepo.get(repo);
|
|
540
|
+
if (!pin) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`source repository "${repo}" has no committed pin and is not the ` +
|
|
543
|
+
`refresh scope. Refusing to resolve it live: that is how a scoped ` +
|
|
544
|
+
`refresh silently re-pins a repository nobody asked about.`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return pin[field];
|
|
548
|
+
};
|
|
549
|
+
return Object.freeze({
|
|
550
|
+
// A record of pins carries no mode of its own, and only PUBLISHED mode can
|
|
551
|
+
// have produced one: a workspace snapshot is local-only and unpublishable by
|
|
552
|
+
// construction, so it is never written to this index. Stated rather than
|
|
553
|
+
// defaulted — a reader asking a pure-record authority for its mode is asking
|
|
554
|
+
// what produced the pins, and the answer is not "whatever the caller had".
|
|
555
|
+
mode: delegate?.mode ?? DistributionSourceMode.Published,
|
|
556
|
+
commitForRepository(repo) {
|
|
557
|
+
if (repo === refreshRepository) {return live.commitForRepository(repo);}
|
|
558
|
+
return pinned(repo, 'sourceCommit');
|
|
559
|
+
},
|
|
560
|
+
vendoredSourceCommitForRepository(repo) {
|
|
561
|
+
if (repo === refreshRepository) {
|
|
562
|
+
return live.vendoredSourceCommitForRepository(repo);
|
|
563
|
+
}
|
|
564
|
+
return pinned(repo, 'vendoredSourceCommit');
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Pin ONE repository at an EXACT named commit, and answer every other from the
|
|
571
|
+
* delegate.
|
|
572
|
+
*
|
|
573
|
+
* WHY A NAMED COMMIT AND NOT "THE NEWEST BUILT ONE". `publishedSourceCommit`
|
|
574
|
+
* asks the world a question whose answer moves whenever a build lands, so a
|
|
575
|
+
* refresh that means to ship a SPECIFIC fix is a race against the fleet: the
|
|
576
|
+
* operator names the commit that carries the fix, the resolver picks whatever is
|
|
577
|
+
* newest at the instant it runs, and the two are the same value only until they
|
|
578
|
+
* are not. The ruling this plane now follows says the pin is an EXACT SOURCE
|
|
579
|
+
* REF; this is how an operator states one.
|
|
580
|
+
*
|
|
581
|
+
* It is a NARROWING, never a widening. The commit must exist, must be a real
|
|
582
|
+
* commit object, and must be CONTAINED IN the published ref — so this cannot pin
|
|
583
|
+
* a topic branch, a commit that was force-pushed away, or anything a consumer
|
|
584
|
+
* could not fetch. What it can do is name a commit OLDER than the newest built
|
|
585
|
+
* one, which is the whole point: that is a deliberate hold-back or a deliberate
|
|
586
|
+
* choice of the commit that carries a fix.
|
|
587
|
+
*
|
|
588
|
+
* It does NOT verify the commit was built. `publishedSourceCommit` exists to
|
|
589
|
+
* stop an ARBITRARY tip being pinned, and its refusal text explains what that
|
|
590
|
+
* cost; an operator naming a commit by hand has taken that decision explicitly,
|
|
591
|
+
* and the coordinate is proven by `dist:verify-platforms` against the real
|
|
592
|
+
* registry either way. Substituting the resolver's judgement for the operator's
|
|
593
|
+
* here would make the flag a suggestion.
|
|
594
|
+
*/
|
|
595
|
+
export function createExactCommitSourceAuthority({
|
|
596
|
+
delegate,
|
|
597
|
+
repo,
|
|
598
|
+
commit,
|
|
599
|
+
repoRoot = sourcesRoot(),
|
|
600
|
+
}) {
|
|
601
|
+
if (!delegate) {throw new Error('an exact-commit authority needs a delegate');}
|
|
602
|
+
if (!/^[0-9a-f]{40}$/u.test(String(commit ?? ''))) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`--at requires a full 40-character commit sha, not "${String(commit)}". ` +
|
|
605
|
+
`An abbreviation is ambiguous across repositories and a ref is not exact.`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const directory = resolve(repoRoot, repo);
|
|
609
|
+
let resolved;
|
|
610
|
+
try {
|
|
611
|
+
resolved = git(directory, ['rev-parse', '--verify', `${commit}^{commit}`]);
|
|
612
|
+
} catch {
|
|
613
|
+
throw new Error(
|
|
614
|
+
`source repository "${repo}" has no commit ${commit}. Fetch it before pinning.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (resolved !== commit) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
`${commit} resolves to ${resolved} in "${repo}" — pass the commit itself.`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
git(directory, [
|
|
624
|
+
'merge-base',
|
|
625
|
+
'--is-ancestor',
|
|
626
|
+
commit,
|
|
627
|
+
PUBLISHED_SOURCE_REF,
|
|
628
|
+
]);
|
|
629
|
+
} catch {
|
|
630
|
+
throw new Error(
|
|
631
|
+
`${commit} is not contained in ${PUBLISHED_SOURCE_REF} of "${repo}". A ` +
|
|
632
|
+
`distribution pin must name a PUBLISHED commit: anything else cannot be ` +
|
|
633
|
+
`fetched by a consumer and can be force-pushed away under the lock.`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return Object.freeze({
|
|
637
|
+
mode: delegate.mode,
|
|
638
|
+
commitForRepository(target) {
|
|
639
|
+
return target === repo ? commit : delegate.commitForRepository(target);
|
|
640
|
+
},
|
|
641
|
+
vendoredSourceCommitForRepository(target) {
|
|
642
|
+
// BOTH axes. The two-axis split exists because a content-only biome can
|
|
643
|
+
// never earn image evidence and so must be read at the SOURCE tip rather
|
|
644
|
+
// than at a possibly-much-older image commit. An operator naming an exact
|
|
645
|
+
// commit has answered both questions at once, and leaving the source axis
|
|
646
|
+
// on the live tip would put a moving value back into a pin the operator
|
|
647
|
+
// asked to be exact — half a race is still a race.
|
|
648
|
+
return target === repo
|
|
649
|
+
? commit
|
|
650
|
+
: delegate.vendoredSourceCommitForRepository(target);
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function createSourceAuthority({
|
|
656
|
+
mode = DistributionSourceMode.Published,
|
|
657
|
+
repoRoot = sourcesRoot(),
|
|
658
|
+
knownRepositories,
|
|
659
|
+
log = console.log,
|
|
660
|
+
} = {}) {
|
|
661
|
+
if (!Object.values(DistributionSourceMode).includes(mode)) {
|
|
662
|
+
throw new Error(`unknown distribution source mode "${String(mode)}"`);
|
|
663
|
+
}
|
|
664
|
+
if (
|
|
665
|
+
!knownRepositories ||
|
|
666
|
+
typeof knownRepositories === 'string' ||
|
|
667
|
+
typeof knownRepositories[Symbol.iterator] !== 'function'
|
|
668
|
+
) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
'source authority requires the caller to declare its source repositories',
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const known = new Set(knownRepositories);
|
|
674
|
+
if (known.size === 0) {
|
|
675
|
+
throw new Error('source authority repository declaration must not be empty');
|
|
676
|
+
}
|
|
677
|
+
const resolvedRoot = realpathSync(repoRoot);
|
|
678
|
+
const identities = new Map();
|
|
679
|
+
const vendored = new Map();
|
|
680
|
+
return Object.freeze({
|
|
681
|
+
mode,
|
|
682
|
+
/**
|
|
683
|
+
* The SOURCE-provenance revision — see {@link publishedSourceTip} for why
|
|
684
|
+
* this is a different question from `commitForRepository`.
|
|
685
|
+
*
|
|
686
|
+
* In WORKSPACE mode the two deliberately coincide: a workspace snapshot is
|
|
687
|
+
* local-only and unpublishable by construction, so there is no image-
|
|
688
|
+
* evidence constraint to diverge from and no second fact to resolve.
|
|
689
|
+
*/
|
|
690
|
+
vendoredSourceCommitForRepository(repo) {
|
|
691
|
+
const cached = vendored.get(repo);
|
|
692
|
+
if (cached) {return cached;}
|
|
693
|
+
const directory = validateRepository(resolvedRoot, repo, known);
|
|
694
|
+
if (mode !== DistributionSourceMode.Published) {
|
|
695
|
+
return this.commitForRepository(repo);
|
|
696
|
+
}
|
|
697
|
+
const commit = publishedSourceTip(directory, repo);
|
|
698
|
+
vendored.set(repo, commit);
|
|
699
|
+
log(
|
|
700
|
+
`[source-authority] vendored-source ${repo}@${commit} (${PUBLISHED_SOURCE_REF} tip)`,
|
|
701
|
+
);
|
|
702
|
+
return commit;
|
|
703
|
+
},
|
|
704
|
+
commitForRepository(repo) {
|
|
705
|
+
const cached = identities.get(repo);
|
|
706
|
+
if (cached) {return cached.commit;}
|
|
707
|
+
const directory = validateRepository(resolvedRoot, repo, known);
|
|
708
|
+
if (mode === DistributionSourceMode.Published) {
|
|
709
|
+
const commit = publishedSourceCommit(directory, repo);
|
|
710
|
+
identities.set(repo, { commit });
|
|
711
|
+
log(
|
|
712
|
+
`[source-authority] published ${repo}@${commit} (${PUBLISHED_SOURCE_REF})`,
|
|
713
|
+
);
|
|
714
|
+
return commit;
|
|
715
|
+
}
|
|
716
|
+
const snapshot = workspaceSnapshotCommit(directory, repo);
|
|
717
|
+
identities.set(repo, snapshot);
|
|
718
|
+
log(
|
|
719
|
+
`[source-authority] workspace ${repo}@${snapshot.commit} ` +
|
|
720
|
+
`(HEAD ${snapshot.head}; ${snapshot.dirty ? 'dirty snapshot — local only, do not publish' : 'clean'})`,
|
|
721
|
+
);
|
|
722
|
+
return snapshot.commit;
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|