@kungfu-tech/buildchain 4.0.1-alpha.6 → 4.0.1-alpha.9
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/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/maintainability-policy.json +13 -13
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +19 -9
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/kfd-claims.json +31 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +154 -73
- package/dist/site/page-registry.json +14 -4
- package/dist/site/public-surface-audit.json +14 -3
- package/dist/site/publication-authority-registry.json +23 -5
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +25 -4
- package/docs/dev-delivery-warrant.md +22 -1
- package/docs/node-api-reference.md +161 -148
- package/package.json +3 -2
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/ci-lane-change-budget.js +247 -0
- package/packages/core/dev-delivery-native-proof.js +43 -27
- package/packages/core/dev-delivery-warrant-state.js +31 -8
- package/packages/core/dev-delivery-warrant.js +8 -3
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- package/packages/core/release-candidate-recovery.js +66 -11
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +10 -1
- package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +138 -35
- package/packages/core/v4-floating-consumer-release-passport.js +30 -3
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +2 -2
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +5 -1
- package/scripts/dev-pr-delivery-warrant.mjs +1 -0
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/resume-from-candidate-run.mjs +11 -14
- package/scripts/site-capability-metadata.mjs +1 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { evaluateCiLaneChangeBudget } from "../packages/core/ci-lane-change-budget.js";
|
|
8
|
+
|
|
9
|
+
const root = process.cwd();
|
|
10
|
+
const workflowRoot = path.join(root, ".github", "workflows");
|
|
11
|
+
const policyPath = path.join(
|
|
12
|
+
root,
|
|
13
|
+
"architecture",
|
|
14
|
+
"ci-lane-change-budget.json",
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const workflows = fs
|
|
18
|
+
.readdirSync(workflowRoot, { withFileTypes: true })
|
|
19
|
+
.filter((entry) => entry.isFile() && /\.(?:yaml|yml)$/u.test(entry.name))
|
|
20
|
+
.map((entry) => {
|
|
21
|
+
const workflowPath = path.posix.join(".github/workflows", entry.name);
|
|
22
|
+
return {
|
|
23
|
+
path: workflowPath,
|
|
24
|
+
text: fs.readFileSync(path.join(workflowRoot, entry.name), "utf8"),
|
|
25
|
+
};
|
|
26
|
+
})
|
|
27
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
28
|
+
|
|
29
|
+
const policy = JSON.parse(fs.readFileSync(policyPath, "utf8"));
|
|
30
|
+
const baselineRevision = String(policy.baseline?.gitRevision || "").trim();
|
|
31
|
+
if (!/^[0-9a-f]{40}$/u.test(baselineRevision)) {
|
|
32
|
+
throw new Error("baseline.gitRevision must be an exact Git commit");
|
|
33
|
+
}
|
|
34
|
+
const embeddedBaselineLanes = policy.baseline?.lanes;
|
|
35
|
+
if (
|
|
36
|
+
!Array.isArray(embeddedBaselineLanes) ||
|
|
37
|
+
embeddedBaselineLanes.length === 0
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("baseline.lanes must contain the exact offline lane set");
|
|
40
|
+
}
|
|
41
|
+
const embeddedLaneSetRoot = `sha256:${crypto
|
|
42
|
+
.createHash("sha256")
|
|
43
|
+
.update(`${JSON.stringify(embeddedBaselineLanes)}\n`)
|
|
44
|
+
.digest("hex")}`;
|
|
45
|
+
if (embeddedLaneSetRoot !== policy.baseline?.laneSetRoot) {
|
|
46
|
+
throw new Error("baseline laneSetRoot does not match baseline.lanes");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let baselineWorkflows = null;
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("git", ["cat-file", "-e", `${baselineRevision}^{commit}`], {
|
|
52
|
+
cwd: root,
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
});
|
|
55
|
+
const baselinePaths = execFileSync(
|
|
56
|
+
"git",
|
|
57
|
+
[
|
|
58
|
+
"ls-tree",
|
|
59
|
+
"-r",
|
|
60
|
+
"--name-only",
|
|
61
|
+
baselineRevision,
|
|
62
|
+
"--",
|
|
63
|
+
".github/workflows",
|
|
64
|
+
],
|
|
65
|
+
{ cwd: root, encoding: "utf8" },
|
|
66
|
+
)
|
|
67
|
+
.split(/\r?\n/u)
|
|
68
|
+
.filter((value) => /\.(?:yaml|yml)$/u.test(value))
|
|
69
|
+
.sort();
|
|
70
|
+
baselineWorkflows = baselinePaths.map((workflowPath) => ({
|
|
71
|
+
path: workflowPath,
|
|
72
|
+
text: execFileSync("git", ["show", `${baselineRevision}:${workflowPath}`], {
|
|
73
|
+
cwd: root,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
} catch {
|
|
78
|
+
// Shallow and exported source cuts may not contain the pinned commit object.
|
|
79
|
+
// The content-rooted embedded lane set remains the exact offline authority.
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const embeddedEvaluation = evaluateCiLaneChangeBudget({
|
|
83
|
+
policy,
|
|
84
|
+
workflows,
|
|
85
|
+
});
|
|
86
|
+
const evaluation = baselineWorkflows
|
|
87
|
+
? evaluateCiLaneChangeBudget({ policy, workflows, baselineWorkflows })
|
|
88
|
+
: embeddedEvaluation;
|
|
89
|
+
if (
|
|
90
|
+
baselineWorkflows &&
|
|
91
|
+
(evaluation.baselineLaneCount !== embeddedEvaluation.baselineLaneCount ||
|
|
92
|
+
JSON.stringify(evaluation.newLanes) !==
|
|
93
|
+
JSON.stringify(embeddedEvaluation.newLanes))
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"embedded baseline lanes do not match the pinned baseline revision",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
|
|
100
|
+
if (!evaluation.ok) process.exitCode = 1;
|
|
@@ -1021,7 +1021,7 @@ for (const forbiddenSnippet of [
|
|
|
1021
1021
|
for (const requiredSnippet of [
|
|
1022
1022
|
"id-token: write",
|
|
1023
1023
|
"actions: write",
|
|
1024
|
-
"uses: kungfu-systems/buildchain/.github/workflows
|
|
1024
|
+
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4-alpha",
|
|
1025
1025
|
"github.event.workflow_run.event == 'push'",
|
|
1026
1026
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
|
|
1027
1027
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
|
|
@@ -1030,7 +1030,7 @@ for (const requiredSnippet of [
|
|
|
1030
1030
|
"resume-buildchain-runtime-sha:",
|
|
1031
1031
|
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
|
|
1032
1032
|
"release-passport-buildchain-self-kfd: true",
|
|
1033
|
-
"
|
|
1033
|
+
"buildchain-channel: alpha",
|
|
1034
1034
|
"release-passport-impact-json: .buildchain/release-impact.json",
|
|
1035
1035
|
]) {
|
|
1036
1036
|
if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
|
|
@@ -52,8 +52,13 @@ function assertOrdered(relative, markers) {
|
|
|
52
52
|
|
|
53
53
|
export function assertPromotionCertificationWiring(source) {
|
|
54
54
|
for (const marker of [
|
|
55
|
-
".buildchain/runtime/
|
|
56
|
-
|
|
55
|
+
"path: .buildchain/runtime/candidate-policy-runtime",
|
|
56
|
+
"BUILDCHAIN_EXPECTED_RUNTIME_SHA: ${{ steps.v4-policy-caller.outputs.runtime-sha }}",
|
|
57
|
+
"policy_runtime=.buildchain/runtime",
|
|
58
|
+
'node "${policy_runtime}/scripts/v4-consumer-policy.mjs" certify',
|
|
59
|
+
"passport.consumerPolicy?.receipt?.caller?.sourceSha",
|
|
60
|
+
"path: .buildchain/runtime/policy-caller",
|
|
61
|
+
'--caller-root "${{ github.workspace }}/.buildchain/runtime/policy-caller"',
|
|
57
62
|
'--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
|
|
58
63
|
'--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
|
|
59
64
|
"release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
|
|
@@ -78,13 +83,22 @@ function assertPersistedSelectors() {
|
|
|
78
83
|
);
|
|
79
84
|
if (!match) continue;
|
|
80
85
|
const selector = match[2];
|
|
86
|
+
const protectedBootstrap =
|
|
87
|
+
relative ===
|
|
88
|
+
".github/workflows/buildchain-ref-promotion-recovery.yml" &&
|
|
89
|
+
match[1] === ".github/workflows/.release-candidate-promote.yml" &&
|
|
90
|
+
selector === "alpha/v4/v4.0";
|
|
81
91
|
const isV4 =
|
|
82
92
|
selector === "v4" ||
|
|
83
93
|
selector === "v4-alpha" ||
|
|
84
94
|
/^v4(?:[./-]|$)/u.test(selector) ||
|
|
85
95
|
/^[0-9a-f]{40}$/iu.test(selector) ||
|
|
86
96
|
selector.includes("${{");
|
|
87
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
(isV4 || protectedBootstrap) &&
|
|
99
|
+
!["v4", "v4-alpha"].includes(selector) &&
|
|
100
|
+
!protectedBootstrap
|
|
101
|
+
) {
|
|
88
102
|
offenders.push(`${relative}:${node.line} @${selector}`);
|
|
89
103
|
}
|
|
90
104
|
}
|
|
@@ -112,7 +126,12 @@ export function checkV4FloatingConsumerPolicyContract() {
|
|
|
112
126
|
"Enforce v4 floating consumer policy",
|
|
113
127
|
"Validate consumer package manager contract",
|
|
114
128
|
]);
|
|
115
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
!read(".github/workflows/.build.yml").includes(
|
|
131
|
+
"BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: ${{ inputs.buildchain-expected-channel }}",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
fail("channel builds must disambiguate dual-channel caller invocations");
|
|
116
135
|
assertTrustGatedJobs(read(".github/workflows/.build.yml"), [
|
|
117
136
|
"resolve-source",
|
|
118
137
|
"resolve-contract",
|
|
@@ -46,7 +46,7 @@ function decodeBlob(blob) {
|
|
|
46
46
|
).toString("utf8");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function validateStoredState(queue) {
|
|
49
|
+
function validateStoredState(queue, { allowLegacyV3Readback = false } = {}) {
|
|
50
50
|
const body = structuredClone(queue || {});
|
|
51
51
|
const embeddedStateRoot = body.stateRoot;
|
|
52
52
|
delete body.stateRoot;
|
|
@@ -59,7 +59,7 @@ function validateStoredState(queue) {
|
|
|
59
59
|
) {
|
|
60
60
|
normalizeDevDeliveryAuthorityState(queue);
|
|
61
61
|
} else {
|
|
62
|
-
normalizeDevDeliveryQueue(queue);
|
|
62
|
+
normalizeDevDeliveryQueue(queue, { allowLegacyV3Readback });
|
|
63
63
|
}
|
|
64
64
|
return queue;
|
|
65
65
|
}
|
|
@@ -108,7 +108,7 @@ export class GitHubDevDeliveryStore {
|
|
|
108
108
|
return data;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
async read({ stateRef, protectedBase, now }) {
|
|
111
|
+
async read({ stateRef, protectedBase, now, allowLegacyV3Readback = false }) {
|
|
112
112
|
let ref;
|
|
113
113
|
try {
|
|
114
114
|
ref = await this.request(
|
|
@@ -128,11 +128,13 @@ export class GitHubDevDeliveryStore {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
const commitSha = exactSha(ref?.object?.sha, "state ref commit");
|
|
131
|
-
const readback = await this.readCommit(commitSha
|
|
131
|
+
const readback = await this.readCommit(commitSha, {
|
|
132
|
+
allowLegacyV3Readback,
|
|
133
|
+
});
|
|
132
134
|
return { exists: true, ...readback };
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
async readCommit(commitShaInput) {
|
|
137
|
+
async readCommit(commitShaInput, { allowLegacyV3Readback = false } = {}) {
|
|
136
138
|
const commitSha = exactSha(commitShaInput, "state commit");
|
|
137
139
|
const commit = await this.request(
|
|
138
140
|
"GET",
|
|
@@ -152,7 +154,9 @@ export class GitHubDevDeliveryStore {
|
|
|
152
154
|
`/repos/${this.repository.fullName}/git/blobs/${entry.sha}`,
|
|
153
155
|
);
|
|
154
156
|
const bytes = decodeBlob(blob);
|
|
155
|
-
const queue = validateStoredState(JSON.parse(bytes)
|
|
157
|
+
const queue = validateStoredState(JSON.parse(bytes), {
|
|
158
|
+
allowLegacyV3Readback,
|
|
159
|
+
});
|
|
156
160
|
return { commitSha, queue, bytes };
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -295,7 +295,10 @@ async function observeQueue(loaded, options) {
|
|
|
295
295
|
const readMode = text(options.readMode || "v3").toLowerCase();
|
|
296
296
|
if (!["v3", "v4"].includes(readMode))
|
|
297
297
|
throw new Error("readMode must be v3 or v4");
|
|
298
|
-
let observation = observeDevDeliveryQueue(loaded.queue, {
|
|
298
|
+
let observation = observeDevDeliveryQueue(loaded.queue, {
|
|
299
|
+
now: options.now,
|
|
300
|
+
allowLegacyV3Readback: true,
|
|
301
|
+
});
|
|
299
302
|
let readCandidate;
|
|
300
303
|
if (readMode === "v4") {
|
|
301
304
|
const qualification =
|
|
@@ -374,6 +377,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
|
|
|
374
377
|
stateRef: options.stateRef,
|
|
375
378
|
protectedBase: options.branch,
|
|
376
379
|
now: options.now,
|
|
380
|
+
allowLegacyV3Readback: options.command === "observe",
|
|
377
381
|
});
|
|
378
382
|
if (
|
|
379
383
|
options.expectedOldStateRoot &&
|
|
@@ -143,6 +143,7 @@ export async function readCurrentDeliveryQueueState(client, repository, targetBr
|
|
|
143
143
|
const { queue } = await store.read({
|
|
144
144
|
stateRef: defaultDevDeliveryStateRef(targetBranch),
|
|
145
145
|
protectedBase: targetBranch,
|
|
146
|
+
allowLegacyV3Readback: true,
|
|
146
147
|
});
|
|
147
148
|
return queue;
|
|
148
149
|
}
|
|
@@ -501,16 +501,16 @@ export function createGitHubDevQualificationClient({
|
|
|
501
501
|
sourceBranch,
|
|
502
502
|
{ activeOnly = false } = {},
|
|
503
503
|
) {
|
|
504
|
-
const query = sourceBranch
|
|
505
|
-
|
|
506
|
-
: ""
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
504
|
+
const query = sourceBranch ? `branch=${encodeRef(sourceBranch)}&` : "";
|
|
505
|
+
const runs = await Promise.all(
|
|
506
|
+
[...(activeOnly ? ACTIVE_STATUSES : [""])].map((status) =>
|
|
507
|
+
paged(
|
|
508
|
+
`/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflow)}/runs?${query}${status ? `status=${status}` : ""}`,
|
|
509
|
+
"workflow_runs",
|
|
510
|
+
),
|
|
511
|
+
),
|
|
510
512
|
);
|
|
511
|
-
return
|
|
512
|
-
? runs.filter((run) => ACTIVE_STATUSES.has(text(run.status)))
|
|
513
|
-
: runs;
|
|
513
|
+
return runs.flat();
|
|
514
514
|
},
|
|
515
515
|
async listRunJobs(runId, runAttempt) {
|
|
516
516
|
return paged(
|
|
@@ -159,8 +159,8 @@ function validateWorkflowRoute(name, route, expectedLogicalRef) {
|
|
|
159
159
|
if (!/^\.github\/workflows\/[.a-z0-9-]+\.ya?ml$/.test(route.workflowPath || "")) {
|
|
160
160
|
throw new Error(`promotion shell ${name} workflowPath must name a reusable workflow`);
|
|
161
161
|
}
|
|
162
|
-
if (!/^(?:v[0-9]+(?:-alpha)?|train\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
|
|
163
|
-
throw new Error(`promotion shell ${name} callRef must be an official channel ref, trusted train, or exact SHA`);
|
|
162
|
+
if (!/^(?:v[0-9]+(?:-alpha)?|(?:train|alpha|release)\/[A-Za-z0-9._+\/-]+|[0-9a-f]{40})$/.test(route.callRef || "")) {
|
|
163
|
+
throw new Error(`promotion shell ${name} callRef must be an official tag, protected channel ref, trusted train, or exact SHA`);
|
|
164
164
|
}
|
|
165
165
|
if (typeof route.forwardInternalInputs !== "boolean") {
|
|
166
166
|
throw new Error(`promotion shell ${name} forwardInternalInputs must be boolean`);
|
|
@@ -66,8 +66,8 @@ export function resolvePromotionChannel({
|
|
|
66
66
|
}
|
|
67
67
|
const shellRef = intent.shellChannel === "alpha" ? `v${selected.major}-alpha` : `v${selected.major}`;
|
|
68
68
|
return {
|
|
69
|
-
targetRef: intent.targetRef,
|
|
70
|
-
|
|
69
|
+
targetRef: intent.targetRef, publicationChannel: intent.publicationChannel,
|
|
70
|
+
routerRef: normalized(routerRef), routerSha: normalized(routerSha).toLowerCase(),
|
|
71
71
|
channel: intent.shellChannel,
|
|
72
72
|
major: selected.major,
|
|
73
73
|
shellRef,
|
|
@@ -99,8 +99,8 @@ function readPackageVersion(cwd) {
|
|
|
99
99
|
|
|
100
100
|
function writeOutputs(file, result) {
|
|
101
101
|
const outputs = {
|
|
102
|
-
"target-ref": result.targetRef,
|
|
103
|
-
"
|
|
102
|
+
"target-ref": result.targetRef, "publication-channel": result.publicationChannel,
|
|
103
|
+
"router-ref": result.routerRef, "router-sha": result.routerSha,
|
|
104
104
|
channel: result.channel,
|
|
105
105
|
major: String(result.major),
|
|
106
106
|
"shell-ref": result.shellRef,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId = "" } = {}) {
|
|
4
|
+
if (Number(passport?.pullRequest?.number || 0)) return undefined;
|
|
5
|
+
if (passport?.target?.channel !== "anchor" || passport?.target?.ref !== "publish-gate/anchor") {
|
|
6
|
+
throw new Error("Release Candidate Passport has no PR identity");
|
|
7
|
+
}
|
|
8
|
+
let releaseManifest;
|
|
9
|
+
try {
|
|
10
|
+
releaseManifest = JSON.parse(buildSummary?.publishSource?.releaseManifest || "");
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error("anchored candidate release manifest is invalid");
|
|
13
|
+
}
|
|
14
|
+
const request = releaseManifest?.anchorRequest;
|
|
15
|
+
if (
|
|
16
|
+
releaseManifest?.sourceRef !== "publish-gate/anchor" ||
|
|
17
|
+
releaseManifest?.sourceSha !== passport.source?.headSha ||
|
|
18
|
+
request?.schema !== 1 ||
|
|
19
|
+
request?.contract !== "kungfu-buildchain-explicit-publish-anchor-request/v1" ||
|
|
20
|
+
!request.assignmentId ||
|
|
21
|
+
!/^native:[0-9a-f-]+$/u.test(String(request.attemptId || "")) ||
|
|
22
|
+
request.transactionId !== transactionId ||
|
|
23
|
+
request.source?.sha !== passport.source?.headSha ||
|
|
24
|
+
request.source?.tree !== passport.source?.treeHash ||
|
|
25
|
+
!/^\d+$/u.test(String(request.supersededCandidate?.workflowRunId || "")) ||
|
|
26
|
+
!/^[0-9a-f]{40}$/u.test(String(request.supersededCandidate?.sha || "")) ||
|
|
27
|
+
!/^sha256:[0-9a-f]{64}$/u.test(String(request.supersededCandidate?.root || "")) ||
|
|
28
|
+
request.runtime?.sha !== passport.buildchain?.sha
|
|
29
|
+
) {
|
|
30
|
+
throw new Error("anchored candidate request does not bind the sealed recovery identity");
|
|
31
|
+
}
|
|
32
|
+
return request;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRun(run, repository) {
|
|
36
|
+
return {
|
|
37
|
+
id: String(run.id), repository,
|
|
38
|
+
headRepository: run.head_repository?.full_name || "",
|
|
39
|
+
status: run.status, conclusion: run.conclusion,
|
|
40
|
+
event: run.event, path: run.path, name: run.name,
|
|
41
|
+
headSha: run.head_sha || "", headBranch: run.head_branch || "",
|
|
42
|
+
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCandidateRun(run, repository) {
|
|
47
|
+
return normalizeRun(run, repository);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeAnchorProvenance(anchorProvenance, repository) {
|
|
51
|
+
if (!anchorProvenance) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
request: anchorProvenance.request,
|
|
54
|
+
run: normalizeRun(anchorProvenance.run, repository),
|
|
55
|
+
workflow: {
|
|
56
|
+
path: anchorProvenance.workflow.path,
|
|
57
|
+
name: anchorProvenance.workflow.name,
|
|
58
|
+
state: anchorProvenance.workflow.state,
|
|
59
|
+
},
|
|
60
|
+
passport: anchorProvenance.passport,
|
|
61
|
+
buildSummary: anchorProvenance.buildSummary,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function recoverCandidateProvenance({
|
|
66
|
+
passport, buildSummary, transactionId, repoInfo, artifactName,
|
|
67
|
+
expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
|
|
68
|
+
fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
|
|
69
|
+
downloadArtifact, readOnlyJson,
|
|
70
|
+
}) {
|
|
71
|
+
const request = resolveAnchorRecoveryRequest({ passport, buildSummary, transactionId });
|
|
72
|
+
if (!request) {
|
|
73
|
+
return { anchorProvenance: undefined, provenancePassport: passport };
|
|
74
|
+
}
|
|
75
|
+
const runId = String(request.supersededCandidate.workflowRunId);
|
|
76
|
+
const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
|
|
77
|
+
const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
|
|
78
|
+
const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
|
|
79
|
+
const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
|
|
80
|
+
if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("superseded candidate run has more than 100 artifacts; complete pagination is required before recovery");
|
|
81
|
+
const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
|
|
82
|
+
const provenanceRoot = path.join(bundleRoot, "anchored-provenance");
|
|
83
|
+
const passportDownload = await downloadArtifact({ artifact: selected.passport, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
84
|
+
const summaryDownload = await downloadArtifact({ artifact: selected.summary, repoInfo, apiUrl, token, archiveDir, bundleRoot: provenanceRoot, fetchImpl });
|
|
85
|
+
const recoveredPassport = readOnlyJson(passportDownload.files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "superseded release-candidate-passport.json");
|
|
86
|
+
const recoveredBuildSummary = readOnlyJson(summaryDownload.files.filter((file) => path.basename(file.path) === "build-summary.json"), "superseded build-summary.json");
|
|
87
|
+
if (
|
|
88
|
+
String(run.id) !== runId ||
|
|
89
|
+
recoveredPassport.workflow?.runId !== runId ||
|
|
90
|
+
recoveredPassport.workflow?.name !== expectedWorkflowName ||
|
|
91
|
+
String(workflow.path || run.path || "").split("@")[0].replace(/^\.github\/workflows\//u, "") !== expectedWorkflowFile.replace(/^\.github\/workflows\//u, "") ||
|
|
92
|
+
recoveredPassport.target?.channel !== channel ||
|
|
93
|
+
recoveredPassport.source?.headSha !== request.supersededCandidate.sha ||
|
|
94
|
+
recoveredPassport.source?.treeHash !== request.source.tree ||
|
|
95
|
+
`sha256:${recoveredPassport.candidateHash}` !== request.supersededCandidate.root
|
|
96
|
+
) throw new Error("superseded candidate provenance does not match the anchored request");
|
|
97
|
+
const anchorProvenance = { request, run, workflow, passport: recoveredPassport, buildSummary: recoveredBuildSummary };
|
|
98
|
+
return { anchorProvenance, provenancePassport: recoveredPassport };
|
|
99
|
+
}
|
|
@@ -7,6 +7,8 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
|
|
9
9
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
10
|
+
import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
|
|
11
|
+
export { resolveAnchorRecoveryRequest } from "./release-candidate-anchor-provenance.mjs";
|
|
10
12
|
import {
|
|
11
13
|
generatePublishRequiredArtifacts,
|
|
12
14
|
githubDownload,
|
|
@@ -878,7 +880,13 @@ export async function resumeFromCandidateRun({
|
|
|
878
880
|
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
879
881
|
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
880
882
|
});
|
|
881
|
-
const
|
|
883
|
+
const { anchorProvenance, provenancePassport } = await recoverCandidateProvenance({
|
|
884
|
+
passport, buildSummary, transactionId, repoInfo, artifactName,
|
|
885
|
+
expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
|
|
886
|
+
fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
|
|
887
|
+
downloadArtifact, readOnlyJson,
|
|
888
|
+
});
|
|
889
|
+
const prNumber = Number(provenancePassport.pullRequest?.number || 0);
|
|
882
890
|
if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
|
|
883
891
|
const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
|
|
884
892
|
const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
|
|
@@ -944,20 +952,9 @@ export async function resumeFromCandidateRun({
|
|
|
944
952
|
expectedRuntimeSha: candidateRuntimeSha,
|
|
945
953
|
expectedTransactionId: transactionId,
|
|
946
954
|
existingTransaction,
|
|
947
|
-
run:
|
|
948
|
-
id: String(run.id),
|
|
949
|
-
repository: repoInfo.fullName,
|
|
950
|
-
headRepository: run.head_repository?.full_name || "",
|
|
951
|
-
status: run.status,
|
|
952
|
-
conclusion: run.conclusion,
|
|
953
|
-
event: run.event,
|
|
954
|
-
path: run.path,
|
|
955
|
-
name: run.name,
|
|
956
|
-
headSha: run.head_sha || "",
|
|
957
|
-
headBranch: run.head_branch || "",
|
|
958
|
-
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
959
|
-
},
|
|
955
|
+
run: normalizeCandidateRun(run, repoInfo.fullName),
|
|
960
956
|
workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
|
|
957
|
+
anchorProvenance: normalizeAnchorProvenance(anchorProvenance, repoInfo.fullName),
|
|
961
958
|
pullRequest: {
|
|
962
959
|
number: pullRequest.number,
|
|
963
960
|
merged: pullRequest.merged === true,
|
|
@@ -195,6 +195,7 @@ export function nodeApiMeta(exportName) {
|
|
|
195
195
|
"./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
|
|
196
196
|
"./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
|
|
197
197
|
"./candidate-timeline": { group: "observability-diagnostics", summary: "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs." },
|
|
198
|
+
"./ci-lane-change-budget": { group: "governance-versioning", summary: "Exact-baseline CI lane admission requiring authority, trigger, concurrency, runner, cancellation, and merge-critical SLO budgets." },
|
|
198
199
|
"./channel-candidate": { group: "governance-versioning", summary: "Exact-source channel candidate decisions, same-SHA workflow evidence validation, and deterministic source-lock reference APIs." },
|
|
199
200
|
"./dev-delivery-authority": { group: "governance-versioning", summary: "Opt-in bounded Qualification Lease, exclusive Landing Warrant, provider-attempt admission and heartbeat readback, terminal sealing, and expired-Landing verification APIs." },
|
|
200
201
|
"./dev-delivery-warrant": { group: "governance-versioning", summary: "Durable fair Dev Delivery Warrant queue, fenced lease, and split source and integration proof APIs." },
|
|
@@ -41,6 +41,9 @@ export function scanCommand(options = {}) {
|
|
|
41
41
|
const root = path.resolve(
|
|
42
42
|
options.root || env("BUILDCHAIN_CONSUMER_ROOT", process.cwd()),
|
|
43
43
|
);
|
|
44
|
+
const invocationRoot = path.resolve(
|
|
45
|
+
options.invocationRoot || env("BUILDCHAIN_INVOCATION_SOURCE_ROOT", root),
|
|
46
|
+
);
|
|
44
47
|
const output = path.resolve(
|
|
45
48
|
root,
|
|
46
49
|
options.output ||
|
|
@@ -54,6 +57,7 @@ export function scanCommand(options = {}) {
|
|
|
54
57
|
);
|
|
55
58
|
const result = scanV4FloatingConsumerPolicy({
|
|
56
59
|
root,
|
|
60
|
+
invocationRoot,
|
|
57
61
|
repository: options.repository || env("GITHUB_REPOSITORY"),
|
|
58
62
|
sourceSha: options.sourceSha || env("GITHUB_SHA"),
|
|
59
63
|
invokedWorkflow:
|