@kungfu-tech/buildchain 2.10.4 → 2.10.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 +16 -1
- package/bin/buildchain.mjs +299 -16
- package/dist/site/buildchain-contract.json +7 -7
- package/dist/site/buildchain-site.json +42 -37
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +164 -8
- package/dist/site/kfd-claims.json +190 -19
- package/dist/site/manual-registry.json +10 -10
- package/dist/site/node-api-registry.json +24 -11
- package/dist/site/page-registry.json +27 -22
- package/dist/site/public-surface-audit.json +334 -126
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/site-manifest.json +14 -14
- package/dist/site/workflow-registry.json +4 -1
- package/docs/MAP.md +2 -2
- package/docs/cli.md +48 -14
- package/docs/install.md +1 -1
- package/docs/kfd-support.md +80 -21
- package/docs/lifecycle-protocol.md +7 -2
- package/docs/publish-transaction.md +1 -1
- package/docs/readme-badges.md +2 -2
- package/docs/release-passport.md +1 -1
- package/docs/reusable-build-surface.md +7 -7
- package/docs/web-surface-deployments.md +33 -4
- package/package.json +3 -2
- package/packages/core/buildchain-config.js +10 -5
- package/packages/core/buildchain-layout.js +157 -0
- package/packages/core/index.js +34 -0
- package/packages/core/kfd.js +273 -0
- package/packages/core/kfd3-surface-register.js +25 -17
- package/packages/core/public-surface-audit.js +14 -7
- package/scripts/buildchain-contract-lock.mjs +6 -2
- package/scripts/check-inventory.mjs +5 -4
- package/scripts/generate-site-bundle.mjs +20 -3
- package/scripts/init-repo.mjs +2 -1
- package/scripts/web-surface-production-release-pr.mjs +209 -36
- package/scripts/web-surface-release-pr-review.mjs +2 -1
|
@@ -40,9 +40,11 @@ function listDirectories(root, dir) {
|
|
|
40
40
|
.sort();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function commandId(first = "", second = "") {
|
|
43
|
+
function commandId(first = "", second = "", third = "") {
|
|
44
44
|
const head = String(first || "").trim();
|
|
45
45
|
const sub = String(second || "").trim();
|
|
46
|
+
const leaf = String(third || "").trim();
|
|
47
|
+
const normalizedLeaf = leaf === "..." ? "" : leaf;
|
|
46
48
|
if (!head) return "";
|
|
47
49
|
if (["-h", "--help", "help"].includes(head)) return "help";
|
|
48
50
|
if (["-v", "--version", "version"].includes(head)) return "version";
|
|
@@ -59,7 +61,12 @@ function commandId(first = "", second = "") {
|
|
|
59
61
|
if (head === "log" && sub) return "logging";
|
|
60
62
|
if (head === "diagnostics" && sub) return `diagnostics-${sub}`;
|
|
61
63
|
if (head === "facts" && sub) return "build-facts";
|
|
62
|
-
if (head === "kfd
|
|
64
|
+
if (head === "kfd") {
|
|
65
|
+
if (!sub || sub === "...") return "kfd";
|
|
66
|
+
if (sub === "schema") return normalizedLeaf ? `kfd-schema-${normalizedLeaf}` : "kfd-schema";
|
|
67
|
+
if (/^[1-9][0-9]*$/.test(sub)) return normalizedLeaf ? `kfd-${sub}-${normalizedLeaf}` : `kfd-${sub}`;
|
|
68
|
+
return `kfd-${sub}`;
|
|
69
|
+
}
|
|
63
70
|
if (head === "sample" && sub) return `sample-${sub}`;
|
|
64
71
|
if (head === "badges" && sub) return `badges-${sub}`;
|
|
65
72
|
if (head === "homebrew" && sub) return `homebrew-${sub}`;
|
|
@@ -77,10 +84,10 @@ export function enumerateCliCommandsFromBin({ root = process.cwd(), binPath = "b
|
|
|
77
84
|
const usage = usageMatch?.[1] || "";
|
|
78
85
|
const usageCommands = [];
|
|
79
86
|
for (const line of usage.split(/\r?\n/)) {
|
|
80
|
-
const match = line.trim().match(/^buildchain\s+([^\s]+)(?:\s+([^\s]+))?/);
|
|
87
|
+
const match = line.trim().match(/^buildchain\s+([^\s]+)(?:\s+([^\s]+))?(?:\s+([^\s]+))?/);
|
|
81
88
|
if (!match) continue;
|
|
82
89
|
usageCommands.push({
|
|
83
|
-
id: commandId(match[1], match[2]),
|
|
90
|
+
id: commandId(match[1], match[2], match[3]),
|
|
84
91
|
usage: line.trim().replace(/\s+/g, " "),
|
|
85
92
|
});
|
|
86
93
|
}
|
|
@@ -180,15 +187,15 @@ export function enumerateDocCommandRefs({ root = process.cwd() } = {}) {
|
|
|
180
187
|
.filter((line) => /^(?:[$>]\s*)?(?:npx\s+(?:@kungfu-tech\/buildchain|buildchain)\s+|node\s+bin\/buildchain\.mjs\s+|buildchain\s+)/.test(line))
|
|
181
188
|
.join("\n")
|
|
182
189
|
: segment.text;
|
|
183
|
-
for (const match of commandText.matchAll(/(?:^|[\s$>])(?:npx\s+(?:@kungfu-tech\/buildchain|buildchain)\s+|node\s+bin\/buildchain\.mjs\s+|buildchain\s+)([a-z0-9][a-z0-9-]*|--help|--version|-h|-v)(?:\s+([a-z0-9][a-z0-9-]*|--[a-z0-9-]+))?/gm)) {
|
|
184
|
-
const id = commandId(match[1], match[2]);
|
|
190
|
+
for (const match of commandText.matchAll(/(?:^|[\s$>])(?:npx\s+(?:@kungfu-tech\/buildchain|buildchain)\s+|node\s+bin\/buildchain\.mjs\s+|buildchain\s+)([a-z0-9][a-z0-9-]*|--help|--version|-h|-v)(?:\s+([a-z0-9][a-z0-9-]*|--[a-z0-9-]+))?(?:\s+([a-z0-9][a-z0-9-]*|--[a-z0-9-]+))?/gm)) {
|
|
191
|
+
const id = commandId(match[1], match[2], match[3]);
|
|
185
192
|
if (segment.kind === "inline" && !knownCommandIds.has(id)) {
|
|
186
193
|
continue;
|
|
187
194
|
}
|
|
188
195
|
refs.push({
|
|
189
196
|
id,
|
|
190
197
|
path: relPath,
|
|
191
|
-
command: `buildchain ${match[1]}${match[2] ? ` ${match[2]}` : ""}`,
|
|
198
|
+
command: `buildchain ${match[1]}${match[2] ? ` ${match[2]}` : ""}${match[3] ? ` ${match[3]}` : ""}`,
|
|
192
199
|
});
|
|
193
200
|
}
|
|
194
201
|
}
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
readBuildchainContractWorld,
|
|
11
11
|
renderBuildchainContractDriftIssueBody,
|
|
12
12
|
} from "../packages/core/buildchain-contract.js";
|
|
13
|
+
import {
|
|
14
|
+
BUILDCHAIN_CONTRACT_LOCK_PATH,
|
|
15
|
+
resolveBuildchainContractLockPath,
|
|
16
|
+
} from "../packages/core/buildchain-layout.js";
|
|
13
17
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
14
18
|
|
|
15
19
|
function env(name, fallback = "") {
|
|
@@ -58,7 +62,7 @@ function issueModeAllows(mode, evaluation) {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
export function checkBuildchainContractLock({
|
|
61
|
-
lockPath = env("BUILDCHAIN_CONTRACT_LOCK_PATH"
|
|
65
|
+
lockPath = env("BUILDCHAIN_CONTRACT_LOCK_PATH") || resolveBuildchainContractLockPath(process.cwd()),
|
|
62
66
|
currentContractPath = env("BUILDCHAIN_CONTRACT_CURRENT_PATH", ".buildchain/runtime/dist/site/buildchain-contract.json"),
|
|
63
67
|
runtimeRoot = env("BUILDCHAIN_RUNTIME_ROOT", ".buildchain/runtime"),
|
|
64
68
|
runtimeRef = env("BUILDCHAIN_RUNTIME_REF"),
|
|
@@ -124,7 +128,7 @@ export function checkBuildchainContractLock({
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
export function writeBuildchainContractLock({
|
|
127
|
-
output = env("BUILDCHAIN_CONTRACT_LOCK_PATH",
|
|
131
|
+
output = env("BUILDCHAIN_CONTRACT_LOCK_PATH", BUILDCHAIN_CONTRACT_LOCK_PATH),
|
|
128
132
|
currentContractPath = env("BUILDCHAIN_CONTRACT_CURRENT_PATH", "dist/site/buildchain-contract.json"),
|
|
129
133
|
runtimeRoot = env("BUILDCHAIN_RUNTIME_ROOT", process.cwd()),
|
|
130
134
|
buildchainRef = env("BUILDCHAIN_RUNTIME_REF", "v2"),
|
|
@@ -54,7 +54,8 @@ const requiredPaths = [
|
|
|
54
54
|
"docs/lifecycle-protocol.md",
|
|
55
55
|
"docs/ownership.md",
|
|
56
56
|
"tests/buildchain-inventory.json",
|
|
57
|
-
"buildchain.toml",
|
|
57
|
+
".buildchain/buildchain.toml",
|
|
58
|
+
".buildchain/contract-lock.json",
|
|
58
59
|
".github/actionlint.yaml",
|
|
59
60
|
".github/workflows/self-hosted-runner-smoke.yml",
|
|
60
61
|
".github/workflows/buildchain-ref-promotion.yml",
|
|
@@ -126,8 +127,8 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
|
|
|
126
127
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
127
128
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
128
129
|
}
|
|
129
|
-
if (rootPackage.exports?.["./kfd
|
|
130
|
-
throw new Error("root package must export @kungfu-tech/buildchain/kfd
|
|
130
|
+
if (rootPackage.exports?.["./kfd"] !== "./packages/core/kfd.js") {
|
|
131
|
+
throw new Error("root package must export @kungfu-tech/buildchain/kfd");
|
|
131
132
|
}
|
|
132
133
|
if (rootPackage.exports?.["./release-line-bootstrap"] !== "./packages/core/release-line-bootstrap.js") {
|
|
133
134
|
throw new Error("root package must export @kungfu-tech/buildchain/release-line-bootstrap");
|
|
@@ -498,7 +499,7 @@ for (const requiredSnippet of [
|
|
|
498
499
|
"buildFacts[]",
|
|
499
500
|
"--build-facts-json",
|
|
500
501
|
"Floating Buildchain contract lock",
|
|
501
|
-
"buildchain
|
|
502
|
+
".buildchain/contract-lock.json",
|
|
502
503
|
"--kfd-3-prebuild-witness-json",
|
|
503
504
|
"--kfd-3-artifact-verify-cmd",
|
|
504
505
|
"currently named `kfd-3`",
|
|
@@ -388,12 +388,28 @@ function cliCommandMeta(id) {
|
|
|
388
388
|
inspect: { group: "release-passport-trust", purpose: "Inspect release and artifact evidence command families." },
|
|
389
389
|
"inspect-artifact": { group: "release-passport-trust", purpose: "Inspect artifact evidence." },
|
|
390
390
|
"inspect-release": { group: "release-passport-trust", purpose: "Inspect release passport evidence." },
|
|
391
|
-
|
|
391
|
+
kfd: { group: "kfd-trust", purpose: "Inspect KFD standards, schemas, and versioned KFD command families." },
|
|
392
|
+
"kfd-schema": { group: "kfd-trust", purpose: "Inspect KFD schema command families." },
|
|
393
|
+
"kfd-schema-list": { group: "kfd-trust", purpose: "List machine-readable schemas exposed by the KFD package standards metadata." },
|
|
394
|
+
"kfd-schema-show": { group: "kfd-trust", purpose: "Print a machine-readable KFD schema from the KFD package standards metadata." },
|
|
395
|
+
"kfd-status": { group: "kfd-trust", purpose: "Report implemented KFD support and the active .buildchain repository layout." },
|
|
396
|
+
"kfd-migrate-layout": { group: "kfd-trust", purpose: "Plan or apply migration of legacy root Buildchain files into .buildchain/." },
|
|
397
|
+
"kfd-1": { group: "kfd-trust", purpose: "Inspect KFD-1 contract-world witness, gate, verify, and schema command families." },
|
|
398
|
+
"kfd-1-gate": { group: "kfd-trust", purpose: "Generate KFD-1 release gate evidence from declared contract-world witnesses." },
|
|
399
|
+
"kfd-1-schema": { group: "kfd-trust", purpose: "Print the default KFD-1 schema exposed by the KFD package standards metadata." },
|
|
400
|
+
"kfd-1-verify": { group: "kfd-trust", purpose: "Validate KFD-1 release gate evidence." },
|
|
401
|
+
"kfd-1-witness": { group: "kfd-trust", purpose: "Generate Buildchain's KFD-1 self contract-world witness." },
|
|
402
|
+
"kfd-2": { group: "kfd-trust", purpose: "Inspect KFD-2 trust taxonomy, public claims, and schema command families." },
|
|
403
|
+
"kfd-2-claims": { group: "kfd-trust", purpose: "Generate Buildchain's KFD-2 public trust claim evidence." },
|
|
404
|
+
"kfd-2-schema": { group: "kfd-trust", purpose: "Print the default KFD-2 schema exposed by the KFD package standards metadata." },
|
|
405
|
+
"kfd-2-taxonomy": { group: "kfd-trust", purpose: "Validate KFD-2 trust taxonomy entries from the KFD package standards metadata." },
|
|
406
|
+
"kfd-3": { group: "kfd-trust", purpose: "Inspect KFD-3 surface detection, registry, witness, audit, and query command families under the unified KFD namespace." },
|
|
392
407
|
"kfd-3-audit": { group: "kfd-trust", purpose: "Compare detected, declared, and enforced KFD-3 public surfaces." },
|
|
393
408
|
"kfd-3-detect": { group: "kfd-trust", purpose: "Detect standard KFD-3 public surface candidates from source and artifact metadata." },
|
|
394
409
|
"kfd-3-query": { group: "kfd-trust", purpose: "Return an agent-readable KFD-3 capability map for a product, registry, or release passport." },
|
|
395
410
|
"kfd-3-register": { group: "kfd-trust", purpose: "Declare detected KFD-3 public surfaces into a product-owned surface registry." },
|
|
396
411
|
"kfd-3-witness": { group: "kfd-trust", purpose: "Generate release-passport-compatible KFD-3 surface witnesses from a product registry." },
|
|
412
|
+
"kfd-4-schema": { group: "kfd-trust", purpose: "Print the default KFD-4 schema exposed by the KFD package standards metadata." },
|
|
397
413
|
lifecycle: { group: "reusable-build", purpose: "Run configured lifecycle commands and write deterministic artifact manifests." },
|
|
398
414
|
log: { group: "observability-diagnostics", purpose: "Inspect Buildchain logging command families." },
|
|
399
415
|
logging: { group: "observability-diagnostics", purpose: "Emit timestamped build events, summarize logs, and enforce required phases." },
|
|
@@ -409,7 +425,7 @@ function cliCommandMeta(id) {
|
|
|
409
425
|
"sample-process-tree": { group: "observability-diagnostics", purpose: "Sample process-tree diagnostics for a wrapped command." },
|
|
410
426
|
span: { group: "observability-diagnostics", purpose: "Run a command inside a Buildchain log span." },
|
|
411
427
|
"transaction-inspect": { group: "release-passport-trust", purpose: "Inspect durable release transaction state." },
|
|
412
|
-
validate: { group: "getting-started", purpose: "Validate buildchain.toml and declared lifecycle surfaces." },
|
|
428
|
+
validate: { group: "getting-started", purpose: "Validate .buildchain/buildchain.toml and declared lifecycle surfaces." },
|
|
413
429
|
verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
|
|
414
430
|
"verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
|
|
415
431
|
"verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
|
|
@@ -443,7 +459,8 @@ function nodeApiMeta(exportName) {
|
|
|
443
459
|
"./buildchain-contract": { group: "governance-versioning", summary: "Runtime contract world and compatibility digest APIs for floating-ref drift checks." },
|
|
444
460
|
"./surface-manifest": { group: "site-and-propagation", summary: "Surface manifest timestamp and reproducibility policy APIs." },
|
|
445
461
|
"./issue-reporting": { group: "observability-diagnostics", summary: "Buildchain-owned issue reporting API for workflow friction feedback." },
|
|
446
|
-
"./
|
|
462
|
+
"./buildchain-layout": { group: "kfd-trust", summary: "Canonical .buildchain repository layout constants, path resolution, and migration APIs." },
|
|
463
|
+
"./kfd": { group: "kfd-trust", summary: "Unified KFD standards, schema discovery, KFD-1/KFD-2/KFD-3 grouped APIs, KFD-4 schema discovery, and Buildchain KFD claim helpers." },
|
|
447
464
|
"./public-surface-audit": { group: "kfd-trust", summary: "Reverse audit APIs for CLI, workflow, action, site page, and documentation command surfaces." },
|
|
448
465
|
"./kfd-gate": { group: "kfd-trust", summary: "KFD-1/KFD-2/KFD-3 release gate evidence and validation APIs." },
|
|
449
466
|
"./buildchain-kfd-claims": { group: "kfd-trust", summary: "Buildchain self KFD claim registry, witnesses, and public claim APIs." },
|
package/scripts/init-repo.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { BUILDCHAIN_CONFIG_PATH } from "../packages/core/buildchain-layout.js";
|
|
5
6
|
import { detectPackageManager, assertPackageManager } from "../packages/core/package-manager.js";
|
|
6
7
|
|
|
7
8
|
const BUILDCHAIN_WORKFLOW_REF = "kungfu-systems/buildchain/.github/workflows/.build.yml@v2";
|
|
@@ -356,7 +357,7 @@ export function initBuildchainRepo({
|
|
|
356
357
|
})();
|
|
357
358
|
|
|
358
359
|
const written = [
|
|
359
|
-
writeIfAllowed(path.join(resolvedCwd,
|
|
360
|
+
writeIfAllowed(path.join(resolvedCwd, BUILDCHAIN_CONFIG_PATH), toml, { force }),
|
|
360
361
|
writeIfAllowed(path.join(resolvedCwd, ".github", "workflows", "build.yml"), workflowYaml({
|
|
361
362
|
type,
|
|
362
363
|
runnerPreset,
|
|
@@ -30,6 +30,14 @@ function parseJson(value, name) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function parseBoolean(value, defaultValue = false) {
|
|
34
|
+
const normalized = optionalString(value).toLowerCase();
|
|
35
|
+
if (!normalized) return defaultValue;
|
|
36
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
37
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
38
|
+
throw new Error(`expected boolean value, got: ${value}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
function readArg(name, fallback = "") {
|
|
34
42
|
const index = process.argv.indexOf(`--${name}`);
|
|
35
43
|
if (index === -1) return fallback;
|
|
@@ -51,6 +59,12 @@ function writeJsonFile(filePath, value) {
|
|
|
51
59
|
fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`);
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
function writeTextFile(filePath, value) {
|
|
63
|
+
const resolved = path.resolve(requiredString(filePath, "filePath"));
|
|
64
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
65
|
+
fs.writeFileSync(resolved, String(value));
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
function runUrl({ serverUrl = "", repository = "", runId = "" } = {}) {
|
|
55
69
|
if (!serverUrl || !repository || !runId) return "";
|
|
56
70
|
return `${serverUrl.replace(/\/$/, "")}/${repository}/actions/runs/${runId}`;
|
|
@@ -153,6 +167,113 @@ ${urlLines.join("\n")}
|
|
|
153
167
|
This PR intentionally contains one empty release-intent commit.`;
|
|
154
168
|
}
|
|
155
169
|
|
|
170
|
+
export function createProductionReleasePrHandoff({
|
|
171
|
+
repository,
|
|
172
|
+
sourceSha,
|
|
173
|
+
stagingResult = {},
|
|
174
|
+
productionReleaseLabel = "buildchain-release",
|
|
175
|
+
productionReleaseHeadPrefix = "release/",
|
|
176
|
+
productionReleaseChannel = "production",
|
|
177
|
+
runId = "",
|
|
178
|
+
serverUrl = "https://github.com",
|
|
179
|
+
releasePassportArtifact = "buildchain-web-surface-staging-release-passport",
|
|
180
|
+
} = {}) {
|
|
181
|
+
const [owner, repo] = requiredString(repository, "repository").split("/");
|
|
182
|
+
if (!owner || !repo) throw new Error(`invalid repository: ${repository}`);
|
|
183
|
+
const normalizedSourceSha = requiredString(sourceSha, "sourceSha");
|
|
184
|
+
const label = requiredString(productionReleaseLabel, "productionReleaseLabel");
|
|
185
|
+
const branchName = releaseBranchName({
|
|
186
|
+
prefix: productionReleaseHeadPrefix || "release/",
|
|
187
|
+
channel: productionReleaseChannel || "production",
|
|
188
|
+
sourceSha: normalizedSourceSha,
|
|
189
|
+
});
|
|
190
|
+
const workflowRunUrl = runUrl({ serverUrl, repository, runId });
|
|
191
|
+
const title = `Release production from ${normalizedSourceSha.slice(0, 12)}`;
|
|
192
|
+
const body = renderProductionReleasePrBody({
|
|
193
|
+
stagingResult,
|
|
194
|
+
sourceSha: normalizedSourceSha,
|
|
195
|
+
artifactHash: stagingResult.artifactHash || "",
|
|
196
|
+
releasePassportArtifact,
|
|
197
|
+
workflowRunUrl,
|
|
198
|
+
productionReleaseLabel: label,
|
|
199
|
+
branchName,
|
|
200
|
+
});
|
|
201
|
+
const emptyCommitMessage = `buildchain release intent: ${productionReleaseChannel} ${normalizedSourceSha.slice(0, 12)}`;
|
|
202
|
+
const bodyPath = ".buildchain/production-release-pr/body.md";
|
|
203
|
+
const manualCommand = [
|
|
204
|
+
"git fetch origin main",
|
|
205
|
+
`git switch -C ${branchName} ${normalizedSourceSha}`,
|
|
206
|
+
`git commit --allow-empty -m ${JSON.stringify(emptyCommitMessage)}`,
|
|
207
|
+
`git push origin HEAD:${branchName}`,
|
|
208
|
+
`gh pr create --repo ${repository} --base main --head ${branchName} --title ${JSON.stringify(title)} --body-file ${bodyPath}`,
|
|
209
|
+
].join(" && ");
|
|
210
|
+
return {
|
|
211
|
+
contract: "kungfu-buildchain-web-surface-production-release-pr-handoff",
|
|
212
|
+
schemaVersion: 1,
|
|
213
|
+
repository,
|
|
214
|
+
owner,
|
|
215
|
+
repo,
|
|
216
|
+
base: "main",
|
|
217
|
+
head: `${owner}:${branchName}`,
|
|
218
|
+
branchName,
|
|
219
|
+
sourceSha: normalizedSourceSha,
|
|
220
|
+
title,
|
|
221
|
+
body,
|
|
222
|
+
label,
|
|
223
|
+
productionReleaseChannel,
|
|
224
|
+
releasePassportArtifact,
|
|
225
|
+
workflowRunUrl,
|
|
226
|
+
manualCommand,
|
|
227
|
+
staging: {
|
|
228
|
+
channel: stagingResult.channel || "",
|
|
229
|
+
status: stagingResult.status || "",
|
|
230
|
+
urls: urlsFromResult(stagingResult),
|
|
231
|
+
artifactHash: stagingResult.artifactHash || "",
|
|
232
|
+
target: stagingResult.target || "",
|
|
233
|
+
manifestKey: stagingResult.manifestKey || "",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function classifyReleasePrError(error) {
|
|
239
|
+
const message = String(error?.message || error || "");
|
|
240
|
+
if (
|
|
241
|
+
error?.status === 403 &&
|
|
242
|
+
/not permitted to create or approve pull requests|Resource not accessible by integration|permission|forbidden/i.test(message)
|
|
243
|
+
) {
|
|
244
|
+
return "permission-denied";
|
|
245
|
+
}
|
|
246
|
+
return "failed";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderStepSummary(result = {}) {
|
|
250
|
+
const status = result.status || result.action || "unknown";
|
|
251
|
+
const lines = [
|
|
252
|
+
"## Buildchain production release PR handoff",
|
|
253
|
+
"",
|
|
254
|
+
`- status: \`${status}\``,
|
|
255
|
+
`- mode: \`${result.mode || "auto"}\``,
|
|
256
|
+
`- source: \`${result.sourceSha || ""}\``,
|
|
257
|
+
`- release branch: \`${result.branchName || ""}\``,
|
|
258
|
+
];
|
|
259
|
+
if (result.pullUrl) lines.push(`- pull request: ${result.pullUrl}`);
|
|
260
|
+
if (result.error?.message) lines.push(`- error: \`${result.error.message.replace(/`/g, "'")}\``);
|
|
261
|
+
if (result.manualCommand) {
|
|
262
|
+
lines.push("", "Manual PR creation command:", "", "```bash", result.manualCommand, "```");
|
|
263
|
+
}
|
|
264
|
+
return `${lines.join("\n")}\n`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function writeGitHubOutputs(env, outputs) {
|
|
268
|
+
for (const [name, value] of Object.entries(outputs)) {
|
|
269
|
+
const normalized = String(value ?? "");
|
|
270
|
+
console.log(`${name}=${normalized}`);
|
|
271
|
+
if (env.GITHUB_OUTPUT) {
|
|
272
|
+
fs.appendFileSync(env.GITHUB_OUTPUT, `${name}=${normalized}\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
156
277
|
async function githubJson({ apiUrl, token, method = "GET", path, body }) {
|
|
157
278
|
const response = await fetch(`${apiUrl.replace(/\/$/, "")}${path}`, {
|
|
158
279
|
method,
|
|
@@ -264,28 +385,19 @@ export async function openProductionReleasePr({
|
|
|
264
385
|
serverUrl = "https://github.com",
|
|
265
386
|
releasePassportArtifact = "buildchain-web-surface-staging-release-passport",
|
|
266
387
|
} = {}) {
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const normalizedSourceSha = requiredString(sourceSha, "sourceSha");
|
|
271
|
-
const label = requiredString(productionReleaseLabel, "productionReleaseLabel");
|
|
272
|
-
const branchName = releaseBranchName({
|
|
273
|
-
prefix: productionReleaseHeadPrefix || "release/",
|
|
274
|
-
channel: productionReleaseChannel || "production",
|
|
275
|
-
sourceSha: normalizedSourceSha,
|
|
276
|
-
});
|
|
277
|
-
const workflowRunUrl = runUrl({ serverUrl, repository, runId });
|
|
278
|
-
const body = renderProductionReleasePrBody({
|
|
388
|
+
const handoff = createProductionReleasePrHandoff({
|
|
389
|
+
repository,
|
|
390
|
+
sourceSha,
|
|
279
391
|
stagingResult,
|
|
280
|
-
|
|
281
|
-
|
|
392
|
+
productionReleaseLabel,
|
|
393
|
+
productionReleaseHeadPrefix,
|
|
394
|
+
productionReleaseChannel,
|
|
395
|
+
runId,
|
|
396
|
+
serverUrl,
|
|
282
397
|
releasePassportArtifact,
|
|
283
|
-
workflowRunUrl,
|
|
284
|
-
productionReleaseLabel: label,
|
|
285
|
-
branchName,
|
|
286
398
|
});
|
|
287
|
-
const
|
|
288
|
-
const
|
|
399
|
+
const { owner, repo, branchName, title, body, head } = handoff;
|
|
400
|
+
const normalizedToken = requiredString(token, "token");
|
|
289
401
|
const existing = await githubJson({
|
|
290
402
|
apiUrl,
|
|
291
403
|
token: normalizedToken,
|
|
@@ -300,13 +412,14 @@ export async function openProductionReleasePr({
|
|
|
300
412
|
path: `/repos/${owner}/${repo}/pulls/${pull.number}`,
|
|
301
413
|
body: { title, body },
|
|
302
414
|
});
|
|
303
|
-
await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label });
|
|
415
|
+
await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label: handoff.label });
|
|
304
416
|
return {
|
|
305
417
|
action: "updated",
|
|
418
|
+
status: "updated",
|
|
419
|
+
...handoff,
|
|
306
420
|
branchName,
|
|
307
421
|
pullNumber: pull.number,
|
|
308
422
|
pullUrl: pull.html_url,
|
|
309
|
-
sourceSha: normalizedSourceSha,
|
|
310
423
|
};
|
|
311
424
|
}
|
|
312
425
|
|
|
@@ -316,8 +429,8 @@ export async function openProductionReleasePr({
|
|
|
316
429
|
owner,
|
|
317
430
|
repo,
|
|
318
431
|
branchName,
|
|
319
|
-
sourceSha:
|
|
320
|
-
message: `buildchain release intent: ${productionReleaseChannel} ${
|
|
432
|
+
sourceSha: handoff.sourceSha,
|
|
433
|
+
message: `buildchain release intent: ${productionReleaseChannel} ${handoff.sourceSha.slice(0, 12)}`,
|
|
321
434
|
});
|
|
322
435
|
const pull = await githubJson({
|
|
323
436
|
apiUrl,
|
|
@@ -332,23 +445,27 @@ export async function openProductionReleasePr({
|
|
|
332
445
|
maintainer_can_modify: true,
|
|
333
446
|
},
|
|
334
447
|
});
|
|
335
|
-
await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label });
|
|
448
|
+
await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label: handoff.label });
|
|
336
449
|
return {
|
|
337
450
|
action: "created",
|
|
451
|
+
status: "created",
|
|
452
|
+
...handoff,
|
|
338
453
|
branchName,
|
|
339
454
|
commitSha,
|
|
340
455
|
pullNumber: pull.number,
|
|
341
456
|
pullUrl: pull.html_url,
|
|
342
|
-
sourceSha: normalizedSourceSha,
|
|
343
457
|
};
|
|
344
458
|
}
|
|
345
459
|
|
|
346
460
|
export async function webSurfaceProductionReleasePrCli(env = process.env) {
|
|
347
461
|
const stagingResult = readStagingReleasePrSummary(env);
|
|
348
462
|
const sourceSha = optionalString(stagingResult.sourceSha) || requiredString(env.GITHUB_SHA, "GITHUB_SHA");
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
463
|
+
const mode = optionalString(env.PRODUCTION_RELEASE_PR_MODE || "auto") || "auto";
|
|
464
|
+
if (!["auto", "summary-only", "disabled"].includes(mode)) {
|
|
465
|
+
throw new Error("PRODUCTION_RELEASE_PR_MODE must be auto, summary-only, or disabled");
|
|
466
|
+
}
|
|
467
|
+
const failOnReleasePrError = parseBoolean(env.FAIL_ON_RELEASE_PR_ERROR, false);
|
|
468
|
+
const handoff = createProductionReleasePrHandoff({
|
|
352
469
|
repository: env.GITHUB_REPOSITORY,
|
|
353
470
|
sourceSha,
|
|
354
471
|
stagingResult,
|
|
@@ -359,19 +476,75 @@ export async function webSurfaceProductionReleasePrCli(env = process.env) {
|
|
|
359
476
|
serverUrl: env.GITHUB_SERVER_URL || "https://github.com",
|
|
360
477
|
releasePassportArtifact: env.RELEASE_PASSPORT_ARTIFACT || "buildchain-web-surface-staging-release-passport",
|
|
361
478
|
});
|
|
362
|
-
|
|
479
|
+
|
|
480
|
+
const bodyPath = optionalString(env.PRODUCTION_RELEASE_PR_BODY_PATH) || ".buildchain/production-release-pr/body.md";
|
|
481
|
+
const summaryPath = optionalString(env.PRODUCTION_RELEASE_PR_SUMMARY_PATH) || ".buildchain/production-release-pr/handoff.json";
|
|
482
|
+
writeTextFile(bodyPath, `${handoff.body}\n`);
|
|
483
|
+
|
|
484
|
+
let result = {
|
|
485
|
+
...handoff,
|
|
486
|
+
mode,
|
|
487
|
+
status: mode,
|
|
488
|
+
action: "skipped",
|
|
489
|
+
pullNumber: "",
|
|
490
|
+
pullUrl: "",
|
|
491
|
+
bodyPath,
|
|
492
|
+
summaryPath,
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
if (mode === "auto") {
|
|
496
|
+
try {
|
|
497
|
+
result = {
|
|
498
|
+
...result,
|
|
499
|
+
...(await openProductionReleasePr({
|
|
500
|
+
apiUrl: env.GITHUB_API_URL || "https://api.github.com",
|
|
501
|
+
token: env.GITHUB_TOKEN,
|
|
502
|
+
repository: env.GITHUB_REPOSITORY,
|
|
503
|
+
sourceSha,
|
|
504
|
+
stagingResult,
|
|
505
|
+
productionReleaseLabel: env.PRODUCTION_RELEASE_LABEL || "buildchain-release",
|
|
506
|
+
productionReleaseHeadPrefix: env.PRODUCTION_RELEASE_HEAD_PREFIX || "release/",
|
|
507
|
+
productionReleaseChannel: env.PRODUCTION_RELEASE_CHANNEL || "production",
|
|
508
|
+
runId: env.GITHUB_RUN_ID,
|
|
509
|
+
serverUrl: env.GITHUB_SERVER_URL || "https://github.com",
|
|
510
|
+
releasePassportArtifact: env.RELEASE_PASSPORT_ARTIFACT || "buildchain-web-surface-staging-release-passport",
|
|
511
|
+
})),
|
|
512
|
+
mode,
|
|
513
|
+
};
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const status = classifyReleasePrError(error);
|
|
516
|
+
result = {
|
|
517
|
+
...result,
|
|
518
|
+
status,
|
|
519
|
+
action: status,
|
|
520
|
+
error: {
|
|
521
|
+
status: error.status || "",
|
|
522
|
+
message: error.message || String(error),
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
if (status !== "permission-denied" || failOnReleasePrError) {
|
|
526
|
+
writeJsonFile(summaryPath, result);
|
|
527
|
+
if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, renderStepSummary(result));
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
writeJsonFile(summaryPath, result);
|
|
534
|
+
if (env.GITHUB_STEP_SUMMARY) {
|
|
535
|
+
fs.appendFileSync(env.GITHUB_STEP_SUMMARY, renderStepSummary(result));
|
|
536
|
+
}
|
|
537
|
+
writeGitHubOutputs(env, {
|
|
538
|
+
"release-pr-status": result.status,
|
|
539
|
+
"production-release-pr-status": result.status,
|
|
363
540
|
"production-release-pr-action": result.action,
|
|
364
541
|
"production-release-pr": String(result.pullNumber || ""),
|
|
365
542
|
"production-release-pr-url": result.pullUrl || "",
|
|
366
543
|
"production-release-branch": result.branchName || "",
|
|
367
544
|
"production-release-source-sha": result.sourceSha || "",
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const fs = await import("node:fs");
|
|
372
|
-
fs.appendFileSync(env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
545
|
+
"production-release-pr-summary-path": summaryPath,
|
|
546
|
+
"production-release-pr-body-path": bodyPath,
|
|
547
|
+
});
|
|
375
548
|
return result;
|
|
376
549
|
}
|
|
377
550
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parse as parseToml } from "smol-toml";
|
|
5
|
+
import { resolveBuildchainConfigPath } from "../packages/core/buildchain-layout.js";
|
|
5
6
|
|
|
6
7
|
export const RELEASE_REVIEW_MARKER = "<!-- buildchain:web-surface-release-review -->";
|
|
7
8
|
|
|
@@ -51,7 +52,7 @@ export function resolveReleaseReviewState(payload, options = {}) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function loadWebSurfaceReleaseUrls(cwd = ".") {
|
|
54
|
-
const configPath = path.join(cwd,
|
|
55
|
+
const configPath = path.join(cwd, resolveBuildchainConfigPath(cwd));
|
|
55
56
|
const raw = fs.readFileSync(configPath, "utf8");
|
|
56
57
|
const config = parseToml(raw);
|
|
57
58
|
const stagingUrl = String(config.channels?.staging?.url || "").trim();
|