@kungfu-tech/buildchain 3.0.2-alpha.3 → 3.0.2-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/actions/github-artifact-attestation/README.md +10 -0
- package/actions/promote-buildchain-ref/README.md +7 -0
- package/bin/buildchain.mjs +5 -0
- package/bin/internal/trust-release-cli.mjs +74 -3
- package/dist/site/artifact-schemas.json +5 -1
- package/dist/site/buildchain-contract.json +79 -28
- package/dist/site/buildchain-site.json +110 -27
- package/dist/site/capability-registry.json +8 -7
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/controller-registry.json +40 -4
- package/dist/site/kfd-claims.json +203 -18
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +19 -5
- package/dist/site/node-api-registry.json +23 -10
- package/dist/site/page-registry.json +91 -17
- package/dist/site/public-surface-audit.json +125 -15
- package/dist/site/publication-authority-registry.json +27 -2
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-model.json +2 -1
- package/dist/site/release-passport-check-manifest.json +1 -0
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/schemas/release-passport-v1.schema.json +6 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +84 -7
- package/docs/MAP.md +3 -1
- package/docs/binary-distribution.md +7 -0
- package/docs/cli.md +9 -0
- package/docs/dev-alpha-candidate-patrol.md +16 -4
- package/docs/github-artifact-attestation.md +219 -0
- package/docs/release-passport.md +13 -0
- package/docs/reusable-build-surface.md +6 -0
- package/package.json +2 -1
- package/packages/core/buildchain-contract.js +6 -0
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/channel-candidate.js +67 -16
- package/packages/core/github-artifact-attestation.js +642 -0
- package/packages/core/index.js +19 -0
- package/packages/core/publication-authority.js +1 -1
- package/packages/core/release-passport-contract.js +2 -0
- package/packages/core/release-passport.js +51 -0
- package/scripts/check-inventory.mjs +4 -0
- package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +256 -46
- package/scripts/generate-site-bundle.mjs +12 -0
- package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
- package/scripts/release-candidate-resolver.mjs +12 -0
- package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
import { createSurfaceTimestampPolicy } from "./surface-manifest.js";
|
|
22
22
|
import { validatePublishEvidence as validateTransactionPublishEvidence } from "./publish-transaction.js";
|
|
23
23
|
import { normalizeControllerReceiptReferences } from "./controller-evidence.js";
|
|
24
|
+
import {
|
|
25
|
+
normalizeGitHubArtifactAttestationPolicy,
|
|
26
|
+
} from "./github-artifact-attestation.js";
|
|
24
27
|
|
|
25
28
|
export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
|
|
26
29
|
export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
|
|
@@ -1166,6 +1169,7 @@ export function createReleasePassport({
|
|
|
1166
1169
|
kfdAgentHubEvidencePath = "",
|
|
1167
1170
|
controllerReceipts = [],
|
|
1168
1171
|
controllerReceiptReferences = [],
|
|
1172
|
+
githubArtifactAttestations = [],
|
|
1169
1173
|
} = {}) {
|
|
1170
1174
|
const normalizedTag = nonEmptyString(tag, "tag");
|
|
1171
1175
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
|
|
@@ -1231,6 +1235,8 @@ export function createReleasePassport({
|
|
|
1231
1235
|
: [],
|
|
1232
1236
|
requirePassed: true,
|
|
1233
1237
|
});
|
|
1238
|
+
const normalizedGitHubArtifactAttestations = (githubArtifactAttestations || [])
|
|
1239
|
+
.map(normalizeGitHubArtifactAttestationPolicy);
|
|
1234
1240
|
const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
|
|
1235
1241
|
const normalizedPublishSummary = normalizePublishSummary({
|
|
1236
1242
|
packageSet: normalizedPackageSet,
|
|
@@ -1360,6 +1366,9 @@ export function createReleasePassport({
|
|
|
1360
1366
|
...(normalizedKfdAgentHub ? { kfdAgentHub: normalizedKfdAgentHub } : {}),
|
|
1361
1367
|
...(invariantPassports ? { invariantPassports } : {}),
|
|
1362
1368
|
...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
|
|
1369
|
+
...(normalizedGitHubArtifactAttestations.length > 0
|
|
1370
|
+
? { githubArtifactAttestations: normalizedGitHubArtifactAttestations }
|
|
1371
|
+
: {}),
|
|
1363
1372
|
versionImpact: normalizedImpact.versionImpact,
|
|
1364
1373
|
surfaceImpacts: normalizedImpact.surfaceImpacts,
|
|
1365
1374
|
artifacts: [
|
|
@@ -1450,6 +1459,7 @@ export function collectGitHubReleasePassport({
|
|
|
1450
1459
|
invariantPassportCommand = "",
|
|
1451
1460
|
kfdAgentHubEvidenceJson = "",
|
|
1452
1461
|
controllerReceiptReferences = [],
|
|
1462
|
+
githubArtifactAttestationPolicyJsons = [],
|
|
1453
1463
|
basePassportJson = "",
|
|
1454
1464
|
requireBaseKfd = false,
|
|
1455
1465
|
releaseJsonExtra = "",
|
|
@@ -1526,6 +1536,13 @@ export function collectGitHubReleasePassport({
|
|
|
1526
1536
|
undefined,
|
|
1527
1537
|
{ cwd, label: "kfdAgentHubEvidenceJson" },
|
|
1528
1538
|
);
|
|
1539
|
+
const githubArtifactAttestationPolicies = (githubArtifactAttestationPolicyJsons || [])
|
|
1540
|
+
.filter(Boolean)
|
|
1541
|
+
.map((policyJson) => parseJsonInput(policyJson, undefined, {
|
|
1542
|
+
cwd,
|
|
1543
|
+
label: "githubArtifactAttestationPolicyJsons entry",
|
|
1544
|
+
}))
|
|
1545
|
+
.map(normalizeGitHubArtifactAttestationPolicy);
|
|
1529
1546
|
const kfd3ArtifactWitnesses = [
|
|
1530
1547
|
...kfd3ArtifactWitnessMetas.map((meta) => meta.value),
|
|
1531
1548
|
...(kfd3ArtifactCommandMeta.value ? [kfd3ArtifactCommandMeta.value] : []),
|
|
@@ -1623,6 +1640,7 @@ export function collectGitHubReleasePassport({
|
|
|
1623
1640
|
: undefined,
|
|
1624
1641
|
kfdAgentHubEvidencePath: kfdAgentHubEvidenceMeta.value ? "kfd-agent-hub-evidence.json" : "",
|
|
1625
1642
|
controllerReceiptReferences,
|
|
1643
|
+
githubArtifactAttestations: githubArtifactAttestationPolicies,
|
|
1626
1644
|
publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
|
|
1627
1645
|
transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
|
|
1628
1646
|
workflow,
|
|
@@ -1972,6 +1990,39 @@ export function createReleaseCheckReport({
|
|
|
1972
1990
|
issues.push(issue("error", "kfdSupport.section", "KFD support evidence is present without a release-passport projection"));
|
|
1973
1991
|
}
|
|
1974
1992
|
|
|
1993
|
+
for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
|
|
1994
|
+
try {
|
|
1995
|
+
const policy = normalizeGitHubArtifactAttestationPolicy(value);
|
|
1996
|
+
if (policy.caller.sourceSha !== String(passport?.release?.sourceSha || "").toLowerCase()) {
|
|
1997
|
+
issues.push(issue(
|
|
1998
|
+
"error",
|
|
1999
|
+
`githubArtifactAttestations[${index}].caller.sourceSha`,
|
|
2000
|
+
"attestation policy source SHA must match passport.release.sourceSha",
|
|
2001
|
+
));
|
|
2002
|
+
}
|
|
2003
|
+
const artifact = (passport?.artifacts || []).find((entry) => entry.name === policy.subject.name);
|
|
2004
|
+
if (!artifact) {
|
|
2005
|
+
issues.push(issue(
|
|
2006
|
+
"error",
|
|
2007
|
+
`githubArtifactAttestations[${index}].subject.name`,
|
|
2008
|
+
`attestation subject ${policy.subject.name} is absent from the Release Passport artifacts`,
|
|
2009
|
+
));
|
|
2010
|
+
} else if (artifact.sha256 !== policy.subject.digest.sha256) {
|
|
2011
|
+
issues.push(issue(
|
|
2012
|
+
"error",
|
|
2013
|
+
`githubArtifactAttestations[${index}].subject.digest`,
|
|
2014
|
+
`attestation subject ${policy.subject.name} digest differs from the Release Passport artifact`,
|
|
2015
|
+
));
|
|
2016
|
+
}
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
issues.push(issue(
|
|
2019
|
+
"error",
|
|
2020
|
+
`githubArtifactAttestations[${index}]`,
|
|
2021
|
+
error.message,
|
|
2022
|
+
));
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1975
2026
|
const tag = passport?.release?.tag || "";
|
|
1976
2027
|
if (!tag) {
|
|
1977
2028
|
issues.push(issue("error", "release.tag", "release.tag is required"));
|
|
@@ -68,6 +68,7 @@ const requiredPaths = [
|
|
|
68
68
|
"docs/product-mechanism.md",
|
|
69
69
|
"docs/readme-badges.md",
|
|
70
70
|
"docs/release-passport.md",
|
|
71
|
+
"docs/github-artifact-attestation.md",
|
|
71
72
|
"docs/shifu-gate-profiles.md",
|
|
72
73
|
"docs/auditable-demo.md",
|
|
73
74
|
"docs/release-propagation.md",
|
|
@@ -358,6 +359,9 @@ if (rootPackage.exports?.["./kfd-gate"] !== "./packages/core/kfd-gate.js") {
|
|
|
358
359
|
if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-passport.js") {
|
|
359
360
|
throw new Error("root package must export @kungfu-tech/buildchain/release-passport");
|
|
360
361
|
}
|
|
362
|
+
if (rootPackage.exports?.["./github-artifact-attestation"] !== "./packages/core/github-artifact-attestation.js") {
|
|
363
|
+
throw new Error("root package must export @kungfu-tech/buildchain/github-artifact-attestation");
|
|
364
|
+
}
|
|
361
365
|
if (rootPackage.exports?.["./release-passport-contract"] !== "./packages/core/release-passport-contract.js") {
|
|
362
366
|
throw new Error("root package must export @kungfu-tech/buildchain/release-passport-contract");
|
|
363
367
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
GITHUB_ARTIFACT_ATTESTATION_WORKFLOW,
|
|
9
|
+
createGitHubArtifactAttestationPolicy,
|
|
10
|
+
githubArtifactAttestationSha256File,
|
|
11
|
+
} from "../packages/core/github-artifact-attestation.js";
|
|
12
|
+
|
|
13
|
+
function env(name, fallback = "") {
|
|
14
|
+
return String(process.env[name] || fallback).trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function required(name) {
|
|
18
|
+
const value = env(name);
|
|
19
|
+
if (!value) throw new Error(`${name} is required`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cwd = path.resolve(env("BUILDCHAIN_SOURCE_CWD", "."));
|
|
24
|
+
const subjectRelativePath = required("BUILDCHAIN_GITHUB_ATTESTATION_SUBJECT_PATH").replace(/\\/g, "/");
|
|
25
|
+
const subjectPath = path.resolve(cwd, subjectRelativePath);
|
|
26
|
+
const manifestPath = path.resolve(required("BUILDCHAIN_GITHUB_ATTESTATION_PLATFORM_MANIFEST"));
|
|
27
|
+
const outputPath = path.resolve(required("BUILDCHAIN_GITHUB_ATTESTATION_POLICY_OUTPUT"));
|
|
28
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
29
|
+
const manifestEntry = (manifest.files || []).find((entry) => (
|
|
30
|
+
String(entry.path || "").replace(/\\/g, "/") === subjectRelativePath
|
|
31
|
+
));
|
|
32
|
+
if (!manifestEntry) throw new Error(`platform manifest does not contain ${subjectRelativePath}`);
|
|
33
|
+
const digest = githubArtifactAttestationSha256File(subjectPath);
|
|
34
|
+
const size = fs.statSync(subjectPath).size;
|
|
35
|
+
if (`sha256:${String(manifestEntry.sha256 || "").toLowerCase()}` !== digest || Number(manifestEntry.size) !== size) {
|
|
36
|
+
throw new Error("subject bytes do not match the final platform manifest");
|
|
37
|
+
}
|
|
38
|
+
const manifestDigest = githubArtifactAttestationSha256File(manifestPath);
|
|
39
|
+
const runtimeSha = required("BUILDCHAIN_RUNTIME_SHA").toLowerCase();
|
|
40
|
+
const signerSha = required("BUILDCHAIN_GITHUB_ATTESTATION_SIGNER_SHA").toLowerCase();
|
|
41
|
+
const policy = createGitHubArtifactAttestationPolicy({
|
|
42
|
+
subject: { name: path.basename(subjectPath), path: subjectRelativePath, size, digest },
|
|
43
|
+
caller: {
|
|
44
|
+
repository: required("BUILDCHAIN_SOURCE_REPOSITORY"),
|
|
45
|
+
sourceSha: required("BUILDCHAIN_SOURCE_SHA").toLowerCase(),
|
|
46
|
+
sourceTreeSha: required("BUILDCHAIN_SOURCE_TREE_SHA").toLowerCase(),
|
|
47
|
+
},
|
|
48
|
+
signer: {
|
|
49
|
+
repository: "kungfu-systems/buildchain",
|
|
50
|
+
workflowPath: GITHUB_ARTIFACT_ATTESTATION_WORKFLOW,
|
|
51
|
+
workflowDigest: signerSha,
|
|
52
|
+
},
|
|
53
|
+
build: {
|
|
54
|
+
platform: required("BUILDCHAIN_PLATFORM_ID"),
|
|
55
|
+
platformManifestDigest: manifestDigest,
|
|
56
|
+
runnerReceiptRoot: manifestDigest,
|
|
57
|
+
buildchainRuntimeSha: runtimeSha,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
61
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(policy, null, 2)}\n`);
|
|
62
|
+
process.stdout.write(`github-artifact-attestation-policy=${outputPath}\n`);
|
|
@@ -16,13 +16,18 @@ function bool(value, fallback = false) {
|
|
|
16
16
|
|
|
17
17
|
function repository(value) {
|
|
18
18
|
const normalized = text(value);
|
|
19
|
-
if (!/^[^/\s]+\/[^/\s]+$/.test(normalized))
|
|
19
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(normalized))
|
|
20
|
+
throw new Error(`repository must be owner/repo, got ${value || "<empty>"}`);
|
|
20
21
|
return normalized;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function branch(value, name) {
|
|
24
25
|
const normalized = text(value).replace(/^refs\/heads\//, "");
|
|
25
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
!normalized ||
|
|
28
|
+
normalized.startsWith("-") ||
|
|
29
|
+
/[\s~^:?*[\\]/.test(normalized)
|
|
30
|
+
) {
|
|
26
31
|
throw new Error(`${name} is not a valid branch name`);
|
|
27
32
|
}
|
|
28
33
|
return normalized;
|
|
@@ -43,24 +48,69 @@ function integer(value, fallback) {
|
|
|
43
48
|
|
|
44
49
|
export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
45
50
|
return {
|
|
46
|
-
repository: repository(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
repository: repository(
|
|
52
|
+
options.repository ??
|
|
53
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_REPOSITORY ??
|
|
54
|
+
process.env.GITHUB_REPOSITORY,
|
|
55
|
+
),
|
|
56
|
+
sourceBranch: branch(
|
|
57
|
+
options.sourceBranch ??
|
|
58
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH ??
|
|
59
|
+
"dev/v4/v4.0",
|
|
60
|
+
"sourceBranch",
|
|
61
|
+
),
|
|
62
|
+
targetBranch: branch(
|
|
63
|
+
options.targetBranch ??
|
|
64
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH ??
|
|
65
|
+
"alpha/v4/v4.0",
|
|
66
|
+
"targetBranch",
|
|
67
|
+
),
|
|
68
|
+
devWorkflowPath: workflowPath(
|
|
69
|
+
options.devWorkflowPath ??
|
|
70
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_DEV_WORKFLOW ??
|
|
71
|
+
".github/workflows/dev-verify-patrol.yml",
|
|
72
|
+
"devWorkflowPath",
|
|
73
|
+
),
|
|
74
|
+
alphaWorkflowPath: workflowPath(
|
|
75
|
+
options.alphaWorkflowPath ??
|
|
76
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_ALPHA_WORKFLOW ??
|
|
77
|
+
".github/workflows/alpha-promotion-preflight.yml",
|
|
78
|
+
"alphaWorkflowPath",
|
|
79
|
+
),
|
|
80
|
+
maxAgeSeconds: integer(
|
|
81
|
+
options.maxAgeSeconds ??
|
|
82
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS,
|
|
83
|
+
7 * 24 * 60 * 60,
|
|
84
|
+
),
|
|
85
|
+
createPullRequest: bool(
|
|
86
|
+
options.createPullRequest ??
|
|
87
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR,
|
|
88
|
+
false,
|
|
89
|
+
),
|
|
90
|
+
dryRun: bool(
|
|
91
|
+
options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
|
|
92
|
+
true,
|
|
93
|
+
),
|
|
94
|
+
now:
|
|
95
|
+
text(options.now ?? process.env.BUILDCHAIN_CHANNEL_PATROL_NOW) ||
|
|
96
|
+
new Date().toISOString(),
|
|
97
|
+
outputPath:
|
|
98
|
+
text(
|
|
99
|
+
options.outputPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_OUTPUT_PATH,
|
|
100
|
+
) || ".buildchain/patrol/dev-alpha-candidate.json",
|
|
56
101
|
};
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
|
|
60
105
|
const matching = runs
|
|
61
|
-
.filter(
|
|
106
|
+
.filter(
|
|
107
|
+
(run) => run.path === workflowPathValue && run.head_sha === sourceSha,
|
|
108
|
+
)
|
|
62
109
|
.sort((left, right) => Number(right.id) - Number(left.id));
|
|
63
|
-
if (matching.length === 0)
|
|
110
|
+
if (matching.length === 0)
|
|
111
|
+
throw new Error(
|
|
112
|
+
`missing completed same-SHA workflow run: ${workflowPathValue}`,
|
|
113
|
+
);
|
|
64
114
|
const run = matching[0];
|
|
65
115
|
return {
|
|
66
116
|
workflowPath: workflowPathValue,
|
|
@@ -75,25 +125,122 @@ function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
|
|
|
75
125
|
};
|
|
76
126
|
}
|
|
77
127
|
|
|
78
|
-
|
|
128
|
+
function workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }) {
|
|
129
|
+
const completedAt = Date.parse(run.updated_at);
|
|
130
|
+
const ageSeconds = (Date.parse(now) - completedAt) / 1000;
|
|
131
|
+
return (
|
|
132
|
+
run.status === "completed" &&
|
|
133
|
+
run.conclusion === "success" &&
|
|
134
|
+
Number.isFinite(ageSeconds) &&
|
|
135
|
+
ageSeconds >= 0 &&
|
|
136
|
+
ageSeconds <= maxAgeSeconds
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function latestRunsBySha(runs, workflowPathValue) {
|
|
141
|
+
const latest = new Map();
|
|
142
|
+
for (const run of runs.filter((row) => row.path === workflowPathValue)) {
|
|
143
|
+
const current = latest.get(run.head_sha);
|
|
144
|
+
if (!current || Number(run.id) > Number(current.id))
|
|
145
|
+
latest.set(run.head_sha, run);
|
|
146
|
+
}
|
|
147
|
+
return latest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function selectLatestQualifiedSource({
|
|
151
|
+
sourceHistory,
|
|
152
|
+
workflowRunsByPath,
|
|
153
|
+
requiredWorkflowPaths,
|
|
154
|
+
now,
|
|
155
|
+
maxAgeSeconds,
|
|
156
|
+
}) {
|
|
157
|
+
const latestByPath = new Map(
|
|
158
|
+
requiredWorkflowPaths.map((workflow) => [
|
|
159
|
+
workflow,
|
|
160
|
+
latestRunsBySha(workflowRunsByPath.get(workflow) || [], workflow),
|
|
161
|
+
]),
|
|
162
|
+
);
|
|
163
|
+
for (let index = 0; index < sourceHistory.length; index += 1) {
|
|
164
|
+
const sourceSha = sourceHistory[index];
|
|
165
|
+
const rows = requiredWorkflowPaths.map((workflow) =>
|
|
166
|
+
latestByPath.get(workflow).get(sourceSha),
|
|
167
|
+
);
|
|
168
|
+
if (
|
|
169
|
+
rows.every((run) =>
|
|
170
|
+
workflowEvidenceIsFreshAndSuccessful(run || {}, { now, maxAgeSeconds }),
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
return {
|
|
174
|
+
sourceSha,
|
|
175
|
+
skippedNewerCommitCount: index,
|
|
176
|
+
workflowEvidence: requiredWorkflowPaths.map((workflow) =>
|
|
177
|
+
latestWorkflowEvidence(
|
|
178
|
+
workflowRunsByPath.get(workflow) || [],
|
|
179
|
+
workflow,
|
|
180
|
+
sourceSha,
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
throw new Error(
|
|
187
|
+
"no source commit ahead of target has fresh completed successful same-SHA workflow evidence",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function runDevAlphaCandidatePatrol(
|
|
192
|
+
optionsInput = {},
|
|
193
|
+
clientInput,
|
|
194
|
+
) {
|
|
79
195
|
const options = normalizeDevAlphaPatrolOptions(optionsInput);
|
|
80
|
-
if (options.sourceBranch === options.targetBranch)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
196
|
+
if (options.sourceBranch === options.targetBranch)
|
|
197
|
+
throw new Error("source and target branches must differ");
|
|
198
|
+
const client =
|
|
199
|
+
clientInput ||
|
|
200
|
+
createGitHubChannelCandidateClient({
|
|
201
|
+
repository: options.repository,
|
|
202
|
+
token: process.env.GITHUB_TOKEN,
|
|
203
|
+
});
|
|
204
|
+
const [observedSourceHeadSha, targetSha] = await Promise.all([
|
|
86
205
|
client.resolveBranch(options.sourceBranch),
|
|
87
206
|
client.resolveBranch(options.targetBranch),
|
|
88
207
|
]);
|
|
89
|
-
const
|
|
90
|
-
const requiredWorkflowPaths = [
|
|
208
|
+
const headComparison = await client.compare(targetSha, observedSourceHeadSha);
|
|
209
|
+
const requiredWorkflowPaths = [
|
|
210
|
+
options.devWorkflowPath,
|
|
211
|
+
options.alphaWorkflowPath,
|
|
212
|
+
];
|
|
213
|
+
let sourceSha = observedSourceHeadSha;
|
|
214
|
+
let comparison = headComparison;
|
|
91
215
|
let workflowEvidence = [];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
216
|
+
let skippedNewerCommitCount = 0;
|
|
217
|
+
if (
|
|
218
|
+
headComparison.status === "ahead" &&
|
|
219
|
+
Number(headComparison.ahead_by) > 0
|
|
220
|
+
) {
|
|
221
|
+
const [sourceHistory, ...workflowRunSets] = await Promise.all([
|
|
222
|
+
client.listBranchHistory(options.sourceBranch, targetSha),
|
|
223
|
+
...requiredWorkflowPaths.map((workflow) =>
|
|
224
|
+
client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
|
|
225
|
+
),
|
|
226
|
+
]);
|
|
227
|
+
const selected = selectLatestQualifiedSource({
|
|
228
|
+
sourceHistory,
|
|
229
|
+
workflowRunsByPath: new Map(
|
|
230
|
+
requiredWorkflowPaths.map((workflow, index) => [
|
|
231
|
+
workflow,
|
|
232
|
+
workflowRunSets[index],
|
|
233
|
+
]),
|
|
234
|
+
),
|
|
235
|
+
requiredWorkflowPaths,
|
|
236
|
+
now: options.now,
|
|
237
|
+
maxAgeSeconds: options.maxAgeSeconds,
|
|
238
|
+
});
|
|
239
|
+
sourceSha = selected.sourceSha;
|
|
240
|
+
skippedNewerCommitCount = selected.skippedNewerCommitCount;
|
|
241
|
+
workflowEvidence = selected.workflowEvidence;
|
|
242
|
+
if (sourceSha !== observedSourceHeadSha)
|
|
243
|
+
comparison = await client.compare(targetSha, sourceSha);
|
|
97
244
|
}
|
|
98
245
|
const decision = decideChannelCandidate({
|
|
99
246
|
repository: options.repository,
|
|
@@ -102,6 +249,11 @@ export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput)
|
|
|
102
249
|
sourceSha,
|
|
103
250
|
targetSha,
|
|
104
251
|
comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
|
|
252
|
+
selection: {
|
|
253
|
+
mode: "latest-qualified-source-ancestor",
|
|
254
|
+
observedSourceHeadSha,
|
|
255
|
+
skippedNewerCommitCount,
|
|
256
|
+
},
|
|
105
257
|
workflowEvidence,
|
|
106
258
|
requiredWorkflowPaths,
|
|
107
259
|
maxAgeSeconds: options.maxAgeSeconds,
|
|
@@ -118,11 +270,14 @@ export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput)
|
|
|
118
270
|
"Buildchain exact-source channel candidate.",
|
|
119
271
|
"",
|
|
120
272
|
`- Source branch: \`${options.sourceBranch}\``,
|
|
273
|
+
`- Observed source HEAD: \`${observedSourceHeadSha}\``,
|
|
121
274
|
`- Source SHA: \`${sourceSha}\``,
|
|
275
|
+
`- Skipped newer unqualified commits: \`${skippedNewerCommitCount}\``,
|
|
122
276
|
`- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
|
|
123
277
|
`- Decision root: \`${decision.decisionRoot}\``,
|
|
124
278
|
...decision.workflowEvidence.map(
|
|
125
|
-
(row) =>
|
|
279
|
+
(row) =>
|
|
280
|
+
`- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
|
|
126
281
|
),
|
|
127
282
|
"",
|
|
128
283
|
"The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
|
|
@@ -142,7 +297,11 @@ function encodeRef(value) {
|
|
|
142
297
|
return value.split("/").map(encodeURIComponent).join("/");
|
|
143
298
|
}
|
|
144
299
|
|
|
145
|
-
export function createGitHubChannelCandidateClient({
|
|
300
|
+
export function createGitHubChannelCandidateClient({
|
|
301
|
+
repository: repositoryInput,
|
|
302
|
+
token,
|
|
303
|
+
fetchImpl = globalThis.fetch,
|
|
304
|
+
}) {
|
|
146
305
|
const [owner, repo] = repository(repositoryInput).split("/");
|
|
147
306
|
const headers = {
|
|
148
307
|
accept: "application/vnd.github+json",
|
|
@@ -150,34 +309,74 @@ export function createGitHubChannelCandidateClient({ repository: repositoryInput
|
|
|
150
309
|
"user-agent": "buildchain-dev-alpha-candidate-patrol",
|
|
151
310
|
"x-github-api-version": "2022-11-28",
|
|
152
311
|
};
|
|
153
|
-
async function api(
|
|
312
|
+
async function api(
|
|
313
|
+
requestPath,
|
|
314
|
+
{ method = "GET", body, allow404 = false } = {},
|
|
315
|
+
) {
|
|
154
316
|
const response = await fetchImpl(`https://api.github.com${requestPath}`, {
|
|
155
317
|
method,
|
|
156
|
-
headers: Object.fromEntries(
|
|
318
|
+
headers: Object.fromEntries(
|
|
319
|
+
Object.entries(headers).filter(([, value]) => value),
|
|
320
|
+
),
|
|
157
321
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
158
322
|
});
|
|
159
323
|
const raw = await response.text();
|
|
160
324
|
const payload = raw ? JSON.parse(raw) : undefined;
|
|
161
325
|
if (allow404 && response.status === 404) return undefined;
|
|
162
|
-
if (!response.ok)
|
|
326
|
+
if (!response.ok)
|
|
327
|
+
throw new Error(
|
|
328
|
+
`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`,
|
|
329
|
+
);
|
|
163
330
|
return payload;
|
|
164
331
|
}
|
|
165
332
|
return {
|
|
166
333
|
async resolveBranch(ref) {
|
|
167
|
-
const payload = await api(
|
|
334
|
+
const payload = await api(
|
|
335
|
+
`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
|
|
336
|
+
);
|
|
168
337
|
return text(payload.object?.sha);
|
|
169
338
|
},
|
|
170
339
|
async compare(baseSha, headSha) {
|
|
171
340
|
return api(`/repos/${owner}/${repo}/compare/${baseSha}...${headSha}`);
|
|
172
341
|
},
|
|
173
|
-
async
|
|
174
|
-
const
|
|
175
|
-
|
|
342
|
+
async listCompletedWorkflowRuns(workflowPathValue, sourceBranch) {
|
|
343
|
+
const runs = [];
|
|
344
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
345
|
+
const payload = await api(
|
|
346
|
+
`/repos/${owner}/${repo}/actions/workflows/${encodeURIComponent(workflowPathValue)}/runs?branch=${encodeURIComponent(sourceBranch)}&status=completed&per_page=100&page=${page}`,
|
|
347
|
+
);
|
|
348
|
+
const rows = payload.workflow_runs || [];
|
|
349
|
+
runs.push(...rows);
|
|
350
|
+
if (rows.length < 100) return runs;
|
|
351
|
+
}
|
|
352
|
+
throw new Error(
|
|
353
|
+
`${workflowPathValue} completed workflow history exceeds 1000 runs`,
|
|
354
|
+
);
|
|
355
|
+
},
|
|
356
|
+
async listBranchHistory(sourceBranch, targetSha) {
|
|
357
|
+
const commits = [];
|
|
358
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
359
|
+
const rows = await api(
|
|
360
|
+
`/repos/${owner}/${repo}/commits?sha=${encodeURIComponent(sourceBranch)}&per_page=100&page=${page}`,
|
|
361
|
+
);
|
|
362
|
+
for (const commit of rows) {
|
|
363
|
+
const commitSha = text(commit.sha);
|
|
364
|
+
if (commitSha === targetSha) return commits;
|
|
365
|
+
commits.push(commitSha);
|
|
366
|
+
}
|
|
367
|
+
if (rows.length < 100) return commits;
|
|
368
|
+
}
|
|
369
|
+
return commits;
|
|
176
370
|
},
|
|
177
371
|
async ensureImmutableBranch(ref, sourceSha) {
|
|
178
|
-
const current = await api(
|
|
372
|
+
const current = await api(
|
|
373
|
+
`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`,
|
|
374
|
+
{ allow404: true },
|
|
375
|
+
);
|
|
179
376
|
if (current && current.object?.sha !== sourceSha) {
|
|
180
|
-
throw new Error(
|
|
377
|
+
throw new Error(
|
|
378
|
+
`source-lock branch ${ref} points to ${current.object?.sha}, not ${sourceSha}`,
|
|
379
|
+
);
|
|
181
380
|
}
|
|
182
381
|
if (current) return current;
|
|
183
382
|
return api(`/repos/${owner}/${repo}/git/refs`, {
|
|
@@ -186,11 +385,16 @@ export function createGitHubChannelCandidateClient({ repository: repositoryInput
|
|
|
186
385
|
});
|
|
187
386
|
},
|
|
188
387
|
async ensurePullRequest({ head, base, title, body }) {
|
|
189
|
-
const open = await api(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
388
|
+
const open = await api(
|
|
389
|
+
`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
|
|
390
|
+
);
|
|
391
|
+
return (
|
|
392
|
+
open[0] ||
|
|
393
|
+
api(`/repos/${owner}/${repo}/pulls`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
body: { head, base, title, body },
|
|
396
|
+
})
|
|
397
|
+
);
|
|
194
398
|
},
|
|
195
399
|
};
|
|
196
400
|
}
|
|
@@ -214,7 +418,8 @@ async function main() {
|
|
|
214
418
|
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
215
419
|
fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
216
420
|
const summary = markdown(result);
|
|
217
|
-
if (process.env.GITHUB_STEP_SUMMARY)
|
|
421
|
+
if (process.env.GITHUB_STEP_SUMMARY)
|
|
422
|
+
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
|
218
423
|
else process.stdout.write(summary);
|
|
219
424
|
if (process.env.GITHUB_OUTPUT) {
|
|
220
425
|
const outputs = {
|
|
@@ -224,7 +429,12 @@ async function main() {
|
|
|
224
429
|
"source-lock-ref": result.decision.sourceLockRef || "",
|
|
225
430
|
"promotion-pr": result.pullRequest?.html_url || "",
|
|
226
431
|
};
|
|
227
|
-
fs.appendFileSync(
|
|
432
|
+
fs.appendFileSync(
|
|
433
|
+
process.env.GITHUB_OUTPUT,
|
|
434
|
+
`${Object.entries(outputs)
|
|
435
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
436
|
+
.join("\n")}\n`,
|
|
437
|
+
);
|
|
228
438
|
}
|
|
229
439
|
}
|
|
230
440
|
|
|
@@ -328,6 +328,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
328
328
|
"product-mechanism": { capabilityGroup: "getting-started", audience: ["agent", "maintainer"], maturity: "stable", order: 30 },
|
|
329
329
|
cli: { capabilityGroup: "api-cli-reference", audience: ["agent", "developer"], maturity: "stable", order: 40 },
|
|
330
330
|
"release-passport": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 100 },
|
|
331
|
+
"github-artifact-attestation": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 108 },
|
|
331
332
|
"publication-authority": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 105 },
|
|
332
333
|
"github-governance-authority": { capabilityGroup: "governance-versioning", audience: ["maintainer", "release-operator", "agent"], maturity: "preview", order: 106 },
|
|
333
334
|
"controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
|
|
@@ -405,6 +406,7 @@ function cliCommandMeta(id) {
|
|
|
405
406
|
"collect-github-release": { group: "release-passport-trust", purpose: "Collect GitHub Release assets into a release passport." },
|
|
406
407
|
create: { group: "release-passport-trust", purpose: "Create canonical sealed publication evidence documents." },
|
|
407
408
|
"create-publication-admission": { group: "release-passport-trust", purpose: "Create a canonical short-lived publication admission envelope from exact consumer bindings." },
|
|
409
|
+
"create-github-artifact-attestation-policy": { group: "release-passport-trust", purpose: "Create an exact source, signer, Linux build, and Release Passport attestation policy." },
|
|
408
410
|
"create-runner-provenance": { group: "release-passport-trust", purpose: "Create runner provenance evidence with an explicit qualification floor." },
|
|
409
411
|
diagnostics: { group: "observability-diagnostics", purpose: "Inspect diagnostics command families." },
|
|
410
412
|
"diagnostics-summary": { group: "observability-diagnostics", purpose: "Summarize diagnostics artifacts into JSON and cross-platform lifecycle timing tables." },
|
|
@@ -494,6 +496,7 @@ function cliCommandMeta(id) {
|
|
|
494
496
|
verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
|
|
495
497
|
"verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
|
|
496
498
|
"verify-artifact-envelope": { group: "release-passport-trust", purpose: "Verify exact roots, identity, lifecycle, revocation, and an existing KFD assessment in a sealed artifact envelope." },
|
|
499
|
+
"verify-github-artifact-attestation": { group: "release-passport-trust", purpose: "Verify GitHub keyless attestation identity plus local artifact, manifest, Passport, predicate, bundle, and evidence bindings." },
|
|
497
500
|
"verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
|
|
498
501
|
"verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
|
|
499
502
|
"verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
|
|
@@ -532,6 +535,7 @@ function nodeApiMeta(exportName) {
|
|
|
532
535
|
"./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
|
|
533
536
|
"./anchored-version-material": { group: "reusable-build", summary: "Anchored/manual derived version material preflight, exact-tree binding, and digest evidence APIs." },
|
|
534
537
|
"./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
|
|
538
|
+
"./github-artifact-attestation": { group: "release-passport-trust", summary: "GitHub keyless artifact attestation policy, predicate, provider evidence, and fail-closed verification APIs." },
|
|
535
539
|
"./kfd-agent-hub": { group: "kfd-trust", summary: "Declarative Agent Hub adapter inspection, fixed-suite execution, exact KFD cut locking, and agent explanation APIs." },
|
|
536
540
|
"./release-passport-contract": { group: "release-passport-trust", summary: "Standalone release passport JSON Schema, ownership/check manifest, and structural validation APIs." },
|
|
537
541
|
"./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
|
|
@@ -606,6 +610,7 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
|
|
|
606
610
|
}
|
|
607
611
|
|
|
608
612
|
function workflowCapabilityGroup(entry) {
|
|
613
|
+
if (entry.id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
|
|
609
614
|
if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
|
|
610
615
|
if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
|
|
611
616
|
if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
|
|
@@ -615,6 +620,7 @@ function workflowCapabilityGroup(entry) {
|
|
|
615
620
|
}
|
|
616
621
|
|
|
617
622
|
function actionCapabilityGroup(id) {
|
|
623
|
+
if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
|
|
618
624
|
if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
|
|
619
625
|
if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
|
|
620
626
|
if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
|
|
@@ -898,6 +904,7 @@ function buildSiteBundle() {
|
|
|
898
904
|
["dev-pr-auto-merge", "dev-governance"],
|
|
899
905
|
["github-governance-audit", "dev-governance"],
|
|
900
906
|
["binary-distribution", "release-passport"],
|
|
907
|
+
["github-artifact-attestation", "release-passport"],
|
|
901
908
|
["buildchain-patrol", "repository-patrol"],
|
|
902
909
|
["buildchain-patrol-daily", "repository-patrol"],
|
|
903
910
|
["buildchain-patrol-weekly", "repository-patrol"],
|
|
@@ -991,6 +998,7 @@ function buildSiteBundle() {
|
|
|
991
998
|
"publish evidence JSON",
|
|
992
999
|
"buildchain.release.json",
|
|
993
1000
|
"release passport assets",
|
|
1001
|
+
"GitHub artifact attestation Sigstore bundle and Buildchain evidence JSON",
|
|
994
1002
|
],
|
|
995
1003
|
owner: "promote-buildchain-ref",
|
|
996
1004
|
},
|
|
@@ -1023,6 +1031,10 @@ function buildSiteBundle() {
|
|
|
1023
1031
|
"llms.txt",
|
|
1024
1032
|
"buildchain-release-bundle.json",
|
|
1025
1033
|
"buildchain-release-bundle.tar.gz",
|
|
1034
|
+
"github-artifact-attestation.policy.json",
|
|
1035
|
+
"github-artifact-attestation.predicate.json",
|
|
1036
|
+
"github-artifact-attestation.evidence.json",
|
|
1037
|
+
"attestation.sigstore.json",
|
|
1026
1038
|
],
|
|
1027
1039
|
site: [
|
|
1028
1040
|
"buildchain-site.json",
|