@kungfu-tech/buildchain 0.0.0-bootstrap.0 → 2.0.13-alpha.10

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.
Files changed (35) hide show
  1. package/README.md +262 -0
  2. package/bin/buildchain.mjs +222 -0
  3. package/docs/cli.md +124 -0
  4. package/docs/lifecycle-protocol.md +422 -0
  5. package/docs/publish-transaction.md +285 -0
  6. package/docs/reusable-build-surface.md +350 -0
  7. package/docs/web-surface-deployments.md +211 -0
  8. package/package.json +52 -1
  9. package/packages/core/README.md +15 -0
  10. package/packages/core/buildchain-config.js +721 -0
  11. package/packages/core/index.js +40 -0
  12. package/packages/core/package-manager.js +291 -0
  13. package/packages/core/publish-transaction.js +418 -0
  14. package/packages/core/release-line-dry-run.js +296 -0
  15. package/scripts/aggregate-build-summary.mjs +88 -0
  16. package/scripts/build-contract-core.mjs +731 -0
  17. package/scripts/check-inventory.mjs +325 -0
  18. package/scripts/init-repo.mjs +316 -0
  19. package/scripts/npm-publish-dry-run.mjs +176 -0
  20. package/scripts/npm-publish-transaction.mjs +268 -0
  21. package/scripts/publish-source-ref-resolver.mjs +113 -0
  22. package/scripts/release-line-dry-run.mjs +53 -0
  23. package/scripts/release-line-policy.mjs +141 -0
  24. package/scripts/release-transaction.mjs +212 -0
  25. package/scripts/resolve-build-contract.mjs +63 -0
  26. package/scripts/resolve-publish-gate.mjs +33 -0
  27. package/scripts/resolve-publish-source.mjs +99 -0
  28. package/scripts/run-lifecycle-core.mjs +162 -0
  29. package/scripts/run-lifecycle.mjs +40 -0
  30. package/scripts/strip-trailing-whitespace.mjs +14 -0
  31. package/scripts/tsup-action.config.mjs +19 -0
  32. package/scripts/verify-publish-source-lock.mjs +37 -0
  33. package/scripts/verify-release-pr.mjs +34 -0
  34. package/scripts/web-surface-core.mjs +382 -0
  35. package/scripts/web-surface.mjs +112 -0
@@ -0,0 +1,296 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import {
5
+ getLifecycleStage,
6
+ getVersionStrategy,
7
+ loadBuildchainConfig,
8
+ loadConfiguredAnchorManifest,
9
+ } from "./buildchain-config.js";
10
+
11
+ const MAJOR_GATE_REF = "publish-gate/major";
12
+ const LEGACY_MAJOR_GATE_REF = "major-gate";
13
+
14
+ function assertShaIfPresent(sha) {
15
+ if (sha && !/^[0-9a-f]{40}$/i.test(String(sha))) {
16
+ throw new Error(`Invalid commit SHA: ${sha}`);
17
+ }
18
+ }
19
+
20
+ function parseTags(input) {
21
+ const tags = Array.isArray(input)
22
+ ? input.map((tag) => String(tag).trim()).filter(Boolean)
23
+ : String(input || "").split(",").map((tag) => tag.trim()).filter(Boolean);
24
+ for (const tag of tags) {
25
+ if (
26
+ !/^v\d+$|^v\d+\.\d+$|^v\d+\.\d+-alpha$|^v\d+\.\d+\.\d+$|^v\d+\.\d+\.\d+-alpha\.\d+$/.test(tag)
27
+ ) {
28
+ throw new Error(`Unsupported buildchain dry-run tag: ${tag}`);
29
+ }
30
+ }
31
+ return [...new Set(tags)];
32
+ }
33
+
34
+ function getPromotionRule(targetRef, sourceRef = "") {
35
+ if (targetRef === MAJOR_GATE_REF || targetRef === LEGACY_MAJOR_GATE_REF) {
36
+ const sourceMatch = String(sourceRef || "").match(/^release\/v(\d+)\/v(\d+)\.(\d+)$/);
37
+ const sourceMajor = sourceMatch ? Number(sourceMatch[1]) : undefined;
38
+ const nextMajor = sourceMajor ? sourceMajor + 1 : undefined;
39
+ return {
40
+ channel: "major",
41
+ targetRef,
42
+ sourceRef: sourceRef || "release/vN/vN.M",
43
+ releasePrefix: nextMajor ? `v${nextMajor}.0` : "v(N+1).0",
44
+ majorTag: nextMajor ? `v${nextMajor}` : "v(N+1)",
45
+ minorTag: nextMajor ? `v${nextMajor}.0` : "v(N+1).0",
46
+ alphaTag: nextMajor ? `v${nextMajor}.0-alpha` : "v(N+1).0-alpha",
47
+ exactReleasePattern: nextMajor ? `v${nextMajor}.0.0` : "v(N+1).0.0",
48
+ exactAlphaPattern: nextMajor ? `v${nextMajor}.0.1-alpha.0` : "v(N+1).0.1-alpha.0",
49
+ };
50
+ }
51
+ const match = String(targetRef || "").match(/^(alpha|release)\/v(\d+)\/v(\d+)\.(\d+)$/);
52
+ if (!match) {
53
+ throw new Error(
54
+ `Release dry-run target must be alpha/vN/vN.M, release/vN/vN.M, publish-gate/major, or major-gate; got ${targetRef}`,
55
+ );
56
+ }
57
+ const channel = match[1];
58
+ const major = Number(match[2]);
59
+ const minorMajor = Number(match[3]);
60
+ const minor = Number(match[4]);
61
+ if (major !== minorMajor) {
62
+ throw new Error(`Release dry-run target major mismatch: ${targetRef}`);
63
+ }
64
+ const releasePrefix = `v${major}.${minor}`;
65
+ return {
66
+ channel,
67
+ targetRef,
68
+ major,
69
+ minor,
70
+ releasePrefix,
71
+ majorTag: `v${major}`,
72
+ minorTag: releasePrefix,
73
+ alphaTag: `${releasePrefix}-alpha`,
74
+ sourceRef: channel === "alpha" ? `dev/v${major}/v${major}.${minor}` : `alpha/v${major}/v${major}.${minor}`,
75
+ exactReleasePattern: `${releasePrefix}.Z`,
76
+ exactAlphaPattern: `${releasePrefix}.Z-alpha.N`,
77
+ };
78
+ }
79
+
80
+ function discoverVersionFiles(cwd, loadedConfig) {
81
+ if (loadedConfig?.config?.version?.files?.length) {
82
+ return {
83
+ manager: "buildchain.toml",
84
+ files: loadedConfig.config.version.files.map((file) => file.path).filter(Boolean),
85
+ reason: "configured-version-files",
86
+ };
87
+ }
88
+ const files = [];
89
+ for (const relativePath of ["lerna.json", "package.json"]) {
90
+ const filePath = path.join(cwd, relativePath);
91
+ if (fs.existsSync(filePath)) {
92
+ try {
93
+ const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
94
+ if (typeof json.version === "string") {
95
+ files.push(relativePath);
96
+ }
97
+ } catch {
98
+ // Keep dry-run advisory-only. Validation catches malformed files elsewhere.
99
+ }
100
+ }
101
+ }
102
+ return {
103
+ manager: files.length ? "package-manager" : "none",
104
+ files,
105
+ reason: files.length ? "default-node-version-files" : "no-version-state-discovered",
106
+ };
107
+ }
108
+
109
+ function currentGitHead(cwd) {
110
+ try {
111
+ return execFileSync("git", ["rev-parse", "HEAD"], {
112
+ cwd,
113
+ encoding: "utf8",
114
+ stdio: ["ignore", "pipe", "ignore"],
115
+ }).trim();
116
+ } catch {
117
+ return "";
118
+ }
119
+ }
120
+
121
+ function firstMatching(tags, pattern) {
122
+ return tags.find((tag) => pattern.test(tag)) || "";
123
+ }
124
+
125
+ function explainReleaseLineDryRun({
126
+ cwd = process.cwd(),
127
+ targetRef,
128
+ sha = "",
129
+ sourceRef = "",
130
+ tags,
131
+ publishTransaction = false,
132
+ publishCommand = "",
133
+ } = {}) {
134
+ if (!targetRef) {
135
+ throw new Error("release dry-run requires --target-ref");
136
+ }
137
+ const resolvedSha = sha || currentGitHead(cwd);
138
+ assertShaIfPresent(resolvedSha);
139
+ const explicitTags = parseTags(tags);
140
+ const rule = getPromotionRule(targetRef, sourceRef);
141
+ const loadedConfig = loadBuildchainConfig(cwd);
142
+ const versionFiles = discoverVersionFiles(cwd, loadedConfig);
143
+ const versionStrategy = getVersionStrategy(loadedConfig);
144
+ const anchorManifest = loadConfiguredAnchorManifest(cwd, loadedConfig);
145
+ const lifecycleVerify = getLifecycleStage(loadedConfig, "verify");
146
+ const lifecyclePublish = getLifecycleStage(loadedConfig, "publish");
147
+ const publishEnabled = Boolean(publishTransaction || publishCommand || lifecyclePublish);
148
+
149
+ const plan = {
150
+ schemaVersion: 1,
151
+ dryRun: true,
152
+ cwd,
153
+ targetRef,
154
+ source: {
155
+ expectedHeadRef: rule.sourceRef,
156
+ sha: resolvedSha || "unknown",
157
+ },
158
+ channel: rule.channel,
159
+ line: rule.releasePrefix,
160
+ exactTags: [],
161
+ floatingRefs: [],
162
+ branchUpdates: [],
163
+ versionState: {
164
+ manager: versionFiles.manager,
165
+ files: versionFiles.files,
166
+ reason: versionFiles.reason,
167
+ strategy: versionStrategy.strategy,
168
+ next: versionStrategy.next,
169
+ anchorManifest: anchorManifest?.path || "",
170
+ verification: lifecycleVerify ? "lifecycle.verify" : "not-configured",
171
+ },
172
+ governanceChecks: [
173
+ "target branch protection must be readable",
174
+ "branch protection must enforce administrators",
175
+ "required pull request review must be enabled",
176
+ "strict required status check must include the Verify check",
177
+ `source PR must be a merged same-repository PR from ${rule.sourceRef} to ${targetRef}`,
178
+ ],
179
+ publishTransaction: {
180
+ enabled: publishEnabled,
181
+ source: publishCommand ? "publish-command" : lifecyclePublish ? "lifecycle.publish" : publishTransaction ? "workflow-input" : "none",
182
+ behavior: publishEnabled
183
+ ? "would create or resume durable release transaction and require publish evidence before public refs move"
184
+ : "not part of this dry-run unless lifecycle.publish or publish transaction input is enabled",
185
+ },
186
+ notes: [],
187
+ };
188
+
189
+ if (rule.channel === "alpha") {
190
+ const exactAlpha = firstMatching(explicitTags, /^v\d+\.\d+\.\d+-alpha\.\d+$/) || `next ${rule.exactAlphaPattern}`;
191
+ plan.exactTags.push({
192
+ tag: exactAlpha,
193
+ kind: "alpha",
194
+ action: "would create or reuse immutable alpha evidence tag",
195
+ });
196
+ plan.floatingRefs.push({ ref: rule.alphaTag, kind: "tag", action: "would move to alpha version-state commit" });
197
+ plan.branchUpdates.push(
198
+ { ref: targetRef, action: "would move to alpha version-state commit" },
199
+ { ref: `dev/v${rule.major}/v${rule.major}.${rule.minor}`, action: "would align dev with the published alpha state" },
200
+ );
201
+ plan.versionState.targetVersions = [exactAlpha.replace(/^next v?/, "").replace(/^v/, "")];
202
+ plan.notes.push("Alpha promotion opens or advances the test channel; it does not move production refs.");
203
+ } else if (rule.channel === "release") {
204
+ const exactRelease = firstMatching(explicitTags, /^v\d+\.\d+\.\d+$/) || `next ${rule.exactReleasePattern}`;
205
+ const exactAlpha = firstMatching(explicitTags, /^v\d+\.\d+\.\d+-alpha\.\d+$/) || `next ${rule.releasePrefix}.(Z+1)-alpha.0`;
206
+ plan.exactTags.push(
207
+ { tag: exactRelease, kind: "release", action: "would create or reuse immutable production evidence tag" },
208
+ { tag: exactAlpha, kind: "next-alpha", action: "would create or reuse immutable next-alpha evidence tag" },
209
+ );
210
+ plan.floatingRefs.push(
211
+ { ref: rule.minorTag, kind: "tag", action: "would move to production release commit" },
212
+ { ref: rule.majorTag, kind: "tag", action: "would move when no newer minor line owns the major tag" },
213
+ { ref: rule.alphaTag, kind: "tag", action: "would move to next alpha version-state commit" },
214
+ );
215
+ plan.branchUpdates.push(
216
+ { ref: targetRef, action: "would move to production release commit" },
217
+ { ref: `alpha/v${rule.major}/v${rule.major}.${rule.minor}`, action: "would move to next alpha version-state commit" },
218
+ { ref: `dev/v${rule.major}/v${rule.major}.${rule.minor}`, action: "would move to next alpha version-state commit" },
219
+ );
220
+ plan.governanceChecks.push("release source tree must match the same-patch exact alpha tag tree except generated version-state files");
221
+ plan.versionState.targetVersions = [
222
+ exactRelease.replace(/^next v?/, "").replace(/^v/, ""),
223
+ exactAlpha.replace(/^next v?/, "").replace(/^v/, ""),
224
+ ];
225
+ plan.notes.push("Release promotion publishes production and immediately prepares the next alpha state for the same minor line.");
226
+ } else {
227
+ const exactRelease = firstMatching(explicitTags, /^v\d+\.\d+\.0$/) || rule.exactReleasePattern;
228
+ const exactAlpha = firstMatching(explicitTags, /^v\d+\.\d+\.1-alpha\.0$/) || rule.exactAlphaPattern;
229
+ plan.exactTags.push(
230
+ { tag: exactRelease, kind: "release", action: "would create the first production patch of the next major line" },
231
+ { tag: exactAlpha, kind: "next-alpha", action: "would prepare the first next-alpha patch of the next major line" },
232
+ );
233
+ plan.floatingRefs.push(
234
+ { ref: rule.minorTag, kind: "tag", action: "would move to next-major release commit" },
235
+ { ref: rule.majorTag, kind: "tag", action: "would move to next-major release commit" },
236
+ { ref: rule.alphaTag, kind: "tag", action: "would move to next-major alpha commit" },
237
+ );
238
+ plan.branchUpdates.push(
239
+ { ref: targetRef, action: "would move to next-major release commit" },
240
+ { ref: `release/${rule.majorTag}/${rule.minorTag}`, action: "would move to next-major release commit" },
241
+ { ref: `alpha/${rule.majorTag}/${rule.minorTag}`, action: "would move to next-major alpha commit" },
242
+ { ref: `dev/${rule.majorTag}/${rule.minorTag}`, action: "would move to next-major alpha commit and become the default branch" },
243
+ );
244
+ plan.versionState.targetVersions = [
245
+ exactRelease.replace(/^v/, ""),
246
+ exactAlpha.replace(/^v/, ""),
247
+ ];
248
+ plan.notes.push("Major promotion is a reviewed administrator gate; the gate branch is a frozen release source, not an active trunk.");
249
+ }
250
+
251
+ if (versionFiles.files.length === 0) {
252
+ plan.notes.push("No version-state file was discovered; strict promotion would fail unless the caller disables required version state.");
253
+ }
254
+ if (versionStrategy.next === "manual") {
255
+ plan.notes.push("Manual next-anchor strategy means production release will stop with next-anchor-required instead of auto-preparing the next alpha.");
256
+ }
257
+ return plan;
258
+ }
259
+
260
+ function formatReleaseLineDryRun(plan) {
261
+ const lines = [
262
+ "Buildchain release dry-run",
263
+ `- target ref: ${plan.targetRef}`,
264
+ `- expected source: ${plan.source.expectedHeadRef}`,
265
+ `- source sha: ${plan.source.sha}`,
266
+ `- channel: ${plan.channel}`,
267
+ `- line: ${plan.line}`,
268
+ "- exact tags:",
269
+ ...plan.exactTags.map((tag) => ` - ${tag.tag}: ${tag.action}`),
270
+ "- branch updates:",
271
+ ...plan.branchUpdates.map((update) => ` - ${update.ref}: ${update.action}`),
272
+ "- floating refs:",
273
+ ...plan.floatingRefs.map((update) => ` - ${update.ref}: ${update.action}`),
274
+ `- version state: ${plan.versionState.manager} (${plan.versionState.reason})`,
275
+ ];
276
+ if (plan.versionState.files.length > 0) {
277
+ lines.push(` - files: ${plan.versionState.files.join(", ")}`);
278
+ }
279
+ lines.push(
280
+ ` - strategy: ${plan.versionState.strategy}/${plan.versionState.next}`,
281
+ ` - verification: ${plan.versionState.verification}`,
282
+ `- publish transaction: ${plan.publishTransaction.enabled ? "enabled" : "not enabled"} (${plan.publishTransaction.source})`,
283
+ "- governance checks:",
284
+ ...plan.governanceChecks.map((check) => ` - ${check}`),
285
+ );
286
+ if (plan.notes.length > 0) {
287
+ lines.push("- notes:", ...plan.notes.map((note) => ` - ${note}`));
288
+ }
289
+ lines.push("No refs, tags, packages, or files were modified.");
290
+ return `${lines.join("\n")}\n`;
291
+ }
292
+
293
+ export {
294
+ explainReleaseLineDryRun,
295
+ formatReleaseLineDryRun,
296
+ };
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { findJsonFiles, writeGitHubOutputs } from "./build-contract-core.mjs";
6
+
7
+ function readEnv(name, fallback = "") {
8
+ return process.env[name] || fallback;
9
+ }
10
+
11
+ export function aggregateBuildSummaryCli() {
12
+ const inputRoot = path.resolve(readEnv("BUILDCHAIN_SUMMARY_INPUT", ".buildchain/downloaded-manifests"));
13
+ const outputPath = path.resolve(readEnv("BUILDCHAIN_SUMMARY_OUTPUT", ".buildchain/artifacts/build-summary.json"));
14
+ const artifactName = readEnv("BUILDCHAIN_ARTIFACT_NAME", "buildchain-artifact");
15
+ const expectedPlatformCount = Number(readEnv("BUILDCHAIN_PLATFORM_COUNT", "0"));
16
+ const manifestFiles = findJsonFiles(inputRoot)
17
+ .filter((file) => path.basename(file) === "manifest.json")
18
+ .sort();
19
+ const manifests = manifestFiles.map((file) => JSON.parse(fs.readFileSync(file, "utf8")));
20
+ if (expectedPlatformCount > 0 && manifests.length !== expectedPlatformCount) {
21
+ throw new Error(
22
+ `expected ${expectedPlatformCount} platform manifests, found ${manifests.length} under ${inputRoot}`,
23
+ );
24
+ }
25
+ const summary = {
26
+ contract: "kungfu-buildchain-build-summary",
27
+ artifactName,
28
+ git: {
29
+ repository: process.env.GITHUB_REPOSITORY || "",
30
+ sha: process.env.BUILDCHAIN_SOURCE_SHA || process.env.GITHUB_SHA || "",
31
+ ref: process.env.BUILDCHAIN_SOURCE_REF || process.env.GITHUB_REF || "",
32
+ runId: process.env.GITHUB_RUN_ID || "",
33
+ runAttempt: process.env.GITHUB_RUN_ATTEMPT || "",
34
+ },
35
+ publishGate: {
36
+ trustedEvent: readEnv("BUILDCHAIN_TRUSTED_EVENT", "true") === "true",
37
+ channel: readEnv("BUILDCHAIN_PUBLISH_CHANNEL", "none"),
38
+ allowed: readEnv("BUILDCHAIN_PUBLISH_ALLOWED", "false") === "true",
39
+ reason: readEnv("BUILDCHAIN_PUBLISH_REASON", ""),
40
+ },
41
+ publishSource: {
42
+ ref: readEnv("BUILDCHAIN_PUBLISH_SOURCE_REF", ""),
43
+ sha: readEnv("BUILDCHAIN_PUBLISH_SOURCE_SHA", ""),
44
+ locked: readEnv("BUILDCHAIN_PUBLISH_SOURCE_LOCKED", "false") === "true",
45
+ channel: readEnv("BUILDCHAIN_PUBLISH_SOURCE_CHANNEL", "none"),
46
+ line: readEnv("BUILDCHAIN_PUBLISH_SOURCE_LINE", ""),
47
+ consumerVersion: readEnv("BUILDCHAIN_PUBLISH_SOURCE_CONSUMER_VERSION", ""),
48
+ releaseManifest: readEnv("BUILDCHAIN_RELEASE_MANIFEST_JSON", ""),
49
+ },
50
+ platformCount: manifests.length,
51
+ fileCount: manifests.reduce((sum, manifest) => sum + Number(manifest.summary?.fileCount || 0), 0),
52
+ totalBytes: manifests.reduce((sum, manifest) => sum + Number(manifest.summary?.totalBytes || 0), 0),
53
+ platforms: manifests.map((manifest, index) => ({
54
+ artifactName: manifest.artifactName,
55
+ platform: manifest.platform,
56
+ summary: manifest.summary,
57
+ expectedArtifacts: manifest.expectedArtifacts,
58
+ manifestPath: manifestFiles[index],
59
+ })),
60
+ };
61
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
62
+ fs.writeFileSync(outputPath, `${JSON.stringify(summary, null, 2)}\n`);
63
+ writeGitHubOutputs({
64
+ "summary-path": path.relative(process.cwd(), outputPath).split(path.sep).join("/"),
65
+ "platform-count": String(summary.platformCount),
66
+ "artifact-file-count": String(summary.fileCount),
67
+ "artifact-total-bytes": String(summary.totalBytes),
68
+ "artifact-summary-json": JSON.stringify({
69
+ contract: summary.contract,
70
+ artifactName: summary.artifactName,
71
+ platformCount: summary.platformCount,
72
+ fileCount: summary.fileCount,
73
+ totalBytes: summary.totalBytes,
74
+ publishGate: summary.publishGate,
75
+ publishSource: summary.publishSource,
76
+ }),
77
+ });
78
+ return summary;
79
+ }
80
+
81
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
82
+ try {
83
+ aggregateBuildSummaryCli();
84
+ } catch (error) {
85
+ console.error(`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`);
86
+ process.exitCode = 1;
87
+ }
88
+ }