@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.0
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/report-buildchain-issue/README.md +1 -1
- package/contracts/buildchain-v2-residuals-v1.json +191 -0
- package/dist/site/buildchain-contract.json +58 -35
- package/dist/site/buildchain-site.json +33 -33
- package/dist/site/controller-registry.json +24 -3
- package/dist/site/kfd-claims.json +11 -6
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +22 -22
- package/dist/site/public-surface-audit.json +10 -5
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +8 -3
- package/docs/auditable-demo.md +19 -1
- package/docs/consumer-issue-reporting.md +1 -1
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +23 -30
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/release-governance.md +8 -1
- package/docs/reusable-build-surface.md +124 -64
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/versioning.md +41 -21
- package/docs/web-surface-deployments.md +20 -1
- package/package.json +1 -1
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/release-passport.js +36 -27
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +147 -2
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +20 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +114 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/inspect-artifact-signing-requests.mjs +45 -14
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/runtime-ref-core.mjs +13 -2
- package/scripts/seal-artifact-signing-requests.mjs +51 -6
- package/scripts/stable-candidate-qualification.mjs +8 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -10,6 +10,34 @@ export const BUILDCHAIN_RELEASE_RECONCILIATION_PATHS = Object.freeze([
|
|
|
10
10
|
"package.json",
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
|
+
function deploymentPatternExpression(pattern) {
|
|
14
|
+
let expression = "";
|
|
15
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
16
|
+
const character = pattern[index];
|
|
17
|
+
if (character === "*") {
|
|
18
|
+
if (pattern[index + 1] === "*") {
|
|
19
|
+
expression += ".*";
|
|
20
|
+
index += 1;
|
|
21
|
+
} else {
|
|
22
|
+
expression += "[^/]*";
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
expression += character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return new RegExp(`^${expression}$`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function matchesGithubDeploymentPolicy(policy, { ref, refType = "branch" } = {}) {
|
|
32
|
+
const normalizedRef = String(ref || "");
|
|
33
|
+
const normalizedType = String(refType || "");
|
|
34
|
+
const policyName = String(policy?.name || "");
|
|
35
|
+
const policyType = String(policy?.type || "");
|
|
36
|
+
if (!normalizedRef || !["branch", "tag"].includes(normalizedType)) return false;
|
|
37
|
+
if (policyType !== normalizedType || !policyName) return false;
|
|
38
|
+
return deploymentPatternExpression(policyName).test(normalizedRef);
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
export function evaluateBuildchainReleaseReconciliation({
|
|
14
42
|
repository,
|
|
15
43
|
publicationVersion,
|
|
@@ -2428,41 +2428,50 @@ export function createReleaseCheckReport({
|
|
|
2428
2428
|
};
|
|
2429
2429
|
}
|
|
2430
2430
|
|
|
2431
|
-
export async function readJsonFromLocation(
|
|
2431
|
+
export async function readJsonFromLocation(
|
|
2432
|
+
location,
|
|
2433
|
+
redirectCount = 0,
|
|
2434
|
+
{ timeoutMs = 15_000 } = {},
|
|
2435
|
+
) {
|
|
2432
2436
|
const input = nonEmptyString(location, "location");
|
|
2437
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
2438
|
+
throw new Error("timeoutMs must be a positive integer");
|
|
2439
|
+
}
|
|
2433
2440
|
if (redirectCount > 5) {
|
|
2434
2441
|
throw new Error(`too many redirects while reading ${input}`);
|
|
2435
2442
|
}
|
|
2436
2443
|
if (/^https?:\/\//.test(input)) {
|
|
2437
2444
|
const client = input.startsWith("https:") ? https : http;
|
|
2438
2445
|
return new Promise((resolve, reject) => {
|
|
2439
|
-
client
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2446
|
+
const request = client.get(input, (response) => {
|
|
2447
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
2448
|
+
const nextLocation = new URL(response.headers.location, input).toString();
|
|
2449
|
+
response.resume();
|
|
2450
|
+
readJsonFromLocation(nextLocation, redirectCount + 1, { timeoutMs }).then(resolve, reject);
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
2454
|
+
reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
|
|
2455
|
+
response.resume();
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
let body = "";
|
|
2459
|
+
response.setEncoding("utf8");
|
|
2460
|
+
response.on("data", (chunk) => {
|
|
2461
|
+
body += chunk;
|
|
2462
|
+
});
|
|
2463
|
+
response.on("end", () => {
|
|
2464
|
+
try {
|
|
2465
|
+
resolve(JSON.parse(body));
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
reject(error);
|
|
2451
2468
|
}
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
try {
|
|
2459
|
-
resolve(JSON.parse(body));
|
|
2460
|
-
} catch (error) {
|
|
2461
|
-
reject(error);
|
|
2462
|
-
}
|
|
2463
|
-
});
|
|
2464
|
-
})
|
|
2465
|
-
.on("error", reject);
|
|
2469
|
+
});
|
|
2470
|
+
});
|
|
2471
|
+
request.setTimeout(timeoutMs, () => {
|
|
2472
|
+
request.destroy(new Error(`timed out after ${timeoutMs}ms while reading ${input}`));
|
|
2473
|
+
});
|
|
2474
|
+
request.on("error", reject);
|
|
2466
2475
|
});
|
|
2467
2476
|
}
|
|
2468
2477
|
return readJsonFile(input);
|
|
@@ -194,12 +194,14 @@ export function evaluateStableReleaseGate({
|
|
|
194
194
|
: undefined;
|
|
195
195
|
const attestorAllowed = required.allowedAttestors.length === 0 ||
|
|
196
196
|
required.allowedAttestors.includes(string(evidence?.attestor));
|
|
197
|
+
const completionTimingValid = required.source === "release-candidate" ||
|
|
198
|
+
(completedAt && completedAt.milliseconds >= candidatePublished.milliseconds);
|
|
197
199
|
const valid = Boolean(
|
|
198
200
|
evidence &&
|
|
199
201
|
string(evidence.status) === "success" &&
|
|
200
202
|
string(evidence.candidateSha) === candidateSha &&
|
|
201
203
|
completedAt &&
|
|
202
|
-
|
|
204
|
+
completionTimingValid &&
|
|
203
205
|
attestorAllowed,
|
|
204
206
|
);
|
|
205
207
|
if (completedAt) {
|
|
@@ -222,6 +224,7 @@ export function evaluateStableReleaseGate({
|
|
|
222
224
|
evidenceUrl: string(evidence?.evidenceUrl),
|
|
223
225
|
attestor: string(evidence?.attestor),
|
|
224
226
|
attestorAllowed,
|
|
227
|
+
completionTimingValid: Boolean(completionTimingValid),
|
|
225
228
|
},
|
|
226
229
|
));
|
|
227
230
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
7
|
+
|
|
8
|
+
const CONTRACT = "kungfu-buildchain-artifact-signing-delegation/v1";
|
|
9
|
+
|
|
10
|
+
function required(value, label) {
|
|
11
|
+
const normalized = String(value || "").trim();
|
|
12
|
+
if (!normalized) throw new Error(`${label} is required`);
|
|
13
|
+
if (/[\r\n\0]/u.test(normalized))
|
|
14
|
+
throw new Error(`${label} contains control characters`);
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function optional(value, label) {
|
|
19
|
+
const normalized = String(value || "").trim();
|
|
20
|
+
if (/[\r\n\0]/u.test(normalized))
|
|
21
|
+
throw new Error(`${label} contains control characters`);
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exactSha(value, label) {
|
|
26
|
+
const normalized = required(value, label);
|
|
27
|
+
if (!/^[0-9a-f]{40}$/u.test(normalized))
|
|
28
|
+
throw new Error(`${label} must be an exact SHA`);
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function positiveInteger(value, label, { allowZero = false } = {}) {
|
|
33
|
+
const normalized = Number(value);
|
|
34
|
+
if (!Number.isSafeInteger(normalized) || normalized < (allowZero ? 0 : 1)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${label} must be ${allowZero ? "a non-negative" : "a positive"} integer`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function repository(value, label) {
|
|
43
|
+
const normalized = required(value, label);
|
|
44
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(normalized)) {
|
|
45
|
+
throw new Error(`${label} must be owner/repository`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeRelativePath(value, label) {
|
|
51
|
+
const normalized = required(value, label).replaceAll("\\", "/");
|
|
52
|
+
const resolved = path.posix.normalize(normalized);
|
|
53
|
+
if (
|
|
54
|
+
resolved === ".." ||
|
|
55
|
+
resolved.startsWith("../") ||
|
|
56
|
+
path.posix.isAbsolute(resolved)
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`${label} must be a safe relative path`);
|
|
59
|
+
}
|
|
60
|
+
if (!/^[A-Za-z0-9._ /-]+$/u.test(resolved))
|
|
61
|
+
throw new Error(`${label} contains unsafe shell characters`);
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateArtifactSigningDelegation(value) {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
67
|
+
throw new Error("delegation must be an object");
|
|
68
|
+
if (value.schemaVersion !== 1 || value.contract !== CONTRACT)
|
|
69
|
+
throw new Error("artifact signing delegation contract mismatch");
|
|
70
|
+
const requestCount = positiveInteger(value.request?.count, "request.count", {
|
|
71
|
+
allowZero: true,
|
|
72
|
+
});
|
|
73
|
+
const delegation = {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
contract: CONTRACT,
|
|
76
|
+
source: {
|
|
77
|
+
repository: repository(value.source?.repository, "source.repository"),
|
|
78
|
+
runId: required(value.source?.runId, "source.runId"),
|
|
79
|
+
runAttempt: positiveInteger(
|
|
80
|
+
value.source?.runAttempt,
|
|
81
|
+
"source.runAttempt",
|
|
82
|
+
),
|
|
83
|
+
sha: exactSha(value.source?.sha, "source.sha"),
|
|
84
|
+
treeSha: exactSha(value.source?.treeSha, "source.treeSha"),
|
|
85
|
+
},
|
|
86
|
+
runtime: {
|
|
87
|
+
repository: repository(value.runtime?.repository, "runtime.repository"),
|
|
88
|
+
sha: exactSha(value.runtime?.sha, "runtime.sha"),
|
|
89
|
+
},
|
|
90
|
+
platform: {
|
|
91
|
+
id: required(value.platform?.id, "platform.id"),
|
|
92
|
+
name: required(value.platform?.name, "platform.name"),
|
|
93
|
+
},
|
|
94
|
+
request: {
|
|
95
|
+
count: requestCount,
|
|
96
|
+
artifact:
|
|
97
|
+
requestCount > 0
|
|
98
|
+
? required(value.request?.artifact, "request.artifact")
|
|
99
|
+
: optional(value.request?.artifact, "request.artifact"),
|
|
100
|
+
},
|
|
101
|
+
authority: {
|
|
102
|
+
runId:
|
|
103
|
+
requestCount > 0
|
|
104
|
+
? required(value.authority?.runId, "authority.runId")
|
|
105
|
+
: optional(value.authority?.runId, "authority.runId"),
|
|
106
|
+
resultArtifact:
|
|
107
|
+
requestCount > 0
|
|
108
|
+
? required(
|
|
109
|
+
value.authority?.resultArtifact,
|
|
110
|
+
"authority.resultArtifact",
|
|
111
|
+
)
|
|
112
|
+
: optional(
|
|
113
|
+
value.authority?.resultArtifact,
|
|
114
|
+
"authority.resultArtifact",
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
artifact: {
|
|
118
|
+
name: required(value.artifact?.name, "artifact.name"),
|
|
119
|
+
manifestArtifact: required(
|
|
120
|
+
value.artifact?.manifestArtifact,
|
|
121
|
+
"artifact.manifestArtifact",
|
|
122
|
+
),
|
|
123
|
+
diagnosticsArtifact: required(
|
|
124
|
+
value.artifact?.diagnosticsArtifact,
|
|
125
|
+
"artifact.diagnosticsArtifact",
|
|
126
|
+
),
|
|
127
|
+
},
|
|
128
|
+
workingDirectory: safeRelativePath(
|
|
129
|
+
value.workingDirectory || ".",
|
|
130
|
+
"workingDirectory",
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
if (
|
|
134
|
+
requestCount === 0 &&
|
|
135
|
+
(delegation.authority.runId || delegation.authority.resultArtifact)
|
|
136
|
+
) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"unsigned delegation must not contain authority result coordinates",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return delegation;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createArtifactSigningDelegation({
|
|
145
|
+
sourceRepository = process.env.GITHUB_REPOSITORY,
|
|
146
|
+
sourceRunId = process.env.GITHUB_RUN_ID,
|
|
147
|
+
sourceRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
|
|
148
|
+
sourceSha = process.env.BUILDCHAIN_SOURCE_SHA,
|
|
149
|
+
sourceTreeSha = process.env.BUILDCHAIN_SOURCE_TREE_SHA,
|
|
150
|
+
runtimeRepository = process.env.BUILDCHAIN_RUNTIME_REPOSITORY,
|
|
151
|
+
runtimeSha = process.env.BUILDCHAIN_RUNTIME_SHA,
|
|
152
|
+
platformId = process.env.BUILDCHAIN_PLATFORM_ID,
|
|
153
|
+
platformName = process.env.BUILDCHAIN_PLATFORM_NAME,
|
|
154
|
+
requestCount = process.env.BUILDCHAIN_SIGNING_REQUEST_COUNT || "0",
|
|
155
|
+
requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT || "",
|
|
156
|
+
authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
|
|
157
|
+
resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT || "",
|
|
158
|
+
artifactName = process.env.BUILDCHAIN_ARTIFACT_NAME,
|
|
159
|
+
manifestArtifact = process.env.BUILDCHAIN_MANIFEST_ARTIFACT_NAME,
|
|
160
|
+
diagnosticsArtifact = process.env.BUILDCHAIN_DIAGNOSTICS_ARTIFACT_NAME,
|
|
161
|
+
workingDirectory = process.env.BUILDCHAIN_SIGNING_CWD || ".",
|
|
162
|
+
} = {}) {
|
|
163
|
+
return validateArtifactSigningDelegation({
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
contract: CONTRACT,
|
|
166
|
+
source: {
|
|
167
|
+
repository: sourceRepository,
|
|
168
|
+
runId: sourceRunId,
|
|
169
|
+
runAttempt: Number(sourceRunAttempt),
|
|
170
|
+
sha: sourceSha,
|
|
171
|
+
treeSha: sourceTreeSha,
|
|
172
|
+
},
|
|
173
|
+
runtime: { repository: runtimeRepository, sha: runtimeSha },
|
|
174
|
+
platform: { id: platformId, name: platformName },
|
|
175
|
+
request: { count: Number(requestCount), artifact: requestArtifact },
|
|
176
|
+
authority: { runId: authorityRunId, resultArtifact },
|
|
177
|
+
artifact: { name: artifactName, manifestArtifact, diagnosticsArtifact },
|
|
178
|
+
workingDirectory,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function sealArtifactSigningDelegation({
|
|
183
|
+
outputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
|
|
184
|
+
...values
|
|
185
|
+
} = {}) {
|
|
186
|
+
const delegation = createArtifactSigningDelegation(values);
|
|
187
|
+
const target = path.resolve(required(outputPath, "delegation output path"));
|
|
188
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
189
|
+
fs.writeFileSync(target, `${JSON.stringify(delegation, null, 2)}\n`);
|
|
190
|
+
return delegation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function readArtifactSigningDelegation(
|
|
194
|
+
inputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
|
|
195
|
+
) {
|
|
196
|
+
const target = path.resolve(required(inputPath, "delegation input path"));
|
|
197
|
+
return validateArtifactSigningDelegation(
|
|
198
|
+
JSON.parse(fs.readFileSync(target, "utf8")),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function artifactSigningDelegationOutputs(delegation) {
|
|
203
|
+
const value = validateArtifactSigningDelegation(delegation);
|
|
204
|
+
return {
|
|
205
|
+
"request-count": String(value.request.count),
|
|
206
|
+
"request-artifact": value.request.artifact,
|
|
207
|
+
"authority-run-id": value.authority.runId,
|
|
208
|
+
"result-artifact": value.authority.resultArtifact,
|
|
209
|
+
"artifact-name": value.artifact.name,
|
|
210
|
+
"manifest-artifact-name": value.artifact.manifestArtifact,
|
|
211
|
+
"diagnostics-artifact-name": value.artifact.diagnosticsArtifact,
|
|
212
|
+
"working-directory": value.workingDirectory,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function assertArtifactSigningDelegationContext(
|
|
217
|
+
delegation,
|
|
218
|
+
{
|
|
219
|
+
sourceRepository = process.env.BUILDCHAIN_EXPECTED_SOURCE_REPOSITORY || "",
|
|
220
|
+
sourceRunId = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ID || "",
|
|
221
|
+
sourceRunAttempt = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ATTEMPT || "",
|
|
222
|
+
sourceSha = process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || "",
|
|
223
|
+
runtimeRepository = process.env.BUILDCHAIN_EXPECTED_RUNTIME_REPOSITORY ||
|
|
224
|
+
"",
|
|
225
|
+
runtimeSha = process.env.BUILDCHAIN_EXPECTED_RUNTIME_SHA || "",
|
|
226
|
+
platformId = process.env.BUILDCHAIN_EXPECTED_PLATFORM_ID || "",
|
|
227
|
+
} = {},
|
|
228
|
+
) {
|
|
229
|
+
const value = validateArtifactSigningDelegation(delegation);
|
|
230
|
+
const expectations = [
|
|
231
|
+
[sourceRepository, value.source.repository, "source repository"],
|
|
232
|
+
[sourceRunId, value.source.runId, "source run ID"],
|
|
233
|
+
[sourceRunAttempt, String(value.source.runAttempt), "source run attempt"],
|
|
234
|
+
[sourceSha, value.source.sha, "source SHA"],
|
|
235
|
+
[runtimeRepository, value.runtime.repository, "runtime repository"],
|
|
236
|
+
[runtimeSha, value.runtime.sha, "runtime SHA"],
|
|
237
|
+
[platformId, value.platform.id, "platform ID"],
|
|
238
|
+
];
|
|
239
|
+
for (const [expected, actual, label] of expectations) {
|
|
240
|
+
if (expected && String(expected) !== actual)
|
|
241
|
+
throw new Error(`artifact signing delegation ${label} mismatch`);
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
process.argv[1] &&
|
|
248
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
249
|
+
) {
|
|
250
|
+
try {
|
|
251
|
+
const mode = process.argv[2] || "seal";
|
|
252
|
+
if (mode === "seal") {
|
|
253
|
+
sealArtifactSigningDelegation();
|
|
254
|
+
} else if (mode === "outputs") {
|
|
255
|
+
const delegation = assertArtifactSigningDelegationContext(
|
|
256
|
+
readArtifactSigningDelegation(),
|
|
257
|
+
);
|
|
258
|
+
writeGitHubOutputs(artifactSigningDelegationOutputs(delegation));
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error(`unsupported artifact signing delegation mode: ${mode}`);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error(
|
|
264
|
+
`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
|
|
265
|
+
);
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -254,15 +254,33 @@ function addMinutes(iso, minutes) {
|
|
|
254
254
|
return new Date(Date.parse(iso) + minutes * 60_000).toISOString();
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
257
|
+
function repositoryVisibility(repository) {
|
|
258
|
+
return String(
|
|
259
|
+
repository.visibility || (repository.private ? "private" : "public"),
|
|
260
|
+
).toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function selectGithubGovernanceRepositories(
|
|
264
|
+
repositories,
|
|
265
|
+
requested = "",
|
|
266
|
+
descriptor = BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY,
|
|
267
|
+
) {
|
|
268
|
+
const exact = requested
|
|
269
|
+
? repositories.filter((repository) =>
|
|
270
|
+
repository.full_name === requested || repository.name === requested)
|
|
271
|
+
: repositories;
|
|
272
|
+
if (requested && exact.length !== 1) {
|
|
273
|
+
throw new Error("repository selector must resolve exactly once");
|
|
264
274
|
}
|
|
265
|
-
|
|
275
|
+
const managedVisibilities = new Set(
|
|
276
|
+
descriptor.repositoryAdmission.managedVisibilities || ["public"],
|
|
277
|
+
);
|
|
278
|
+
const selected = exact.filter((repository) =>
|
|
279
|
+
managedVisibilities.has(repositoryVisibility(repository)));
|
|
280
|
+
if (requested && selected.length !== 1) {
|
|
281
|
+
throw new Error("repository selector is outside managed governance scope");
|
|
282
|
+
}
|
|
283
|
+
return selected;
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
export function collectGithubGovernanceAudit({
|
|
@@ -291,7 +309,10 @@ export function collectGithubGovernanceAudit({
|
|
|
291
309
|
if (!organizationState.ok || !repositoriesState.readable) {
|
|
292
310
|
throw new Error("organization or managed repository inventory is unreadable; governance audit fails closed");
|
|
293
311
|
}
|
|
294
|
-
const selected =
|
|
312
|
+
const selected = selectGithubGovernanceRepositories(
|
|
313
|
+
repositoriesState.repositories,
|
|
314
|
+
repository,
|
|
315
|
+
);
|
|
295
316
|
if (targetRef && selected.length !== 1) {
|
|
296
317
|
throw new Error("--target-ref requires exactly one selected repository");
|
|
297
318
|
}
|
|
@@ -316,9 +337,7 @@ export function collectGithubGovernanceAudit({
|
|
|
316
337
|
const diagnostics = [];
|
|
317
338
|
for (const metadata of selected) {
|
|
318
339
|
const fullName = String(metadata.full_name || "");
|
|
319
|
-
const visibilityClass =
|
|
320
|
-
metadata.visibility || (metadata.private ? "private" : "public"),
|
|
321
|
-
);
|
|
340
|
+
const visibilityClass = repositoryVisibility(metadata);
|
|
322
341
|
const repositoryIdentityRoot = githubRepositoryIdentityRoot({
|
|
323
342
|
provider: "github",
|
|
324
343
|
providerRepositoryId: String(metadata.node_id || metadata.id || ""),
|
|
@@ -420,7 +439,7 @@ export function collectGithubGovernanceAudit({
|
|
|
420
439
|
}
|
|
421
440
|
}
|
|
422
441
|
const visibility = selected.reduce((counts, item) => {
|
|
423
|
-
const key =
|
|
442
|
+
const key = repositoryVisibility(item);
|
|
424
443
|
counts[key] = Number(counts[key] || 0) + 1;
|
|
425
444
|
return counts;
|
|
426
445
|
}, {});
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import {
|
|
7
7
|
evaluateBuildchainReleaseReconciliation,
|
|
8
8
|
evaluatePublicationControlPlaneSnapshot,
|
|
9
|
+
matchesGithubDeploymentPolicy,
|
|
9
10
|
} from "../packages/core/publication-control-plane-audit.js";
|
|
10
11
|
|
|
11
12
|
function flag(name, fallback = "") {
|
|
@@ -197,8 +198,10 @@ function main() {
|
|
|
197
198
|
const requiredStatusCheck = flag("required-status-check", "check");
|
|
198
199
|
const jobId = flag("job", "promote");
|
|
199
200
|
const environment = flag("environment", "none");
|
|
200
|
-
const providerEnvironment = environment === "none" ? "" : environment;
|
|
201
201
|
const branch = flag("branch");
|
|
202
|
+
const environmentRef = flag("environment-ref", branch);
|
|
203
|
+
const environmentRefType = flag("environment-ref-type", "branch");
|
|
204
|
+
const providerEnvironment = environment === "none" ? "" : environment;
|
|
202
205
|
const sourceSha = flag("source-sha").toLowerCase();
|
|
203
206
|
const packageName = flag("package", "@kungfu-tech/buildchain");
|
|
204
207
|
const publisherMode = flag("publisher-mode", "npm-trusted-publisher");
|
|
@@ -239,15 +242,19 @@ function main() {
|
|
|
239
242
|
const deploymentBranches = environment !== "none" && environmentState.deployment_branch_policy?.custom_branch_policies === true
|
|
240
243
|
? githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}/deployment-branch-policies?per_page=100`, "Environment deployment branch policy")
|
|
241
244
|
: { branch_policies: [] };
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
if (!["branch", "tag"].includes(environmentRefType)) {
|
|
246
|
+
throw new Error(`unsupported --environment-ref-type: ${environmentRefType}`);
|
|
247
|
+
}
|
|
248
|
+
const matchingEnvironmentPolicy = (deploymentBranches.branch_policies || []).find((entry) =>
|
|
249
|
+
matchesGithubDeploymentPolicy(entry, { ref: environmentRef, refType: environmentRefType })
|
|
244
250
|
);
|
|
245
251
|
const environmentBranchAuthorized = environment !== "none" && (
|
|
246
252
|
(
|
|
253
|
+
environmentRefType === "branch" &&
|
|
247
254
|
environmentState.deployment_branch_policy?.protected_branches === true &&
|
|
248
255
|
branchState.protected === true
|
|
249
256
|
) ||
|
|
250
|
-
Boolean(
|
|
257
|
+
Boolean(matchingEnvironmentPolicy)
|
|
251
258
|
);
|
|
252
259
|
const oidc = githubJson(`repos/${repository}/actions/oidc/customization/sub`, "OIDC subject policy");
|
|
253
260
|
if (!["npm-trusted-publisher", "github-token", "oidc-role"].includes(publisherMode)) {
|
|
@@ -465,12 +472,14 @@ function main() {
|
|
|
465
472
|
environmentState.deployment_branch_policy?.protected_branches === true ||
|
|
466
473
|
(deploymentBranches.branch_policies || []).length > 0,
|
|
467
474
|
branchAuthorized: environmentBranchAuthorized,
|
|
468
|
-
branchPolicyMode: environmentState.deployment_branch_policy?.protected_branches === true
|
|
475
|
+
branchPolicyMode: environmentRefType === "branch" && environmentState.deployment_branch_policy?.protected_branches === true
|
|
469
476
|
? "protected-branches"
|
|
470
|
-
:
|
|
471
|
-
?
|
|
477
|
+
: matchingEnvironmentPolicy
|
|
478
|
+
? `custom-${environmentRefType}-policy`
|
|
472
479
|
: "unqualified",
|
|
473
|
-
authorizedBranch:
|
|
480
|
+
authorizedBranch: matchingEnvironmentPolicy?.name || "",
|
|
481
|
+
authorizedRef: environmentRef,
|
|
482
|
+
authorizedRefType: environmentRefType,
|
|
474
483
|
reviewRequired: reviewRules.length > 0,
|
|
475
484
|
preventSelfReview: reviewRules.some((rule) => rule.prevent_self_review === true),
|
|
476
485
|
},
|