@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
|
@@ -34,28 +34,40 @@ function required(value, name) {
|
|
|
34
34
|
|
|
35
35
|
function sha(value, name) {
|
|
36
36
|
const normalized = required(value, name).toLowerCase();
|
|
37
|
-
if (!SHA.test(normalized))
|
|
37
|
+
if (!SHA.test(normalized))
|
|
38
|
+
throw new Error(`${name} must be an exact 40-character SHA`);
|
|
38
39
|
return normalized;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds }) {
|
|
42
|
-
if (!row || typeof row !== "object")
|
|
43
|
+
if (!row || typeof row !== "object")
|
|
44
|
+
throw new Error("workflow evidence is required");
|
|
43
45
|
const workflowPath = required(row.workflowPath, "workflowPath");
|
|
44
46
|
if (sha(row.headSha, `${workflowPath} headSha`) !== sourceSha) {
|
|
45
|
-
throw new Error(
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${workflowPath} evidence does not bind source SHA ${sourceSha}`,
|
|
49
|
+
);
|
|
46
50
|
}
|
|
47
51
|
if (row.status !== "completed" || row.conclusion !== "success") {
|
|
48
52
|
throw new Error(`${workflowPath} is not a completed successful run`);
|
|
49
53
|
}
|
|
50
54
|
const completedAt = required(row.completedAt, `${workflowPath} completedAt`);
|
|
51
55
|
const ageSeconds = (Date.parse(now) - Date.parse(completedAt)) / 1000;
|
|
52
|
-
if (
|
|
53
|
-
|
|
56
|
+
if (
|
|
57
|
+
!Number.isFinite(ageSeconds) ||
|
|
58
|
+
ageSeconds < 0 ||
|
|
59
|
+
ageSeconds > maxAgeSeconds
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${workflowPath} evidence is stale or has an invalid completion time`,
|
|
63
|
+
);
|
|
54
64
|
}
|
|
55
65
|
const runId = Number(row.runId);
|
|
56
66
|
const runAttempt = Number(row.runAttempt || 1);
|
|
57
|
-
if (!Number.isSafeInteger(runId) || runId <= 0)
|
|
58
|
-
|
|
67
|
+
if (!Number.isSafeInteger(runId) || runId <= 0)
|
|
68
|
+
throw new Error(`${workflowPath} runId is invalid`);
|
|
69
|
+
if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0)
|
|
70
|
+
throw new Error(`${workflowPath} runAttempt is invalid`);
|
|
59
71
|
return {
|
|
60
72
|
workflowPath,
|
|
61
73
|
workflowName: required(row.workflowName, `${workflowPath} workflowName`),
|
|
@@ -79,9 +91,31 @@ export function decideChannelCandidate(input) {
|
|
|
79
91
|
const repository = required(input.repository, "repository");
|
|
80
92
|
const sourceBranch = required(input.sourceBranch, "sourceBranch");
|
|
81
93
|
const targetBranch = required(input.targetBranch, "targetBranch");
|
|
82
|
-
if (sourceBranch === targetBranch)
|
|
94
|
+
if (sourceBranch === targetBranch)
|
|
95
|
+
throw new Error("sourceBranch and targetBranch must differ");
|
|
83
96
|
const sourceSha = sha(input.sourceSha, "sourceSha");
|
|
84
97
|
const targetSha = sha(input.targetSha, "targetSha");
|
|
98
|
+
const selection = input.selection
|
|
99
|
+
? {
|
|
100
|
+
mode: required(input.selection.mode, "selection.mode"),
|
|
101
|
+
observedSourceHeadSha: sha(
|
|
102
|
+
input.selection.observedSourceHeadSha,
|
|
103
|
+
"selection.observedSourceHeadSha",
|
|
104
|
+
),
|
|
105
|
+
skippedNewerCommitCount: Number(
|
|
106
|
+
input.selection.skippedNewerCommitCount || 0,
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
: undefined;
|
|
110
|
+
if (
|
|
111
|
+
selection &&
|
|
112
|
+
(!Number.isSafeInteger(selection.skippedNewerCommitCount) ||
|
|
113
|
+
selection.skippedNewerCommitCount < 0)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"selection.skippedNewerCommitCount must be a non-negative integer",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
85
119
|
const now = required(input.now || new Date().toISOString(), "now");
|
|
86
120
|
const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
|
|
87
121
|
if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
|
|
@@ -89,7 +123,10 @@ export function decideChannelCandidate(input) {
|
|
|
89
123
|
}
|
|
90
124
|
const comparison = input.comparison || {};
|
|
91
125
|
if (comparison.status !== "ahead" || Number(comparison.aheadBy) <= 0) {
|
|
92
|
-
const reason =
|
|
126
|
+
const reason =
|
|
127
|
+
comparison.status === "identical"
|
|
128
|
+
? "target-already-current"
|
|
129
|
+
: "source-does-not-lead-target";
|
|
93
130
|
return {
|
|
94
131
|
schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
|
|
95
132
|
eligible: false,
|
|
@@ -97,15 +134,25 @@ export function decideChannelCandidate(input) {
|
|
|
97
134
|
repository,
|
|
98
135
|
source: { branch: sourceBranch, sha: sourceSha },
|
|
99
136
|
target: { branch: targetBranch, sha: targetSha },
|
|
100
|
-
comparison: {
|
|
137
|
+
comparison: {
|
|
138
|
+
status: String(comparison.status || "unknown"),
|
|
139
|
+
aheadBy: Number(comparison.aheadBy || 0),
|
|
140
|
+
},
|
|
101
141
|
decidedAt: now,
|
|
102
142
|
};
|
|
103
143
|
}
|
|
104
|
-
const rows = Array.isArray(input.workflowEvidence)
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
const rows = Array.isArray(input.workflowEvidence)
|
|
145
|
+
? input.workflowEvidence
|
|
146
|
+
: [];
|
|
147
|
+
const expectedPaths = [
|
|
148
|
+
...new Set((input.requiredWorkflowPaths || []).map(String)),
|
|
149
|
+
];
|
|
150
|
+
if (expectedPaths.length === 0)
|
|
151
|
+
throw new Error("requiredWorkflowPaths must not be empty");
|
|
107
152
|
if (rows.length !== expectedPaths.length) {
|
|
108
|
-
throw new Error(
|
|
153
|
+
throw new Error(
|
|
154
|
+
`expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`,
|
|
155
|
+
);
|
|
109
156
|
}
|
|
110
157
|
const byPath = new Map();
|
|
111
158
|
for (const row of rows) {
|
|
@@ -116,7 +163,8 @@ export function decideChannelCandidate(input) {
|
|
|
116
163
|
byPath.set(qualified.workflowPath, qualified);
|
|
117
164
|
}
|
|
118
165
|
for (const workflowPath of expectedPaths) {
|
|
119
|
-
if (!byPath.has(workflowPath))
|
|
166
|
+
if (!byPath.has(workflowPath))
|
|
167
|
+
throw new Error(`missing workflow evidence: ${workflowPath}`);
|
|
120
168
|
}
|
|
121
169
|
const body = {
|
|
122
170
|
schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
|
|
@@ -126,8 +174,11 @@ export function decideChannelCandidate(input) {
|
|
|
126
174
|
source: { branch: sourceBranch, sha: sourceSha },
|
|
127
175
|
target: { branch: targetBranch, sha: targetSha },
|
|
128
176
|
comparison: { status: "ahead", aheadBy: Number(comparison.aheadBy) },
|
|
177
|
+
...(selection ? { selection } : {}),
|
|
129
178
|
sourceLockRef: channelCandidateSourceLockRef(targetBranch, sourceSha),
|
|
130
|
-
workflowEvidence: expectedPaths.map((workflowPath) =>
|
|
179
|
+
workflowEvidence: expectedPaths.map((workflowPath) =>
|
|
180
|
+
byPath.get(workflowPath),
|
|
181
|
+
),
|
|
131
182
|
policy: { maxAgeSeconds, requiredWorkflowPaths: expectedPaths },
|
|
132
183
|
decidedAt: now,
|
|
133
184
|
};
|