@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.8
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/actions/release-tail/README.md +8 -0
- package/architecture/ci-lane-change-budget.json +187 -0
- package/architecture/internal-capabilities.json +65 -5
- package/architecture/maintainability-policy.json +27 -21
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-floating-consumer-policy.json +6 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +14 -14
- package/dist/site/buildchain-site.json +231 -24
- package/dist/site/capability-registry.json +11 -10
- package/dist/site/cli-registry.json +53 -1
- package/dist/site/kfd-claims.json +279 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -5
- package/dist/site/node-api-registry.json +11569 -9299
- package/dist/site/page-registry.json +214 -15
- package/dist/site/public-surface-audit.json +206 -11
- package/dist/site/publication-authority-registry.json +39 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +15 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +110 -8
- package/docs/MAP.md +3 -1
- package/docs/cli-reference.md +68 -0
- package/docs/cli.md +8 -0
- package/docs/dev-delivery-warrant.md +22 -1
- package/docs/node-api-reference.md +332 -172
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +18 -3
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- 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/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-candidate-recovery.js +66 -11
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- 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/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/buildchain-cli-help.mjs +10 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +3 -3
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- 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/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +30 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +556 -29
- package/scripts/site-capability-metadata.mjs +17 -0
- package/scripts/v4-consumer-policy.mjs +4 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -67,8 +67,7 @@ function directEntries(lines, parentLine) {
|
|
|
67
67
|
const trimmed = line.trim();
|
|
68
68
|
const indent = indentation(line);
|
|
69
69
|
if (trimmed && indent <= parentIndent) break;
|
|
70
|
-
if (!trimmed || trimmed.startsWith("#") || indent !== childIndent)
|
|
71
|
-
continue;
|
|
70
|
+
if (!trimmed || trimmed.startsWith("#") || indent !== childIndent) continue;
|
|
72
71
|
const match = line.match(/^\s*([A-Za-z0-9_.-]+):(?:\s*(.*))?$/);
|
|
73
72
|
if (match) {
|
|
74
73
|
entries.push({
|
|
@@ -263,6 +262,28 @@ export function parseWorkflowCallJobs(text) {
|
|
|
263
262
|
.filter((job) => job.uses);
|
|
264
263
|
}
|
|
265
264
|
|
|
265
|
+
export function parseWorkflowJobs(text) {
|
|
266
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
267
|
+
const jobs = topLevelEntry(lines, "jobs");
|
|
268
|
+
if (!jobs) return [];
|
|
269
|
+
return directEntries(lines, jobs.line)
|
|
270
|
+
.map((job) => {
|
|
271
|
+
const properties = directEntries(lines, job.line);
|
|
272
|
+
const property = (name) =>
|
|
273
|
+
properties.find((entry) => entry.key === name) || null;
|
|
274
|
+
return {
|
|
275
|
+
id: job.key,
|
|
276
|
+
name: unquote(property("name")?.value || job.key),
|
|
277
|
+
uses: unquote(property("uses")?.value || ""),
|
|
278
|
+
runsOn: unquote(property("runs-on")?.value || ""),
|
|
279
|
+
timeoutMinutes: property("timeout-minutes")
|
|
280
|
+
? scalar(property("timeout-minutes").value)
|
|
281
|
+
: null,
|
|
282
|
+
};
|
|
283
|
+
})
|
|
284
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
285
|
+
}
|
|
286
|
+
|
|
266
287
|
function blockScalarIndent(lines, lineIndex, parentIndent) {
|
|
267
288
|
for (let index = lineIndex + 1; index < lines.length; index += 1) {
|
|
268
289
|
if (!lines[index].trim()) continue;
|
|
@@ -315,6 +336,7 @@ export function parseWorkflowDocument(text) {
|
|
|
315
336
|
return {
|
|
316
337
|
triggers: parseTriggers(String(text || "").split(/\r?\n/)),
|
|
317
338
|
interface: parseReusableWorkflowInterface(text),
|
|
339
|
+
jobs: parseWorkflowJobs(text),
|
|
318
340
|
callJobs: parseWorkflowCallJobs(text),
|
|
319
341
|
};
|
|
320
342
|
}
|
|
@@ -8,6 +8,11 @@ export const BUILDCHAIN_USAGE = `Usage:
|
|
|
8
8
|
buildchain architecture qualify --authority-revision <git-revision>
|
|
9
9
|
[--candidate-revision <git-revision>]
|
|
10
10
|
[--cwd <dir>] [--json]
|
|
11
|
+
buildchain adopter-delivery run --input <json-or-path> [--output <path>]
|
|
12
|
+
buildchain adopter-delivery verify --input <json-or-path>
|
|
13
|
+
--readback <json-or-path> [--output <path>]
|
|
14
|
+
buildchain adopter-delivery bootstrap --input <json-or-path> [--output <path>]
|
|
15
|
+
buildchain adopter-delivery archive --input <json-or-path> [--output <path>]
|
|
11
16
|
buildchain portable-cache plan --manifest <file-or-json> [--output <file>]
|
|
12
17
|
[--github-output <file>] [--json]
|
|
13
18
|
buildchain portable-cache receipt --plan <file-or-json> [--matched-key <key>]
|
|
@@ -44,6 +49,11 @@ export const BUILDCHAIN_USAGE = `Usage:
|
|
|
44
49
|
buildchain release-tail init --declaration <json-or-path> [--state <path>]
|
|
45
50
|
buildchain release-tail <status|verify> [--state <path>] [--output <path>]
|
|
46
51
|
buildchain release-tail compat --hooks-json <json-or-path> [--output <path>]
|
|
52
|
+
buildchain release-tail rehearse --capsule <absolute-path>
|
|
53
|
+
--candidate-root <absolute-path>
|
|
54
|
+
--mode <simulate|replay>
|
|
55
|
+
--state <absolute-path>
|
|
56
|
+
--evidence <absolute-path>
|
|
47
57
|
buildchain tail-reseal plan --request <request.json> [--output <plan.json>]
|
|
48
58
|
buildchain tail-reseal admit --request <request.json> [--output <admission.json>]
|
|
49
59
|
buildchain tail-reseal verify-platform --request <request.json> --platform <id>
|
|
@@ -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,16 +1021,16 @@ for (const forbiddenSnippet of [
|
|
|
1021
1021
|
for (const requiredSnippet of [
|
|
1022
1022
|
"id-token: write",
|
|
1023
1023
|
"actions: write",
|
|
1024
|
-
"uses:
|
|
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')",
|
|
1028
1028
|
"resume-candidate-run-id:",
|
|
1029
1029
|
"resume-expected-source-tree:",
|
|
1030
1030
|
"resume-buildchain-runtime-sha:",
|
|
1031
|
-
"github-
|
|
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",
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
6
|
+
|
|
7
|
+
function exactSha(value, label) {
|
|
8
|
+
const normalized = String(value || "")
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
if (!SHA.test(normalized))
|
|
12
|
+
throw new Error(`${label} must be an exact 40-hex Git SHA`);
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function git(args, { cwd, encoding = "utf8" }) {
|
|
17
|
+
const result = spawnSync("git", args, {
|
|
18
|
+
cwd,
|
|
19
|
+
encoding,
|
|
20
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
21
|
+
});
|
|
22
|
+
if (result.status !== 0)
|
|
23
|
+
throw new Error(
|
|
24
|
+
`git ${args.join(" ")} failed: ${String(result.stderr || "").trim()}`,
|
|
25
|
+
);
|
|
26
|
+
return encoding === null ? result.stdout : String(result.stdout || "").trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function patchRoot(base, head, cwd) {
|
|
30
|
+
const patch = git(
|
|
31
|
+
[
|
|
32
|
+
"diff",
|
|
33
|
+
"--binary",
|
|
34
|
+
"--full-index",
|
|
35
|
+
"--no-ext-diff",
|
|
36
|
+
`${base}..${head}`,
|
|
37
|
+
],
|
|
38
|
+
{ cwd, encoding: null },
|
|
39
|
+
);
|
|
40
|
+
return `sha256:${crypto.createHash("sha256").update(patch).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function linearRange(base, head, cwd) {
|
|
44
|
+
try {
|
|
45
|
+
git(["merge-base", "--is-ancestor", base, head], { cwd });
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const commits = git(
|
|
50
|
+
["rev-list", "--reverse", "--topo-order", `${base}..${head}`],
|
|
51
|
+
{ cwd },
|
|
52
|
+
)
|
|
53
|
+
.split(/\s+/u)
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
if (commits.length === 0) return null;
|
|
56
|
+
let expectedParent = base;
|
|
57
|
+
const trees = [];
|
|
58
|
+
const patchRoots = [];
|
|
59
|
+
for (const rangeCommit of commits) {
|
|
60
|
+
const [observedCommit, ...parents] = git(
|
|
61
|
+
["rev-list", "--parents", "-n", "1", rangeCommit],
|
|
62
|
+
{ cwd },
|
|
63
|
+
).split(/\s+/u);
|
|
64
|
+
if (
|
|
65
|
+
observedCommit !== rangeCommit ||
|
|
66
|
+
parents.length !== 1 ||
|
|
67
|
+
parents[0] !== expectedParent
|
|
68
|
+
) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
trees.push(
|
|
72
|
+
exactSha(
|
|
73
|
+
git(["rev-parse", `${rangeCommit}^{tree}`], { cwd }),
|
|
74
|
+
"replayedCommitTree",
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
patchRoots.push(patchRoot(expectedParent, rangeCommit, cwd));
|
|
78
|
+
expectedParent = rangeCommit;
|
|
79
|
+
}
|
|
80
|
+
return { commits, trees, patchRoots };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function exactMergeGroupBinding(input = {}) {
|
|
84
|
+
const cwd = path.resolve(input.cwd || process.cwd());
|
|
85
|
+
const mergeGroupHead = exactSha(input.mergeGroupHead, "mergeGroupHead");
|
|
86
|
+
const expectedTree = exactSha(input.mergeGroupTree, "mergeGroupTree");
|
|
87
|
+
const qualifiedBase = exactSha(input.qualifiedBase, "qualifiedBase");
|
|
88
|
+
const currentBase = exactSha(input.currentBase, "currentBase");
|
|
89
|
+
const sourceHead = exactSha(input.sourceHead, "sourceHead");
|
|
90
|
+
const sourcePatchRoot = String(input.sourcePatchRoot || "").trim();
|
|
91
|
+
const actualTree = exactSha(
|
|
92
|
+
git(["rev-parse", `${mergeGroupHead}^{tree}`], { cwd }),
|
|
93
|
+
"mergeGroupTree",
|
|
94
|
+
);
|
|
95
|
+
if (actualTree !== expectedTree)
|
|
96
|
+
return { ok: false, reason: "merge-group-tree-mismatch" };
|
|
97
|
+
try {
|
|
98
|
+
git(["merge-base", "--is-ancestor", qualifiedBase, currentBase], { cwd });
|
|
99
|
+
} catch {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "current-base-not-descendant-of-qualified-base",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(sourcePatchRoot))
|
|
106
|
+
throw new Error("sourcePatchRoot must be an exact SHA-256 root");
|
|
107
|
+
if (patchRoot(currentBase, mergeGroupHead, cwd) !== sourcePatchRoot)
|
|
108
|
+
return { ok: false, reason: "merge-group-source-patch-mismatch" };
|
|
109
|
+
|
|
110
|
+
const [commit, ...parents] = git(
|
|
111
|
+
["rev-list", "--parents", "-n", "1", mergeGroupHead],
|
|
112
|
+
{ cwd },
|
|
113
|
+
).split(/\s+/u);
|
|
114
|
+
if (commit !== mergeGroupHead)
|
|
115
|
+
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
116
|
+
if (
|
|
117
|
+
parents.length === 2 &&
|
|
118
|
+
parents[0] === currentBase &&
|
|
119
|
+
parents[1] === sourceHead
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
mergeGroupHead,
|
|
124
|
+
mergeGroupTree: actualTree,
|
|
125
|
+
parents,
|
|
126
|
+
compositionMode: "two-parent-merge",
|
|
127
|
+
replayedCommitTrees: [],
|
|
128
|
+
replayedCommitPatchRoots: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sourceRange = linearRange(qualifiedBase, sourceHead, cwd);
|
|
133
|
+
const mergeGroupRange = linearRange(currentBase, mergeGroupHead, cwd);
|
|
134
|
+
if (
|
|
135
|
+
!sourceRange ||
|
|
136
|
+
!mergeGroupRange ||
|
|
137
|
+
sourceRange.commits.length !== mergeGroupRange.commits.length ||
|
|
138
|
+
sourceRange.patchRoots.some(
|
|
139
|
+
(root, index) => root !== mergeGroupRange.patchRoots[index],
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
mergeGroupHead,
|
|
147
|
+
mergeGroupTree: actualTree,
|
|
148
|
+
parents,
|
|
149
|
+
compositionMode: "linear-replay",
|
|
150
|
+
replayedCommitTrees: mergeGroupRange.trees,
|
|
151
|
+
replayedCommitPatchRoots: mergeGroupRange.patchRoots,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
verifySourceQualificationProof,
|
|
12
12
|
} from "../packages/core/dev-delivery-warrant.js";
|
|
13
13
|
import { validateControllerReceipt } from "../packages/core/controller-evidence.js";
|
|
14
|
+
import { exactMergeGroupBinding } from "./dev-delivery-source-proof-replay.mjs";
|
|
14
15
|
|
|
15
16
|
const REUSE_DECISION_SCHEMA =
|
|
16
17
|
"kungfu.buildchain.source-qualification-reuse-decision/v1";
|
|
@@ -113,98 +114,6 @@ function gitPathRoot(ref, paths, label, cwd) {
|
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
function exactMergeGroupBinding(input = {}) {
|
|
117
|
-
const cwd = path.resolve(input.cwd || process.cwd());
|
|
118
|
-
const mergeGroupHead = exactSha(input.mergeGroupHead, "mergeGroupHead");
|
|
119
|
-
const expectedTree = exactSha(input.mergeGroupTree, "mergeGroupTree");
|
|
120
|
-
const currentBase = exactSha(input.currentBase, "currentBase");
|
|
121
|
-
const sourceHead = exactSha(input.sourceHead, "sourceHead");
|
|
122
|
-
const actualTree = exactSha(
|
|
123
|
-
git(["rev-parse", `${mergeGroupHead}^{tree}`], { cwd }),
|
|
124
|
-
"mergeGroupTree",
|
|
125
|
-
);
|
|
126
|
-
if (actualTree !== expectedTree)
|
|
127
|
-
return { ok: false, reason: "merge-group-tree-mismatch" };
|
|
128
|
-
const [commit, ...parents] = git(
|
|
129
|
-
["rev-list", "--parents", "-n", "1", mergeGroupHead],
|
|
130
|
-
{ cwd },
|
|
131
|
-
).split(/\s+/u);
|
|
132
|
-
if (commit !== mergeGroupHead) {
|
|
133
|
-
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
134
|
-
}
|
|
135
|
-
if (
|
|
136
|
-
parents.length === 2 &&
|
|
137
|
-
parents[0] === currentBase &&
|
|
138
|
-
parents[1] === sourceHead
|
|
139
|
-
) {
|
|
140
|
-
return {
|
|
141
|
-
ok: true,
|
|
142
|
-
mergeGroupHead,
|
|
143
|
-
mergeGroupTree: actualTree,
|
|
144
|
-
parents,
|
|
145
|
-
compositionMode: "two-parent-merge",
|
|
146
|
-
replayedCommitTrees: [],
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const linearRange = (head) => {
|
|
151
|
-
try {
|
|
152
|
-
git(["merge-base", "--is-ancestor", currentBase, head], { cwd });
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
const commits = git(
|
|
157
|
-
["rev-list", "--reverse", "--topo-order", `${currentBase}..${head}`],
|
|
158
|
-
{ cwd },
|
|
159
|
-
)
|
|
160
|
-
.split(/\s+/u)
|
|
161
|
-
.filter(Boolean);
|
|
162
|
-
if (commits.length === 0) return null;
|
|
163
|
-
let expectedParent = currentBase;
|
|
164
|
-
const trees = [];
|
|
165
|
-
for (const rangeCommit of commits) {
|
|
166
|
-
const [observedCommit, ...rangeParents] = git(
|
|
167
|
-
["rev-list", "--parents", "-n", "1", rangeCommit],
|
|
168
|
-
{ cwd },
|
|
169
|
-
).split(/\s+/u);
|
|
170
|
-
if (
|
|
171
|
-
observedCommit !== rangeCommit ||
|
|
172
|
-
rangeParents.length !== 1 ||
|
|
173
|
-
rangeParents[0] !== expectedParent
|
|
174
|
-
) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
trees.push(
|
|
178
|
-
exactSha(
|
|
179
|
-
git(["rev-parse", `${rangeCommit}^{tree}`], { cwd }),
|
|
180
|
-
"replayedCommitTree",
|
|
181
|
-
),
|
|
182
|
-
);
|
|
183
|
-
expectedParent = rangeCommit;
|
|
184
|
-
}
|
|
185
|
-
return trees;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const sourceTrees = linearRange(sourceHead);
|
|
189
|
-
const mergeGroupTrees = linearRange(mergeGroupHead);
|
|
190
|
-
if (
|
|
191
|
-
!sourceTrees ||
|
|
192
|
-
!mergeGroupTrees ||
|
|
193
|
-
sourceTrees.length !== mergeGroupTrees.length ||
|
|
194
|
-
sourceTrees.some((tree, index) => tree !== mergeGroupTrees[index])
|
|
195
|
-
) {
|
|
196
|
-
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
ok: true,
|
|
200
|
-
mergeGroupHead,
|
|
201
|
-
mergeGroupTree: actualTree,
|
|
202
|
-
parents,
|
|
203
|
-
compositionMode: "linear-replay",
|
|
204
|
-
replayedCommitTrees: mergeGroupTrees,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
117
|
export function sourceQualificationPredicates(input = {}) {
|
|
209
118
|
const cwd = path.resolve(input.cwd || process.cwd());
|
|
210
119
|
const repository = required(input.repository, "repository");
|
|
@@ -397,7 +306,8 @@ export function materializeReuseLifecycleEvidence({
|
|
|
397
306
|
decisionRoot: decision.decisionRoot,
|
|
398
307
|
sourceWorkflowRunId: decision.sourceWorkflowRunId,
|
|
399
308
|
sourceHead: decision.sourceHead,
|
|
400
|
-
qualifiedBase: decision.
|
|
309
|
+
qualifiedBase: decision.qualifiedBase,
|
|
310
|
+
currentBase: decision.currentBase,
|
|
401
311
|
mergeGroupHead: decision.mergeGroupHead,
|
|
402
312
|
mergeGroupTree: decision.mergeGroupTree,
|
|
403
313
|
},
|
|
@@ -446,7 +356,7 @@ export function verifySourceQualificationReuse(input = {}) {
|
|
|
446
356
|
const verification = verifySourceQualificationProof(proof, {
|
|
447
357
|
repository: required(input.repository, "repository"),
|
|
448
358
|
protectedBase: required(input.protectedBase, "protectedBase"),
|
|
449
|
-
qualifiedBase: exactSha(
|
|
359
|
+
qualifiedBase: exactSha(proof.qualifiedBase, "qualifiedBase"),
|
|
450
360
|
sourceHead: exactSha(input.sourceHead, "sourceHead"),
|
|
451
361
|
sourceWorkflowRunId: Number(input.sourceWorkflowRunId),
|
|
452
362
|
controllerReceiptRoot: receipt.digest,
|
|
@@ -462,7 +372,7 @@ export function verifySourceQualificationReuse(input = {}) {
|
|
|
462
372
|
}
|
|
463
373
|
const current = sourceQualificationPredicates({
|
|
464
374
|
...input,
|
|
465
|
-
qualifiedBase:
|
|
375
|
+
qualifiedBase: proof.qualifiedBase,
|
|
466
376
|
});
|
|
467
377
|
const classification = classifyDevDeliveryDelta({
|
|
468
378
|
proof,
|
|
@@ -476,7 +386,11 @@ export function verifySourceQualificationReuse(input = {}) {
|
|
|
476
386
|
reason: classification.reason,
|
|
477
387
|
});
|
|
478
388
|
}
|
|
479
|
-
const mergeGroup = exactMergeGroupBinding(
|
|
389
|
+
const mergeGroup = exactMergeGroupBinding({
|
|
390
|
+
...input,
|
|
391
|
+
qualifiedBase: proof.qualifiedBase,
|
|
392
|
+
sourcePatchRoot: proof.sourcePatchRoot,
|
|
393
|
+
});
|
|
480
394
|
if (!mergeGroup.ok) {
|
|
481
395
|
return rootedDecision({
|
|
482
396
|
schema: REUSE_DECISION_SCHEMA,
|
|
@@ -493,12 +407,14 @@ export function verifySourceQualificationReuse(input = {}) {
|
|
|
493
407
|
sourceProofRoot: proof.proofRoot,
|
|
494
408
|
sourceWorkflowRunId: proof.sourceWorkflowRunId,
|
|
495
409
|
sourceHead: proof.sourceHead,
|
|
496
|
-
|
|
410
|
+
qualifiedBase: proof.qualifiedBase,
|
|
411
|
+
currentBase: exactSha(input.currentBase, "currentBase"),
|
|
497
412
|
mergeGroupHead: mergeGroup.mergeGroupHead,
|
|
498
413
|
mergeGroupTree: mergeGroup.mergeGroupTree,
|
|
499
414
|
mergeGroupParents: mergeGroup.parents,
|
|
500
415
|
mergeGroupCompositionMode: mergeGroup.compositionMode,
|
|
501
416
|
mergeGroupReplayTrees: mergeGroup.replayedCommitTrees,
|
|
417
|
+
mergeGroupReplayPatchRoots: mergeGroup.replayedCommitPatchRoots,
|
|
502
418
|
predicateRoots: {
|
|
503
419
|
sourceIdentityRoot: proof.sourceIdentityRoot,
|
|
504
420
|
sourcePatchRoot: proof.sourcePatchRoot,
|
|
@@ -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(
|