@kungfu-tech/buildchain 2.12.7-alpha.2 → 2.12.7-alpha.21
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/promote-buildchain-ref/README.md +5 -1
- package/dist/site/buildchain-contract.json +53 -23
- package/dist/site/buildchain-site.json +16 -16
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +63 -9
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +10 -10
- package/dist/site/public-surface-audit.json +74 -10
- package/dist/site/publication-authority-registry.json +25 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +6 -6
- package/dist/site/workflow-registry.json +74 -7
- package/docs/publication-artifacts.md +30 -20
- package/docs/publication-authority.md +21 -9
- package/docs/release-candidate.md +19 -0
- package/docs/shifu-gate-profiles.md +6 -0
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/controller-evidence.js +1 -1
- package/packages/core/index.js +7 -0
- package/packages/core/publication-artifact-candidate.js +128 -0
- package/packages/core/publication-authority.js +41 -2
- package/packages/core/publication-control-plane-audit.js +3 -1
- package/scripts/assemble-publication-artifact-admission.mjs +190 -0
- package/scripts/assemble-self-publication-admission.mjs +19 -9
- package/scripts/audit-publication-control-plane.mjs +5 -2
- package/scripts/check-inventory.mjs +3 -0
- package/scripts/locked-source-checkout.mjs +22 -2
- package/scripts/publication-artifact-candidate.mjs +122 -0
- package/scripts/workflow-friction-report.mjs +13 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { validateControllerReceipt } from "./controller-evidence.js";
|
|
4
|
+
import { createPublicationArtifactCandidate } from "./publication-artifact-candidate.js";
|
|
4
5
|
import { sha256Json, validateReleaseCandidatePassport } from "./release-candidate.js";
|
|
5
6
|
|
|
6
7
|
export const PUBLICATION_AUTHORITY_REGISTRY_CONTRACT =
|
|
@@ -381,6 +382,7 @@ export function createPublicationArtifactManifestSet({
|
|
|
381
382
|
}
|
|
382
383
|
|
|
383
384
|
function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
385
|
+
const passportSourceSha = passport.source?.headSha || passport.source?.sha;
|
|
384
386
|
if (gateAggregate?.contract === "buildchain.shifu-gate-aggregate/v1") {
|
|
385
387
|
const { digest, ...payload } = gateAggregate;
|
|
386
388
|
const actualDigest = sha256Json(payload);
|
|
@@ -390,7 +392,7 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
390
392
|
if (gateAggregate.status !== "pass" || gateAggregate.qualifying !== true) {
|
|
391
393
|
throw new Error("gate aggregate is not qualifying");
|
|
392
394
|
}
|
|
393
|
-
if (![admission.sourceSha,
|
|
395
|
+
if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
|
|
394
396
|
throw new Error("gate aggregate source SHA mismatch");
|
|
395
397
|
}
|
|
396
398
|
normalizeDigest(gateAggregate.registry?.digest, "gateAggregate.registry.digest");
|
|
@@ -408,7 +410,7 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
408
410
|
if (gateAggregate.required !== false) {
|
|
409
411
|
throw new Error("a required Gate policy must supply a qualifying Shifu Gate aggregate");
|
|
410
412
|
}
|
|
411
|
-
if (![admission.sourceSha,
|
|
413
|
+
if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
|
|
412
414
|
throw new Error("publication Gate decision source SHA mismatch");
|
|
413
415
|
}
|
|
414
416
|
const normalizedPolicy = {
|
|
@@ -428,6 +430,43 @@ function validatePublicationEvidence(publicationEvidence, admission) {
|
|
|
428
430
|
if (!publicationEvidence || typeof publicationEvidence !== "object" || Array.isArray(publicationEvidence)) {
|
|
429
431
|
throw new Error("independent publication evidence is required");
|
|
430
432
|
}
|
|
433
|
+
if (publicationEvidence.publicationArtifactCandidate) {
|
|
434
|
+
const candidate = createPublicationArtifactCandidate(publicationEvidence.publicationArtifactCandidate);
|
|
435
|
+
const passport = publicationEvidence.publicationArtifactCandidate.passport;
|
|
436
|
+
const controllerReceipt = publicationEvidence.publicationArtifactCandidate.controllerReceipt;
|
|
437
|
+
const controllerReceiptDigest = normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest");
|
|
438
|
+
if (controllerReceiptDigest !== normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest")) {
|
|
439
|
+
throw new Error("controller receipt evidence binding mismatch");
|
|
440
|
+
}
|
|
441
|
+
const contractDigest = normalizeDigest(controllerReceipt.runtime?.contractDigest, "controllerReceipt.runtime.contractDigest");
|
|
442
|
+
if (contractDigest !== normalizeDigest(admission.contractDigest, "admission.contractDigest")) {
|
|
443
|
+
throw new Error("runtime contract evidence binding mismatch");
|
|
444
|
+
}
|
|
445
|
+
const gate = validateGateAggregate(publicationEvidence.gateAggregate, { admission, passport });
|
|
446
|
+
if (gate.gateAggregateDigest !== normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest")) {
|
|
447
|
+
throw new Error("Gate aggregate evidence binding mismatch");
|
|
448
|
+
}
|
|
449
|
+
if (gate.policyDigest !== normalizeDigest(admission.policyDigest, "admission.policyDigest")) {
|
|
450
|
+
throw new Error("Gate policy evidence binding mismatch");
|
|
451
|
+
}
|
|
452
|
+
if (candidate.candidateDigest !== normalizeDigest(admission.artifactDigest, "admission.artifactDigest")) {
|
|
453
|
+
throw new Error("publication artifact candidate evidence binding mismatch");
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
sourceTreeSha: candidate.sourceTreeSha,
|
|
457
|
+
controllerReceiptDigest,
|
|
458
|
+
contractDigest,
|
|
459
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
460
|
+
policyDigest: gate.policyDigest,
|
|
461
|
+
artifactDigest: candidate.candidateDigest,
|
|
462
|
+
evidenceDigest: publicationAuthorityDigest({
|
|
463
|
+
sourceTreeSha: candidate.sourceTreeSha,
|
|
464
|
+
controllerReceiptDigest,
|
|
465
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
466
|
+
artifactDigest: candidate.candidateDigest,
|
|
467
|
+
}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
431
470
|
const passport = publicationEvidence.releaseCandidatePassport;
|
|
432
471
|
const buildSummary = publicationEvidence.buildSummary;
|
|
433
472
|
const passportValidation = validateReleaseCandidatePassport({
|
|
@@ -16,6 +16,7 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
16
16
|
branch,
|
|
17
17
|
packageName,
|
|
18
18
|
publisherMode = "npm-trusted-publisher",
|
|
19
|
+
requiredStatusCheck = "check",
|
|
19
20
|
snapshot,
|
|
20
21
|
observedAt,
|
|
21
22
|
expiresAt,
|
|
@@ -72,7 +73,8 @@ export function evaluatePublicationControlPlaneSnapshot({
|
|
|
72
73
|
branchPolicy.protected === true &&
|
|
73
74
|
branchPolicy.enforcementLevel === "everyone" &&
|
|
74
75
|
Array.isArray(branchPolicy.requiredStatusChecks) &&
|
|
75
|
-
branchPolicy.
|
|
76
|
+
branchPolicy.requiredStatusCheck === requiredStatusCheck &&
|
|
77
|
+
branchPolicy.requiredStatusChecks.includes(requiredStatusCheck) &&
|
|
76
78
|
branchPolicy.requiredCheckPassed === true &&
|
|
77
79
|
branchPolicy.sourceSha === branchPolicy.headSha &&
|
|
78
80
|
branchPolicy.mergedPullRequest === true &&
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createPublicationAdmission,
|
|
9
|
+
createPublicationGateDecision,
|
|
10
|
+
createRunnerProvenance,
|
|
11
|
+
} from "../packages/core/publication-authority.js";
|
|
12
|
+
import { buildPublicationArtifactCandidate } from "./publication-artifact-candidate.mjs";
|
|
13
|
+
|
|
14
|
+
function required(name) {
|
|
15
|
+
const value = String(process.env[name] || "").trim();
|
|
16
|
+
if (!value) throw new Error(`${name} is required`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sha256(value) {
|
|
21
|
+
return crypto.createHash("sha256").update(String(value)).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function sourceTree(repository, sourceSha, token) {
|
|
25
|
+
const response = await fetch(
|
|
26
|
+
`${required("GITHUB_API_URL")}/repos/${repository}/git/commits/${sourceSha}`,
|
|
27
|
+
{
|
|
28
|
+
headers: {
|
|
29
|
+
accept: "application/vnd.github+json",
|
|
30
|
+
authorization: `Bearer ${token}`,
|
|
31
|
+
"x-github-api-version": "2022-11-28",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(
|
|
37
|
+
`could not resolve admitted source tree: GitHub API ${response.status}`,
|
|
38
|
+
);
|
|
39
|
+
const commit = await response.json();
|
|
40
|
+
return String(commit.tree?.sha || "").toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeBundle(outputDir, values) {
|
|
44
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
+
for (const [name, value] of Object.entries(values)) {
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(outputDir, `${name}.json`),
|
|
48
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
52
|
+
const output = fs.createWriteStream(process.env.GITHUB_OUTPUT, {
|
|
53
|
+
flags: "a",
|
|
54
|
+
});
|
|
55
|
+
for (const [name, value] of Object.entries(values)) {
|
|
56
|
+
if (name === "candidate") continue;
|
|
57
|
+
output.write(
|
|
58
|
+
`${name.replaceAll("_", "-")}-json=${JSON.stringify(value)}\n`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
output.end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
const repository = required("BUILDCHAIN_REPOSITORY");
|
|
67
|
+
const sourceSha = required("BUILDCHAIN_SOURCE_SHA").toLowerCase();
|
|
68
|
+
const runtimeRoot =
|
|
69
|
+
process.env.BUILDCHAIN_RUNTIME_ROOT || ".buildchain/authority-runtime";
|
|
70
|
+
const runtimeSha = execFileSync(
|
|
71
|
+
"git",
|
|
72
|
+
["-C", runtimeRoot, "rev-parse", "HEAD"],
|
|
73
|
+
{ encoding: "utf8" },
|
|
74
|
+
)
|
|
75
|
+
.trim()
|
|
76
|
+
.toLowerCase();
|
|
77
|
+
const token = required("GITHUB_TOKEN");
|
|
78
|
+
const treeSha = await sourceTree(repository, sourceSha, token);
|
|
79
|
+
const candidateBundle = buildPublicationArtifactCandidate({
|
|
80
|
+
artifactRoot:
|
|
81
|
+
process.env.BUILDCHAIN_ARTIFACT_ROOT ||
|
|
82
|
+
".buildchain/publication-evidence/artifact",
|
|
83
|
+
controllerRoot:
|
|
84
|
+
process.env.BUILDCHAIN_CONTROLLER_ROOT ||
|
|
85
|
+
".buildchain/publication-evidence/controller",
|
|
86
|
+
repository,
|
|
87
|
+
sourceSha,
|
|
88
|
+
sourceTreeSha: treeSha,
|
|
89
|
+
runtimeSha,
|
|
90
|
+
});
|
|
91
|
+
const controllerReceipt = candidateBundle.evidence.controllerReceipt;
|
|
92
|
+
const controlPlaneAudit = JSON.parse(
|
|
93
|
+
fs.readFileSync(required("BUILDCHAIN_CONTROL_PLANE_AUDIT_PATH"), "utf8"),
|
|
94
|
+
);
|
|
95
|
+
const registry = JSON.parse(
|
|
96
|
+
fs.readFileSync(
|
|
97
|
+
path.join(runtimeRoot, "dist/site/publication-authority-registry.json"),
|
|
98
|
+
"utf8",
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const gateAggregate = createPublicationGateDecision({
|
|
102
|
+
sourceSha,
|
|
103
|
+
profile: process.env.BUILDCHAIN_GATE_PROFILE || "managed-paper-publication",
|
|
104
|
+
required: false,
|
|
105
|
+
rationale:
|
|
106
|
+
process.env.BUILDCHAIN_GATE_RATIONALE ||
|
|
107
|
+
"The managed paper repository declares no project-specific Shifu Gate registry.",
|
|
108
|
+
policy: { scope: "managed-paper-publication", repository },
|
|
109
|
+
});
|
|
110
|
+
const runnerProvenance = createRunnerProvenance({
|
|
111
|
+
runnerClass: "ephemeral",
|
|
112
|
+
os: required("RUNNER_OS"),
|
|
113
|
+
architecture: required("RUNNER_ARCH"),
|
|
114
|
+
imageDigest: sha256(
|
|
115
|
+
`${process.env.ImageOS || "unknown"}|${process.env.ImageVersion || "unknown"}`,
|
|
116
|
+
),
|
|
117
|
+
measurementDigest: sha256(
|
|
118
|
+
[
|
|
119
|
+
process.env.GITHUB_WORKFLOW,
|
|
120
|
+
process.env.GITHUB_JOB,
|
|
121
|
+
process.env.GITHUB_RUN_ID,
|
|
122
|
+
process.env.GITHUB_RUN_ATTEMPT,
|
|
123
|
+
process.env.RUNNER_ENVIRONMENT,
|
|
124
|
+
].join("|"),
|
|
125
|
+
),
|
|
126
|
+
isolation: "github-hosted-single-job",
|
|
127
|
+
});
|
|
128
|
+
const issuedAt = new Date();
|
|
129
|
+
const admission = createPublicationAdmission({
|
|
130
|
+
registryDigest: registry.registryDigest,
|
|
131
|
+
workflowPath:
|
|
132
|
+
process.env.BUILDCHAIN_AUTHORITY_WORKFLOW_PATH ||
|
|
133
|
+
".github/workflows/paper-release-sealed.yml",
|
|
134
|
+
publisherWorkflowPath: required("BUILDCHAIN_PUBLISHER_WORKFLOW_PATH"),
|
|
135
|
+
repository,
|
|
136
|
+
sourceSha,
|
|
137
|
+
runtimeSha,
|
|
138
|
+
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
139
|
+
policyDigest: gateAggregate.policyDigest,
|
|
140
|
+
controllerReceiptDigest: controllerReceipt.digest,
|
|
141
|
+
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
142
|
+
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
143
|
+
gateAggregateDigest: gateAggregate.digest,
|
|
144
|
+
environment: "none",
|
|
145
|
+
product: required("BUILDCHAIN_PUBLICATION_PRODUCT"),
|
|
146
|
+
target: required("BUILDCHAIN_PUBLICATION_TARGET"),
|
|
147
|
+
version: required("BUILDCHAIN_PUBLICATION_VERSION"),
|
|
148
|
+
channel: required("BUILDCHAIN_PUBLICATION_CHANNEL"),
|
|
149
|
+
artifactDigest: candidateBundle.candidate.candidateDigest,
|
|
150
|
+
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}:paper`,
|
|
151
|
+
issuedAt: issuedAt.toISOString(),
|
|
152
|
+
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
153
|
+
});
|
|
154
|
+
const bindingNames = [
|
|
155
|
+
"repository",
|
|
156
|
+
"publisherWorkflowPath",
|
|
157
|
+
"sourceSha",
|
|
158
|
+
"runtimeSha",
|
|
159
|
+
"contractDigest",
|
|
160
|
+
"policyDigest",
|
|
161
|
+
"controllerReceiptDigest",
|
|
162
|
+
"gateAggregateDigest",
|
|
163
|
+
"environment",
|
|
164
|
+
"product",
|
|
165
|
+
"target",
|
|
166
|
+
"version",
|
|
167
|
+
"channel",
|
|
168
|
+
"artifactDigest",
|
|
169
|
+
];
|
|
170
|
+
const expected = Object.fromEntries(
|
|
171
|
+
bindingNames.map((name) => [name, admission[name]]),
|
|
172
|
+
);
|
|
173
|
+
writeBundle(
|
|
174
|
+
process.env.BUILDCHAIN_OUTPUT_DIR ||
|
|
175
|
+
".buildchain/publication-authority/auto",
|
|
176
|
+
{
|
|
177
|
+
admission,
|
|
178
|
+
runner_provenance: runnerProvenance,
|
|
179
|
+
control_plane_audit: controlPlaneAudit,
|
|
180
|
+
gate_aggregate: gateAggregate,
|
|
181
|
+
expected,
|
|
182
|
+
candidate: candidateBundle.candidate,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch((error) => {
|
|
188
|
+
console.error(`assemble publication artifact admission: ${error.message}`);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
});
|
|
@@ -115,13 +115,22 @@ async function main() {
|
|
|
115
115
|
manifests,
|
|
116
116
|
payloads: manifests.map((manifest) => payloadFor(manifest, path.join(evidenceRoot, "payloads"))),
|
|
117
117
|
});
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
const suppliedGateAggregate = String(process.env.BUILDCHAIN_GATE_AGGREGATE_JSON || "").trim();
|
|
119
|
+
let gateAggregate;
|
|
120
|
+
if (suppliedGateAggregate) {
|
|
121
|
+
gateAggregate = JSON.parse(suppliedGateAggregate);
|
|
122
|
+
} else {
|
|
123
|
+
if (process.env.BUILDCHAIN_ALLOW_NO_GATE !== "true") {
|
|
124
|
+
throw new Error("managed release-candidate admission requires a Gate aggregate or explicit no-Gate decision");
|
|
125
|
+
}
|
|
126
|
+
gateAggregate = createPublicationGateDecision({
|
|
127
|
+
sourceSha,
|
|
128
|
+
profile: process.env.BUILDCHAIN_GATE_PROFILE || "managed-release-candidate-no-gate",
|
|
129
|
+
required: false,
|
|
130
|
+
rationale: process.env.BUILDCHAIN_GATE_RATIONALE || "The consumer explicitly declared no Shifu Gate registry for this publication transaction.",
|
|
131
|
+
policy: { scope: "managed-release-candidate", repository },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
125
134
|
const runnerProvenance = createRunnerProvenance({
|
|
126
135
|
runnerClass: "ephemeral",
|
|
127
136
|
os: required("RUNNER_OS"),
|
|
@@ -137,6 +146,7 @@ async function main() {
|
|
|
137
146
|
isolation: "github-hosted-single-job",
|
|
138
147
|
});
|
|
139
148
|
const packageJson = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "package.json"), "utf8"));
|
|
149
|
+
const publicationVersion = required("BUILDCHAIN_PUBLICATION_VERSION");
|
|
140
150
|
const issuedAt = new Date();
|
|
141
151
|
const admission = createPublicationAdmission({
|
|
142
152
|
registryDigest: registry.registryDigest,
|
|
@@ -154,7 +164,7 @@ async function main() {
|
|
|
154
164
|
environment: process.env.BUILDCHAIN_PUBLICATION_ENVIRONMENT || "none",
|
|
155
165
|
product: process.env.BUILDCHAIN_PUBLICATION_PRODUCT || "Buildchain",
|
|
156
166
|
target: process.env.BUILDCHAIN_PUBLICATION_TARGET || `npm:${packageJson.name}`,
|
|
157
|
-
version:
|
|
167
|
+
version: publicationVersion,
|
|
158
168
|
channel: required("BUILDCHAIN_PUBLICATION_CHANNEL"),
|
|
159
169
|
artifactDigest: artifactSet.manifestSetDigest,
|
|
160
170
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
|
|
@@ -177,6 +187,6 @@ async function main() {
|
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
main().catch((error) => {
|
|
180
|
-
console.error(`assemble
|
|
190
|
+
console.error(`assemble release-candidate admission: ${error.message}`);
|
|
181
191
|
process.exitCode = 1;
|
|
182
192
|
});
|
|
@@ -191,6 +191,7 @@ function main() {
|
|
|
191
191
|
const workflowPath = flag("workflow", ".github/workflows/release-candidate-promote.yml");
|
|
192
192
|
const workflowRef = flag("workflow-ref");
|
|
193
193
|
const publisherWorkflowPath = flag("publisher-workflow", workflowPath);
|
|
194
|
+
const requiredStatusCheck = flag("required-status-check", "check");
|
|
194
195
|
const jobId = flag("job", "promote");
|
|
195
196
|
const environment = flag("environment", "none");
|
|
196
197
|
const providerEnvironment = environment === "none" ? "" : environment;
|
|
@@ -280,15 +281,16 @@ function main() {
|
|
|
280
281
|
const checkRuns = githubJson(`repos/${repository}/commits/${sourceSha}/check-runs?per_page=100`, "source check runs");
|
|
281
282
|
const requiredStatusCheckPolicy = branchState.protection?.required_status_checks || {};
|
|
282
283
|
const requiredStatusChecks = requiredStatusCheckPolicy.contexts || [];
|
|
283
|
-
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) => entry.context ===
|
|
284
|
+
const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) => entry.context === requiredStatusCheck);
|
|
284
285
|
branchPolicy = {
|
|
285
286
|
ref: branch,
|
|
286
287
|
policyMode: "provider-enforced-transaction",
|
|
287
288
|
protected: branchState.protected === true,
|
|
288
289
|
enforcementLevel: branchState.protection?.required_status_checks?.enforcement_level || "",
|
|
289
290
|
requiredStatusChecks,
|
|
291
|
+
requiredStatusCheck,
|
|
290
292
|
requiredCheckPassed: (checkRuns.check_runs || []).some((entry) =>
|
|
291
|
-
entry.name ===
|
|
293
|
+
entry.name === requiredStatusCheck &&
|
|
292
294
|
entry.conclusion === "success" &&
|
|
293
295
|
(!requiredCheckSource?.app_id || entry.app?.id === requiredCheckSource.app_id)
|
|
294
296
|
),
|
|
@@ -350,6 +352,7 @@ function main() {
|
|
|
350
352
|
branch,
|
|
351
353
|
packageName,
|
|
352
354
|
publisherMode,
|
|
355
|
+
requiredStatusCheck,
|
|
353
356
|
observedAt: observedAt.toISOString(),
|
|
354
357
|
expiresAt: expiresAt.toISOString(),
|
|
355
358
|
snapshot: {
|
|
@@ -840,6 +840,9 @@ for (const requiredSnippet of [
|
|
|
840
840
|
"publish-source-ref: ${{ steps.publish-gate.outputs.ref }}",
|
|
841
841
|
"publish-source-sha: ${{ steps.publish-gate.outputs.sha }}",
|
|
842
842
|
"publish-source-locked: ${{ steps.publish-gate.outputs.locked }}",
|
|
843
|
+
"expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
844
|
+
"publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
845
|
+
"name: Plan exact publication version",
|
|
843
846
|
"DRY_RUN: ${{ inputs.dry-run }}",
|
|
844
847
|
"if (dryRun) {",
|
|
845
848
|
"Enforce Buildchain stable release canary gate",
|
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
|
|
9
|
+
export const ISOLATED_GIT_GLOBAL_CONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
9
10
|
|
|
10
11
|
const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
11
12
|
|
|
@@ -133,6 +134,24 @@ function githubAuthEnv(token = "") {
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
function isolatedGitFetchEnv(env = {}, targetPath) {
|
|
138
|
+
const configuredCount = Number.parseInt(env.GIT_CONFIG_COUNT || "0", 10);
|
|
139
|
+
const safeDirectoryIndex = Number.isInteger(configuredCount) && configuredCount >= 0
|
|
140
|
+
? configuredCount
|
|
141
|
+
: 0;
|
|
142
|
+
return {
|
|
143
|
+
...env,
|
|
144
|
+
// Runner-global URL rewrites are shared mutable state. A concurrent job
|
|
145
|
+
// may point the same repository URL at a different single-SHA bundle, so
|
|
146
|
+
// network fetches must not consult the account-level Git config.
|
|
147
|
+
GIT_CONFIG_GLOBAL: ISOLATED_GIT_GLOBAL_CONFIG,
|
|
148
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
149
|
+
GIT_CONFIG_COUNT: String(safeDirectoryIndex + 1),
|
|
150
|
+
[`GIT_CONFIG_KEY_${safeDirectoryIndex}`]: "safe.directory",
|
|
151
|
+
[`GIT_CONFIG_VALUE_${safeDirectoryIndex}`]: path.resolve(targetPath),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
136
155
|
function markSafeDirectory(targetPath, timeoutMs) {
|
|
137
156
|
try {
|
|
138
157
|
git(["config", "--global", "--add", "safe.directory", path.resolve(targetPath)], {
|
|
@@ -206,6 +225,7 @@ export function fetchSourceCommit({
|
|
|
206
225
|
runGit = git,
|
|
207
226
|
containsCommit = hasCommit,
|
|
208
227
|
}) {
|
|
228
|
+
const fetchEnv = isolatedGitFetchEnv(env, targetPath);
|
|
209
229
|
try {
|
|
210
230
|
runGit(["remote", "remove", remoteName], { cwd: targetPath, timeoutMs, stdio: "ignore" });
|
|
211
231
|
} catch {
|
|
@@ -222,7 +242,7 @@ export function fetchSourceCommit({
|
|
|
222
242
|
runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${fetchRef}:refs/buildchain/source-ref`], {
|
|
223
243
|
cwd: targetPath,
|
|
224
244
|
timeoutMs,
|
|
225
|
-
env,
|
|
245
|
+
env: fetchEnv,
|
|
226
246
|
});
|
|
227
247
|
if (containsCommit(targetPath, sha, timeoutMs)) {
|
|
228
248
|
return { selector: "ref", checkoutSha: sha };
|
|
@@ -252,7 +272,7 @@ export function fetchSourceCommit({
|
|
|
252
272
|
runGit(["fetch", "--no-tags", "--depth=1", remoteName, `+${sha}:refs/buildchain/source`], {
|
|
253
273
|
cwd: targetPath,
|
|
254
274
|
timeoutMs,
|
|
255
|
-
env,
|
|
275
|
+
env: fetchEnv,
|
|
256
276
|
});
|
|
257
277
|
if (!containsCommit(targetPath, sha, timeoutMs)) {
|
|
258
278
|
throw new Error(`fetched ${fetchRef || sha}, but ${sha} is not available`);
|
|
@@ -0,0 +1,122 @@
|
|
|
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 { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createPublicationArtifactCandidate,
|
|
9
|
+
resolvePublicationCandidateFile,
|
|
10
|
+
} from "../packages/core/publication-artifact-candidate.js";
|
|
11
|
+
|
|
12
|
+
export { resolvePublicationCandidateFile };
|
|
13
|
+
|
|
14
|
+
function flag(name, fallback = "") {
|
|
15
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
16
|
+
return index === -1 ? fallback : String(process.argv[index + 1] || "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requiredFlag(name) {
|
|
20
|
+
const value = flag(name).trim();
|
|
21
|
+
if (!value) throw new Error(`--${name} is required`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function exactJson(root, relativePath) {
|
|
26
|
+
const absoluteRoot = path.resolve(root);
|
|
27
|
+
const absolutePath = path.resolve(absoluteRoot, relativePath);
|
|
28
|
+
if (!absolutePath.startsWith(`${absoluteRoot}${path.sep}`)) {
|
|
29
|
+
throw new Error(`publication evidence path escapes artifact root: ${relativePath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
32
|
+
throw new Error(`expected publication evidence at ${relativePath}`);
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectFiles(root) {
|
|
38
|
+
const absoluteRoot = path.resolve(root);
|
|
39
|
+
const files = [];
|
|
40
|
+
const pending = [absoluteRoot];
|
|
41
|
+
while (pending.length > 0) {
|
|
42
|
+
const current = pending.pop();
|
|
43
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
44
|
+
const full = path.join(current, entry.name);
|
|
45
|
+
if (entry.isDirectory()) pending.push(full);
|
|
46
|
+
else if (entry.isFile()) {
|
|
47
|
+
files.push({
|
|
48
|
+
path: path.relative(absoluteRoot, full).split(path.sep).join("/"),
|
|
49
|
+
size: fs.statSync(full).size,
|
|
50
|
+
sha256: crypto
|
|
51
|
+
.createHash("sha256")
|
|
52
|
+
.update(fs.readFileSync(full))
|
|
53
|
+
.digest("hex"),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPublicationArtifactCandidate({
|
|
62
|
+
artifactRoot,
|
|
63
|
+
controllerRoot,
|
|
64
|
+
repository,
|
|
65
|
+
sourceSha,
|
|
66
|
+
sourceTreeSha,
|
|
67
|
+
runtimeSha,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const resolvedArtifactRoot = path.resolve(artifactRoot);
|
|
70
|
+
const resolvedControllerRoot = path.resolve(controllerRoot);
|
|
71
|
+
const evidence = {
|
|
72
|
+
repository,
|
|
73
|
+
sourceSha,
|
|
74
|
+
sourceTreeSha,
|
|
75
|
+
runtimeSha,
|
|
76
|
+
manifest: exactJson(
|
|
77
|
+
resolvedArtifactRoot,
|
|
78
|
+
".buildchain/publication/publication-artifact.json",
|
|
79
|
+
),
|
|
80
|
+
passport: exactJson(
|
|
81
|
+
resolvedArtifactRoot,
|
|
82
|
+
".buildchain/publication/publication-artifact-passport.json",
|
|
83
|
+
),
|
|
84
|
+
controllerReceipt: exactJson(resolvedControllerRoot, "receipt.json"),
|
|
85
|
+
files: collectFiles(resolvedArtifactRoot),
|
|
86
|
+
};
|
|
87
|
+
const candidate = createPublicationArtifactCandidate(evidence);
|
|
88
|
+
return { schemaVersion: 1, candidate, evidence };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function main() {
|
|
92
|
+
const result = buildPublicationArtifactCandidate({
|
|
93
|
+
artifactRoot: requiredFlag("artifact-root"),
|
|
94
|
+
controllerRoot: requiredFlag("controller-root"),
|
|
95
|
+
repository: requiredFlag("repository"),
|
|
96
|
+
sourceSha: requiredFlag("source-sha"),
|
|
97
|
+
sourceTreeSha: requiredFlag("source-tree-sha"),
|
|
98
|
+
runtimeSha: requiredFlag("runtime-sha"),
|
|
99
|
+
});
|
|
100
|
+
const output = flag("output");
|
|
101
|
+
if (output) {
|
|
102
|
+
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.resolve(output),
|
|
105
|
+
`${JSON.stringify(result, null, 2)}\n`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (process.argv.includes("--json") || !output)
|
|
109
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
process.argv[1] &&
|
|
114
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
115
|
+
) {
|
|
116
|
+
try {
|
|
117
|
+
main();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`publication artifact candidate: ${error.message}`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -202,6 +202,8 @@ export async function classifyWorkflowFriction({
|
|
|
202
202
|
buildWorkflowName = DEFAULT_BUILD_WORKFLOW_NAME,
|
|
203
203
|
releaseCandidateOutcome = env("BUILDCHAIN_RC_RESOLVE_OUTCOME"),
|
|
204
204
|
releaseCandidateDiagnosis = env("BUILDCHAIN_RC_DIAGNOSIS"),
|
|
205
|
+
promotionOutcome = env("BUILDCHAIN_PROMOTION_OUTCOME"),
|
|
206
|
+
promotionDiagnosis = env("BUILDCHAIN_PROMOTION_DIAGNOSIS"),
|
|
205
207
|
runUrl = env("BUILDCHAIN_WORKFLOW_RUN_URL"),
|
|
206
208
|
outputDir = ".buildchain/workflow-friction",
|
|
207
209
|
fetchImpl = globalThis.fetch,
|
|
@@ -256,14 +258,23 @@ export async function classifyWorkflowFriction({
|
|
|
256
258
|
if (releaseCandidateOutcome === "failure") {
|
|
257
259
|
diagnosisParts.push("Promotion reached the post-Verify workflow before required PR-stage RC evidence could be resolved.");
|
|
258
260
|
}
|
|
259
|
-
if (releaseCandidateDiagnosis) {
|
|
261
|
+
if (releaseCandidateOutcome === "failure" && releaseCandidateDiagnosis) {
|
|
260
262
|
diagnosisParts.push(releaseCandidateDiagnosis);
|
|
261
263
|
}
|
|
264
|
+
if (promotionOutcome === "failure") {
|
|
265
|
+
diagnosisParts.push(
|
|
266
|
+
promotionDiagnosis
|
|
267
|
+
? `Promotion failed: ${promotionDiagnosis}`
|
|
268
|
+
: "Promotion failed after PR-stage release-candidate evidence resolved successfully.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
262
271
|
diagnosisParts.push(...workflowRunDiagnostics);
|
|
263
272
|
const diagnosis = diagnosisParts.join(" ") || "Buildchain ref promotion failed after Verify succeeded; inspect the classified evidence and keep the fix in Buildchain.";
|
|
264
273
|
const nextAction = frictionClass === "late-fail-fast"
|
|
265
274
|
? "Move the missing/stale RC evidence check earlier or make the promotion workflow consume the exact PR-stage RC passport before any publish side effect."
|
|
266
|
-
: "
|
|
275
|
+
: ["duplicate-channel-pr", "duplicate-heavy-build"].includes(frictionClass)
|
|
276
|
+
? "Deduplicate the PR/build path or tighten Buildchain workflow gates so the next channel promotion reaches publish exactly once."
|
|
277
|
+
: "Fix the concrete promotion failure above, then rerun through the protected channel workflow; do not treat successful RC resolution as the failure diagnosis.";
|
|
267
278
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
268
279
|
const bodyFile = path.join(outputDir, "issue-body.md");
|
|
269
280
|
const body = buildWorkflowFrictionBody({
|