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