@kungfu-tech/buildchain 3.0.1-alpha.2 → 3.0.1-alpha.4
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/AGENTS.md +5 -5
- package/README.md +15 -17
- package/actions/promote-buildchain-ref/README.md +8 -8
- package/actions/report-buildchain-issue/README.md +2 -2
- package/actions/run-lifecycle/README.md +1 -1
- package/actions/validate-config/README.md +1 -1
- package/bin/buildchain.mjs +11 -4
- package/dist/site/buildchain-contract.json +59 -24
- package/dist/site/buildchain-site.json +78 -83
- package/dist/site/controller-registry.json +30 -2
- package/dist/site/kfd-claims.json +12 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +18 -18
- package/dist/site/node-api-registry.json +5 -5
- package/dist/site/page-registry.json +54 -59
- package/dist/site/public-surface-audit.json +11 -9
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -2
- package/dist/site/site-manifest.json +22 -22
- package/dist/site/workflow-registry.json +8 -1
- package/docs/MAP.md +5 -5
- package/docs/cli.md +20 -14
- package/docs/consumer-issue-reporting.md +1 -1
- package/docs/homebrew.md +7 -5
- package/docs/install.md +7 -3
- package/docs/kfd-agent-hub.md +1 -1
- package/docs/lifecycle-protocol.md +3 -3
- package/docs/migration-inventory.md +4 -4
- package/docs/ownership.md +4 -4
- package/docs/publication-artifacts.md +2 -2
- package/docs/release-candidate.md +4 -4
- package/docs/release-governance.md +13 -13
- package/docs/release-passport.md +6 -6
- package/docs/release-propagation.md +5 -5
- package/docs/reusable-build-surface.md +23 -23
- package/docs/runtime-train-validation.md +13 -13
- package/docs/shifu-gate-profiles.md +2 -2
- package/docs/site-bundle-contract.md +1 -1
- package/docs/stable-candidate-patrol.md +2 -2
- package/docs/toolkit-observability.md +14 -0
- package/docs/web-surface-deployments.md +7 -7
- package/fixtures/libnode-shaped/README.md +1 -1
- package/package.json +1 -1
- package/packages/core/issue-reporting.js +8 -3
- package/packages/core/logging.js +101 -0
- package/scripts/auditable-demo.mjs +1 -1
- package/scripts/check-inventory.mjs +36 -17
- package/scripts/init-repo.mjs +2 -2
- package/scripts/web-surface-production-release-pr.mjs +99 -0
- package/scripts/workflow-friction-report.mjs +10 -0
|
@@ -214,9 +214,6 @@ export function buildWorkflowFrictionIssueReport(options = {}) {
|
|
|
214
214
|
repository,
|
|
215
215
|
workflow,
|
|
216
216
|
channel,
|
|
217
|
-
releaseIntent,
|
|
218
|
-
sourceRef,
|
|
219
|
-
sourceSha,
|
|
220
217
|
frictionClass,
|
|
221
218
|
});
|
|
222
219
|
const marker = workflowFrictionMarker(fingerprint);
|
|
@@ -265,6 +262,10 @@ export function buildWorkflowFrictionIssueReport(options = {}) {
|
|
|
265
262
|
`- Workflow: ${workflow || "(unknown)"}`,
|
|
266
263
|
`- Run: ${runUrl || runId || "(unknown)"}`,
|
|
267
264
|
`- Attempt: ${runAttempt || "(unknown)"}`,
|
|
265
|
+
`- Channel: ${channel || "(unknown)"}`,
|
|
266
|
+
`- Release intent: ${releaseIntent || "(unknown)"}`,
|
|
267
|
+
`- Source ref: ${sourceRef || "(unknown)"}`,
|
|
268
|
+
`- Source SHA: ${sourceSha || "(unknown)"}`,
|
|
268
269
|
`- Friction class: ${frictionClass}`,
|
|
269
270
|
`- Fingerprint: ${fingerprint}`,
|
|
270
271
|
options.summary ? `\n${options.summary}` : "",
|
|
@@ -275,6 +276,10 @@ export function buildWorkflowFrictionIssueReport(options = {}) {
|
|
|
275
276
|
return {
|
|
276
277
|
contract: BUILDCHAIN_WORKFLOW_FRICTION_ISSUE_CONTRACT,
|
|
277
278
|
targetRepository: target.fullName,
|
|
279
|
+
repository,
|
|
280
|
+
workflow,
|
|
281
|
+
channel,
|
|
282
|
+
frictionClass,
|
|
278
283
|
fingerprint,
|
|
279
284
|
marker,
|
|
280
285
|
title,
|
package/packages/core/logging.js
CHANGED
|
@@ -5,6 +5,13 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
export const BUILDCHAIN_LOG_EVENT_CONTRACT = "kungfu-buildchain-log-event";
|
|
7
7
|
export const BUILDCHAIN_LOG_SUMMARY_CONTRACT = "kungfu-buildchain-log-summary";
|
|
8
|
+
export const BUILDCHAIN_CONTROL_PLANE_SUMMARY_CONTRACT =
|
|
9
|
+
"kungfu-buildchain-control-plane-summary";
|
|
10
|
+
|
|
11
|
+
const CONTROL_PLANE_EVENT_NAMES = {
|
|
12
|
+
"workflow-friction": "control-plane.workflow-friction.outcome",
|
|
13
|
+
"release-intent": "control-plane.release-intent.outcome",
|
|
14
|
+
};
|
|
8
15
|
|
|
9
16
|
const SECRET_KEY_PATTERN =
|
|
10
17
|
/(authorization|cookie|credential|password|passwd|private[_-]?key|secret|token|api[_-]?key)/i;
|
|
@@ -90,6 +97,98 @@ function groupSummary(events, field) {
|
|
|
90
97
|
);
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
function countOutcomes(events) {
|
|
101
|
+
const counts = {};
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
const outcome = String(event.attributes?.outcome || "unknown");
|
|
104
|
+
counts[outcome] = (counts[outcome] || 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ratio(numerator, denominator) {
|
|
112
|
+
return denominator > 0 ? Number((numerator / denominator).toFixed(4)) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function summarizeBuildchainControlPlaneEvents(input = {}) {
|
|
116
|
+
const events = Array.isArray(input)
|
|
117
|
+
? input
|
|
118
|
+
: typeof input === "string"
|
|
119
|
+
? readBuildchainLogEvents(input)
|
|
120
|
+
: readBuildchainLogEvents(input.path);
|
|
121
|
+
const workflowFrictionEvents = events.filter(
|
|
122
|
+
(event) => event.event === CONTROL_PLANE_EVENT_NAMES["workflow-friction"],
|
|
123
|
+
);
|
|
124
|
+
const releaseIntentEvents = events.filter(
|
|
125
|
+
(event) => event.event === CONTROL_PLANE_EVENT_NAMES["release-intent"],
|
|
126
|
+
);
|
|
127
|
+
const workflowFriction = countOutcomes(workflowFrictionEvents);
|
|
128
|
+
const releaseIntent = countOutcomes(releaseIntentEvents);
|
|
129
|
+
const incidentDecisions = Number(workflowFriction.created || 0) + Number(workflowFriction.reused || 0);
|
|
130
|
+
const releaseIntentDecisions =
|
|
131
|
+
Number(releaseIntent.created || 0) +
|
|
132
|
+
Number(releaseIntent.reused || 0) +
|
|
133
|
+
Number(releaseIntent.suppressed || 0);
|
|
134
|
+
const suppressionReasons = {};
|
|
135
|
+
for (const event of releaseIntentEvents) {
|
|
136
|
+
if (event.attributes?.outcome !== "suppressed") continue;
|
|
137
|
+
const reason = String(event.attributes?.reason || "unknown");
|
|
138
|
+
suppressionReasons[reason] = (suppressionReasons[reason] || 0) + 1;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
contract: BUILDCHAIN_CONTROL_PLANE_SUMMARY_CONTRACT,
|
|
143
|
+
eventCount: workflowFrictionEvents.length + releaseIntentEvents.length,
|
|
144
|
+
workflowFriction: {
|
|
145
|
+
eventCount: workflowFrictionEvents.length,
|
|
146
|
+
outcomes: workflowFriction,
|
|
147
|
+
incidentReuseRate: ratio(Number(workflowFriction.reused || 0), incidentDecisions),
|
|
148
|
+
},
|
|
149
|
+
releaseIntent: {
|
|
150
|
+
eventCount: releaseIntentEvents.length,
|
|
151
|
+
outcomes: releaseIntent,
|
|
152
|
+
suppressionRate: ratio(Number(releaseIntent.suppressed || 0), releaseIntentDecisions),
|
|
153
|
+
suppressionReasons: Object.fromEntries(
|
|
154
|
+
Object.entries(suppressionReasons).sort(([left], [right]) => left.localeCompare(right)),
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function recordBuildchainControlPlaneOutcome({
|
|
161
|
+
domain,
|
|
162
|
+
action = "",
|
|
163
|
+
outcome = "",
|
|
164
|
+
reason = "",
|
|
165
|
+
attributes = {},
|
|
166
|
+
} = {}, options = {}) {
|
|
167
|
+
if (!Object.hasOwn(CONTROL_PLANE_EVENT_NAMES, domain)) {
|
|
168
|
+
throw new Error(`unsupported Buildchain control-plane domain: ${domain || "<empty>"}`);
|
|
169
|
+
}
|
|
170
|
+
const logger = options.logger || createBuildchainLogger({
|
|
171
|
+
cwd: options.cwd,
|
|
172
|
+
path: options.path,
|
|
173
|
+
console: options.console ?? false,
|
|
174
|
+
source: "buildchain",
|
|
175
|
+
component: "control-plane",
|
|
176
|
+
phase: domain,
|
|
177
|
+
});
|
|
178
|
+
const normalizedOutcome = outcome || action || "unknown";
|
|
179
|
+
const details = {
|
|
180
|
+
attributes: {
|
|
181
|
+
...attributes,
|
|
182
|
+
action,
|
|
183
|
+
outcome: normalizedOutcome,
|
|
184
|
+
reason,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
return ["failed", "error"].includes(normalizedOutcome)
|
|
188
|
+
? logger.warn(CONTROL_PLANE_EVENT_NAMES[domain], details)
|
|
189
|
+
: logger.info(CONTROL_PLANE_EVENT_NAMES[domain], details);
|
|
190
|
+
}
|
|
191
|
+
|
|
93
192
|
export function summarizeBuildchainLogEvents(input = {}) {
|
|
94
193
|
const events = Array.isArray(input)
|
|
95
194
|
? input
|
|
@@ -109,6 +208,8 @@ export function summarizeBuildchainLogEvents(input = {}) {
|
|
|
109
208
|
sources: groupSummary(events, "source"),
|
|
110
209
|
phases: groupSummary(events, "phase"),
|
|
111
210
|
components: groupSummary(events, "component"),
|
|
211
|
+
events: groupSummary(events, "event"),
|
|
212
|
+
controlPlane: summarizeBuildchainControlPlaneEvents(events),
|
|
112
213
|
};
|
|
113
214
|
}
|
|
114
215
|
|
|
@@ -316,7 +316,7 @@ function runAdapter(values) {
|
|
|
316
316
|
const result = spawnSync(
|
|
317
317
|
adapter,
|
|
318
318
|
["--artifact-root", artifactRoot, "--output", output, "--source-coordinate", sourceCoordinate],
|
|
319
|
-
{ cwd: sourceRoot, env, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 },
|
|
319
|
+
{ cwd: sourceRoot, env: environment, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 },
|
|
320
320
|
);
|
|
321
321
|
fs.writeFileSync(path.join(diagnostics, "adapter.stdout.log"), result.stdout || "");
|
|
322
322
|
fs.writeFileSync(path.join(diagnostics, "adapter.stderr.log"), result.stderr || result.error?.message || "");
|
|
@@ -19,6 +19,28 @@ import {
|
|
|
19
19
|
const root = process.cwd();
|
|
20
20
|
const sharedActionTsupConfig = fs.readFileSync(path.join(root, "scripts/tsup-action.config.mjs"), "utf8");
|
|
21
21
|
const commonJsSourcePattern = /\b(require\s*\(|module\.exports|exports\.|require\.main|createRequire)\b/;
|
|
22
|
+
|
|
23
|
+
function assertSelfReleaseImpactContract(impact, { expectedVersion = "" } = {}) {
|
|
24
|
+
if (!impact || typeof impact !== "object" || Array.isArray(impact)) {
|
|
25
|
+
throw new Error("Buildchain self release impact must be a JSON object");
|
|
26
|
+
}
|
|
27
|
+
const version = String(impact.release?.version || "").trim();
|
|
28
|
+
const versionMatch = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
29
|
+
if (!versionMatch) throw new Error("Buildchain self release impact release.version must be a semantic version");
|
|
30
|
+
if (expectedVersion && version !== expectedVersion) {
|
|
31
|
+
throw new Error("Buildchain self release impact version must match package.json version");
|
|
32
|
+
}
|
|
33
|
+
const expectedLine = `v${versionMatch[1]}.${versionMatch[2]}`;
|
|
34
|
+
const line = String(impact.release?.line || "").trim();
|
|
35
|
+
if (line !== expectedLine) {
|
|
36
|
+
throw new Error(`Buildchain self release impact line must be ${expectedLine} for release.version ${version}`);
|
|
37
|
+
}
|
|
38
|
+
const summary = String(impact.summary || "").trim();
|
|
39
|
+
if (!summary.startsWith(`Buildchain ${line} `)) {
|
|
40
|
+
throw new Error(`Buildchain self release impact summary must describe the current ${line} line`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
const requiredPaths = [
|
|
23
45
|
"AGENTS.md",
|
|
24
46
|
"CONTRIBUTING.md",
|
|
@@ -707,7 +729,7 @@ for (const requiredSnippet of [
|
|
|
707
729
|
for (const requiredSnippet of [
|
|
708
730
|
"Capability Coverage",
|
|
709
731
|
"KFD-1 / KFD-2 / KFD-3",
|
|
710
|
-
"floating `@
|
|
732
|
+
"floating `@v3`",
|
|
711
733
|
"npm publish transactions",
|
|
712
734
|
"Git/source/version/module/product build facts",
|
|
713
735
|
"GitHub Release",
|
|
@@ -778,7 +800,7 @@ for (const requiredSnippet of [
|
|
|
778
800
|
for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs/install.md": installDoc })) {
|
|
779
801
|
for (const requiredSnippet of [
|
|
780
802
|
"minimumReleaseAgeExclude",
|
|
781
|
-
"@kungfu-tech/buildchain@
|
|
803
|
+
"@kungfu-tech/buildchain@3.0.0",
|
|
782
804
|
"package/version-specific",
|
|
783
805
|
]) {
|
|
784
806
|
if (!docSource.includes(requiredSnippet)) {
|
|
@@ -844,19 +866,16 @@ const selfHostedRunnerSmokeWorkflow = fs.readFileSync(path.join(root, ".github/w
|
|
|
844
866
|
const npmDryRunScript = fs.readFileSync(path.join(root, "scripts/npm-publish-dry-run.mjs"), "utf8");
|
|
845
867
|
const npmPublishTransactionScript = fs.readFileSync(path.join(root, "scripts/npm-publish-transaction.mjs"), "utf8");
|
|
846
868
|
const rootPackageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
if (!expectedSelfReleaseLine || selfReleaseImpact.release?.line !== expectedSelfReleaseLine) {
|
|
854
|
-
throw new Error("Buildchain self release impact line must match package.json major/minor line");
|
|
855
|
-
}
|
|
869
|
+
const selfReleaseImpactPath = path.resolve(
|
|
870
|
+
root,
|
|
871
|
+
process.env.BUILDCHAIN_SELF_RELEASE_IMPACT_PATH || ".buildchain/release-impact.json",
|
|
872
|
+
);
|
|
873
|
+
const selfReleaseImpact = JSON.parse(fs.readFileSync(selfReleaseImpactPath, "utf8"));
|
|
874
|
+
assertSelfReleaseImpactContract(selfReleaseImpact, { expectedVersion: rootPackageJson.version });
|
|
856
875
|
if (!["patch", "minor", "major"].includes(selfReleaseImpact.classification)) {
|
|
857
876
|
throw new Error("Buildchain self release impact classification must be patch, minor, or major");
|
|
858
877
|
}
|
|
859
|
-
if (!
|
|
878
|
+
if (!Array.isArray(selfReleaseImpact.surfaceImpacts) || selfReleaseImpact.surfaceImpacts.length === 0) {
|
|
860
879
|
throw new Error("Buildchain self release impact requires a summary and surfaceImpacts[]");
|
|
861
880
|
}
|
|
862
881
|
for (const requiredSnippet of [
|
|
@@ -969,7 +988,7 @@ for (const retiredWorkflow of [
|
|
|
969
988
|
const retiredSource = fs.readFileSync(path.join(workflowDir, retiredWorkflow), "utf8");
|
|
970
989
|
for (const requiredSnippet of [
|
|
971
990
|
"release path is retired",
|
|
972
|
-
"release-candidate-promote.yml@
|
|
991
|
+
"release-candidate-promote.yml@v3",
|
|
973
992
|
"publish-gate source-lock enforcement",
|
|
974
993
|
]) {
|
|
975
994
|
if (!retiredSource.includes(requiredSnippet)) {
|
|
@@ -1115,12 +1134,12 @@ if (inventory.release !== "buildchain-v2") {
|
|
|
1115
1134
|
throw new Error("inventory release must be buildchain-v2");
|
|
1116
1135
|
}
|
|
1117
1136
|
|
|
1118
|
-
if (inventory.stableRefs?.actions !== "kungfu-systems/buildchain/actions/<name>@
|
|
1119
|
-
throw new Error("inventory stable action ref must point at @
|
|
1137
|
+
if (inventory.stableRefs?.actions !== "kungfu-systems/buildchain/actions/<name>@v3") {
|
|
1138
|
+
throw new Error("inventory stable action ref must point at @v3");
|
|
1120
1139
|
}
|
|
1121
1140
|
|
|
1122
|
-
if (inventory.stableRefs?.workflows !== "kungfu-systems/buildchain/.github/workflows/<workflow>.yml@
|
|
1123
|
-
throw new Error("inventory stable workflow ref must point at @
|
|
1141
|
+
if (inventory.stableRefs?.workflows !== "kungfu-systems/buildchain/.github/workflows/<workflow>.yml@v3") {
|
|
1142
|
+
throw new Error("inventory stable workflow ref must point at @v3");
|
|
1124
1143
|
}
|
|
1125
1144
|
if (inventory.safety?.releasePassport?.line !== "v2.2") {
|
|
1126
1145
|
throw new Error("release passport inventory must be registered as a v2.2 surface");
|
package/scripts/init-repo.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { BUILDCHAIN_CONFIG_PATH } from "../packages/core/buildchain-layout.js";
|
|
6
6
|
import { detectPackageManager, assertPackageManager } from "../packages/core/package-manager.js";
|
|
7
7
|
|
|
8
|
-
const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@
|
|
8
|
+
const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@v3";
|
|
9
9
|
const DEFAULT_PUBLICATION_LATEX_IMAGE = "ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
|
|
10
10
|
const DEFAULT_PUBLICATION_LATEX_DIGEST = "sha256:c20f3809e96836c1c78e97c76939d12f1de3fed0ea9b7c40c43332ec2ea480f8";
|
|
11
11
|
const DEFAULT_PUBLICATION_LATEX_COMMAND = "latexmk -pdf -outdir=_build paper/main.tex";
|
|
@@ -349,7 +349,7 @@ permissions:
|
|
|
349
349
|
|
|
350
350
|
jobs:
|
|
351
351
|
publication:
|
|
352
|
-
uses: kungfu-systems/buildchain/.github/workflows/publication-artifact.yml@
|
|
352
|
+
uses: kungfu-systems/buildchain/.github/workflows/publication-artifact.yml@v3
|
|
353
353
|
with:
|
|
354
354
|
buildchain-ref: \${{ inputs.buildchain-ref || '' }}
|
|
355
355
|
toolchain-type: config
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
defaultBuildchainLogPath,
|
|
6
|
+
recordBuildchainControlPlaneOutcome,
|
|
7
|
+
} from "../packages/core/logging.js";
|
|
4
8
|
|
|
5
9
|
function requiredString(value, name) {
|
|
6
10
|
const normalized = String(value || "").trim();
|
|
@@ -70,6 +74,34 @@ function runUrl({ serverUrl = "", repository = "", runId = "" } = {}) {
|
|
|
70
74
|
return `${serverUrl.replace(/\/$/, "")}/${repository}/actions/runs/${runId}`;
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
export function recordProductionReleasePrOutcome(result, env = process.env) {
|
|
78
|
+
const outcome = result.action === "created"
|
|
79
|
+
? "created"
|
|
80
|
+
: result.action === "updated"
|
|
81
|
+
? "reused"
|
|
82
|
+
: result.action === "suppressed-merged-release-pr"
|
|
83
|
+
? "suppressed"
|
|
84
|
+
: ["permission-denied", "app-token-unavailable", "failed"].includes(result.action)
|
|
85
|
+
? "failed"
|
|
86
|
+
: "skipped";
|
|
87
|
+
return recordBuildchainControlPlaneOutcome({
|
|
88
|
+
domain: "release-intent",
|
|
89
|
+
action: result.action,
|
|
90
|
+
outcome,
|
|
91
|
+
reason: result.suppressionReason || result.status,
|
|
92
|
+
attributes: {
|
|
93
|
+
repository: result.repository,
|
|
94
|
+
channel: result.productionReleaseChannel,
|
|
95
|
+
pullNumber: result.pullNumber,
|
|
96
|
+
sourceSha: result.sourceSha,
|
|
97
|
+
},
|
|
98
|
+
}, {
|
|
99
|
+
path: optionalString(env.BUILDCHAIN_LOG_PATH) ||
|
|
100
|
+
(env.GITHUB_ACTIONS === "true" ? defaultBuildchainLogPath() : false),
|
|
101
|
+
console: false,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
function urlsFromResult(result = {}) {
|
|
74
106
|
const urls = result.urls && typeof result.urls === "object" ? result.urls : {};
|
|
75
107
|
if (Object.keys(urls).length > 0) return urls;
|
|
@@ -374,6 +406,33 @@ async function createOrUpdateBranch({ apiUrl, token, owner, repo, branchName, so
|
|
|
374
406
|
return createdCommit.sha;
|
|
375
407
|
}
|
|
376
408
|
|
|
409
|
+
function findMergedProductionReleasePr({
|
|
410
|
+
pullRequests = [],
|
|
411
|
+
repository,
|
|
412
|
+
productionReleaseLabel = "buildchain-release",
|
|
413
|
+
productionReleaseHeadPrefix = "release/",
|
|
414
|
+
base = "main",
|
|
415
|
+
} = {}) {
|
|
416
|
+
const fullName = requiredString(repository, "repository");
|
|
417
|
+
const label = requiredString(productionReleaseLabel, "productionReleaseLabel");
|
|
418
|
+
const headPrefix = optionalString(productionReleaseHeadPrefix);
|
|
419
|
+
const candidates = (Array.isArray(pullRequests) ? pullRequests : []).filter((pull) => {
|
|
420
|
+
const labels = Array.isArray(pull?.labels) ? pull.labels.map((entry) => entry?.name || entry) : [];
|
|
421
|
+
const headRef = optionalString(pull?.head?.ref);
|
|
422
|
+
return Boolean(pull?.merged_at) &&
|
|
423
|
+
pull?.base?.ref === base &&
|
|
424
|
+
pull?.head?.repo?.full_name === fullName &&
|
|
425
|
+
labels.includes(label) &&
|
|
426
|
+
(!headPrefix || headRef.startsWith(headPrefix));
|
|
427
|
+
});
|
|
428
|
+
if (candidates.length > 1) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`multiple merged production release PRs matched the source commit: ${candidates.map((pull) => `#${pull.number}`).join(", ")}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return candidates[0];
|
|
434
|
+
}
|
|
435
|
+
|
|
377
436
|
export async function openProductionReleasePr({
|
|
378
437
|
apiUrl = "https://api.github.com",
|
|
379
438
|
token,
|
|
@@ -400,6 +459,43 @@ export async function openProductionReleasePr({
|
|
|
400
459
|
});
|
|
401
460
|
const { owner, repo, branchName, title, body, head } = handoff;
|
|
402
461
|
const normalizedToken = requiredString(token, "token");
|
|
462
|
+
const associated = await githubJson({
|
|
463
|
+
apiUrl,
|
|
464
|
+
token: normalizedToken,
|
|
465
|
+
path: `/repos/${owner}/${repo}/commits/${encodeURIComponent(handoff.sourceSha)}/pulls?per_page=100`,
|
|
466
|
+
});
|
|
467
|
+
let mergedReleasePull = findMergedProductionReleasePr({
|
|
468
|
+
pullRequests: associated,
|
|
469
|
+
repository,
|
|
470
|
+
productionReleaseLabel,
|
|
471
|
+
productionReleaseHeadPrefix,
|
|
472
|
+
base: handoff.base,
|
|
473
|
+
});
|
|
474
|
+
if (!mergedReleasePull) {
|
|
475
|
+
const closedByDeterministicHead = await githubJson({
|
|
476
|
+
apiUrl,
|
|
477
|
+
token: normalizedToken,
|
|
478
|
+
path: `/repos/${owner}/${repo}/pulls?state=closed&base=${encodeURIComponent(handoff.base)}&head=${encodeURIComponent(head)}&per_page=100`,
|
|
479
|
+
});
|
|
480
|
+
mergedReleasePull = findMergedProductionReleasePr({
|
|
481
|
+
pullRequests: closedByDeterministicHead,
|
|
482
|
+
repository,
|
|
483
|
+
productionReleaseLabel,
|
|
484
|
+
productionReleaseHeadPrefix,
|
|
485
|
+
base: handoff.base,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (mergedReleasePull) {
|
|
489
|
+
return {
|
|
490
|
+
action: "suppressed-merged-release-pr",
|
|
491
|
+
status: "suppressed-merged-release-pr",
|
|
492
|
+
...handoff,
|
|
493
|
+
branchName,
|
|
494
|
+
pullNumber: mergedReleasePull.number,
|
|
495
|
+
pullUrl: mergedReleasePull.html_url || mergedReleasePull.url || "",
|
|
496
|
+
suppressionReason: "source-commit-already-has-qualifying-merged-release-pr",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
403
499
|
const existing = await githubJson({
|
|
404
500
|
apiUrl,
|
|
405
501
|
token: normalizedToken,
|
|
@@ -524,6 +620,7 @@ export async function webSurfaceProductionReleasePrCli(env = process.env) {
|
|
|
524
620
|
if (failOnReleasePrError) {
|
|
525
621
|
writeJsonFile(summaryPath, result);
|
|
526
622
|
if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, renderStepSummary(result));
|
|
623
|
+
recordProductionReleasePrOutcome(result, env);
|
|
527
624
|
throw new Error(result.error.message);
|
|
528
625
|
}
|
|
529
626
|
} else {
|
|
@@ -562,12 +659,14 @@ export async function webSurfaceProductionReleasePrCli(env = process.env) {
|
|
|
562
659
|
if (status !== "permission-denied" || failOnReleasePrError) {
|
|
563
660
|
writeJsonFile(summaryPath, result);
|
|
564
661
|
if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, renderStepSummary(result));
|
|
662
|
+
recordProductionReleasePrOutcome(result, env);
|
|
565
663
|
throw error;
|
|
566
664
|
}
|
|
567
665
|
}
|
|
568
666
|
}
|
|
569
667
|
}
|
|
570
668
|
|
|
669
|
+
recordProductionReleasePrOutcome(result, env);
|
|
571
670
|
writeJsonFile(summaryPath, result);
|
|
572
671
|
if (env.GITHUB_STEP_SUMMARY) {
|
|
573
672
|
fs.appendFileSync(env.GITHUB_STEP_SUMMARY, renderStepSummary(result));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { recordBuildchainControlPlaneOutcome } from "../packages/core/logging.js";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
6
7
|
|
|
@@ -346,6 +347,15 @@ export async function workflowFrictionReportCli() {
|
|
|
346
347
|
})}\n`);
|
|
347
348
|
console.error(`::warning::${diagnosis.replace(/\r?\n/g, "%0A")}`);
|
|
348
349
|
}
|
|
350
|
+
recordBuildchainControlPlaneOutcome({
|
|
351
|
+
domain: "workflow-friction",
|
|
352
|
+
action: "classified",
|
|
353
|
+
outcome: "classified",
|
|
354
|
+
attributes: {
|
|
355
|
+
frictionClass: result.frictionClass,
|
|
356
|
+
pullRequest: result.pullRequest,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
349
359
|
writeGitHubOutputs({
|
|
350
360
|
"friction-class": result.frictionClass,
|
|
351
361
|
summary: result.summary,
|