@kungfu-tech/buildchain 2.8.7-alpha.1 → 2.8.7

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.
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+
3
+ function requiredString(value, name) {
4
+ const normalized = String(value || "").trim();
5
+ if (!normalized) throw new Error(`${name} is required`);
6
+ return normalized;
7
+ }
8
+
9
+ function optionalString(value = "") {
10
+ return String(value || "").trim();
11
+ }
12
+
13
+ function slugify(value) {
14
+ return String(value || "")
15
+ .trim()
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9._-]+/g, "-")
18
+ .replace(/^-+|-+$/g, "") || "production";
19
+ }
20
+
21
+ function parseJson(value, name) {
22
+ const normalized = optionalString(value);
23
+ if (!normalized) return {};
24
+ try {
25
+ return JSON.parse(normalized);
26
+ } catch (error) {
27
+ throw new Error(`${name} must be valid JSON: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ function runUrl({ serverUrl = "", repository = "", runId = "" } = {}) {
32
+ if (!serverUrl || !repository || !runId) return "";
33
+ return `${serverUrl.replace(/\/$/, "")}/${repository}/actions/runs/${runId}`;
34
+ }
35
+
36
+ function urlsFromResult(result = {}) {
37
+ const urls = result.urls && typeof result.urls === "object" ? result.urls : {};
38
+ if (Object.keys(urls).length > 0) return urls;
39
+ return result.url ? { default: result.url } : {};
40
+ }
41
+
42
+ export function releaseBranchName({ prefix = "release/", channel = "production", sourceSha = "" } = {}) {
43
+ const normalizedPrefix = optionalString(prefix) || "release/";
44
+ const normalizedChannel = slugify(channel || "production");
45
+ const shortSha = requiredString(sourceSha, "sourceSha").slice(0, 12);
46
+ return `${normalizedPrefix}${normalizedChannel}-${shortSha}`;
47
+ }
48
+
49
+ export function renderProductionReleasePrBody({
50
+ stagingResult = {},
51
+ sourceSha = "",
52
+ artifactHash = "",
53
+ releasePassportArtifact = "buildchain-web-surface-staging-release-passport",
54
+ workflowRunUrl = "",
55
+ productionReleaseLabel = "buildchain-release",
56
+ branchName = "",
57
+ } = {}) {
58
+ const urls = urlsFromResult(stagingResult);
59
+ const urlLines = Object.entries(urls).length
60
+ ? Object.entries(urls).map(([surface, url]) => `- ${surface}: ${url}`)
61
+ : ["- (no staging URL reported)"];
62
+ const passportLine = workflowRunUrl
63
+ ? `[${releasePassportArtifact}](${workflowRunUrl})`
64
+ : `\`${releasePassportArtifact}\``;
65
+ return `<!-- buildchain:web-surface-production-release-pr -->
66
+ ## Buildchain production release intent
67
+
68
+ Staging has been deployed from the current main commit. Review the staging URLs,
69
+ then merge this PR to approve production. Buildchain will only publish
70
+ production after it verifies that the merged PR is a same-repository release PR
71
+ with the required label.
72
+
73
+ ### Staging URLs
74
+
75
+ ${urlLines.join("\n")}
76
+
77
+ ### Release Evidence
78
+
79
+ - Source SHA: \`${sourceSha}\`
80
+ - Artifact hash: \`${artifactHash || "not reported"}\`
81
+ - Staging release passport: ${passportLine}
82
+ - Required label: \`${productionReleaseLabel}\`
83
+ - Release branch: \`${branchName}\`
84
+
85
+ This PR intentionally contains one empty release-intent commit.`;
86
+ }
87
+
88
+ async function githubJson({ apiUrl, token, method = "GET", path, body }) {
89
+ const response = await fetch(`${apiUrl.replace(/\/$/, "")}${path}`, {
90
+ method,
91
+ headers: {
92
+ accept: "application/vnd.github+json",
93
+ authorization: `Bearer ${token}`,
94
+ "content-type": "application/json",
95
+ "x-github-api-version": "2022-11-28",
96
+ },
97
+ body: body === undefined ? undefined : JSON.stringify(body),
98
+ });
99
+ if (!response.ok) {
100
+ const text = await response.text();
101
+ const detail = text ? `: ${text.slice(0, 500)}` : "";
102
+ const error = new Error(`GitHub API ${method} ${path} failed: HTTP ${response.status}${detail}`);
103
+ error.status = response.status;
104
+ throw error;
105
+ }
106
+ return response.status === 204 ? {} : response.json();
107
+ }
108
+
109
+ async function ensureLabel({ apiUrl, token, owner, repo, label }) {
110
+ if (!label) return;
111
+ try {
112
+ await githubJson({ apiUrl, token, path: `/repos/${owner}/${repo}/labels/${encodeURIComponent(label)}` });
113
+ } catch (error) {
114
+ if (error.status !== 404) throw error;
115
+ await githubJson({
116
+ apiUrl,
117
+ token,
118
+ method: "POST",
119
+ path: `/repos/${owner}/${repo}/labels`,
120
+ body: {
121
+ name: label,
122
+ color: "0e8a16",
123
+ description: "Buildchain production release approval PR",
124
+ },
125
+ });
126
+ }
127
+ }
128
+
129
+ async function addLabel({ apiUrl, token, owner, repo, pullNumber, label }) {
130
+ if (!label) return;
131
+ await ensureLabel({ apiUrl, token, owner, repo, label });
132
+ await githubJson({
133
+ apiUrl,
134
+ token,
135
+ method: "POST",
136
+ path: `/repos/${owner}/${repo}/issues/${pullNumber}/labels`,
137
+ body: { labels: [label] },
138
+ });
139
+ }
140
+
141
+ async function createOrUpdateBranch({ apiUrl, token, owner, repo, branchName, sourceSha, message }) {
142
+ const baseCommit = await githubJson({
143
+ apiUrl,
144
+ token,
145
+ path: `/repos/${owner}/${repo}/git/commits/${sourceSha}`,
146
+ });
147
+ const createdCommit = await githubJson({
148
+ apiUrl,
149
+ token,
150
+ method: "POST",
151
+ path: `/repos/${owner}/${repo}/git/commits`,
152
+ body: {
153
+ message,
154
+ tree: baseCommit.tree.sha,
155
+ parents: [sourceSha],
156
+ },
157
+ });
158
+ const ref = `heads/${branchName}`;
159
+ try {
160
+ await githubJson({
161
+ apiUrl,
162
+ token,
163
+ method: "POST",
164
+ path: `/repos/${owner}/${repo}/git/refs`,
165
+ body: {
166
+ ref: `refs/${ref}`,
167
+ sha: createdCommit.sha,
168
+ },
169
+ });
170
+ } catch (error) {
171
+ if (error.status !== 422) throw error;
172
+ await githubJson({
173
+ apiUrl,
174
+ token,
175
+ method: "PATCH",
176
+ path: `/repos/${owner}/${repo}/git/refs/${encodeURIComponent(ref).replace(/%2F/g, "/")}`,
177
+ body: {
178
+ sha: createdCommit.sha,
179
+ force: true,
180
+ },
181
+ });
182
+ }
183
+ return createdCommit.sha;
184
+ }
185
+
186
+ export async function openProductionReleasePr({
187
+ apiUrl = "https://api.github.com",
188
+ token,
189
+ repository,
190
+ sourceSha,
191
+ stagingResult,
192
+ productionReleaseLabel = "buildchain-release",
193
+ productionReleaseHeadPrefix = "release/",
194
+ productionReleaseChannel = "production",
195
+ runId = "",
196
+ serverUrl = "https://github.com",
197
+ releasePassportArtifact = "buildchain-web-surface-staging-release-passport",
198
+ } = {}) {
199
+ const [owner, repo] = requiredString(repository, "repository").split("/");
200
+ if (!owner || !repo) throw new Error(`invalid repository: ${repository}`);
201
+ const normalizedToken = requiredString(token, "token");
202
+ const normalizedSourceSha = requiredString(sourceSha, "sourceSha");
203
+ const label = requiredString(productionReleaseLabel, "productionReleaseLabel");
204
+ const branchName = releaseBranchName({
205
+ prefix: productionReleaseHeadPrefix || "release/",
206
+ channel: productionReleaseChannel || "production",
207
+ sourceSha: normalizedSourceSha,
208
+ });
209
+ const workflowRunUrl = runUrl({ serverUrl, repository, runId });
210
+ const body = renderProductionReleasePrBody({
211
+ stagingResult,
212
+ sourceSha: normalizedSourceSha,
213
+ artifactHash: stagingResult.artifactHash || "",
214
+ releasePassportArtifact,
215
+ workflowRunUrl,
216
+ productionReleaseLabel: label,
217
+ branchName,
218
+ });
219
+ const title = `Release production from ${normalizedSourceSha.slice(0, 12)}`;
220
+ const head = `${owner}:${branchName}`;
221
+ const existing = await githubJson({
222
+ apiUrl,
223
+ token: normalizedToken,
224
+ path: `/repos/${owner}/${repo}/pulls?state=open&base=main&head=${encodeURIComponent(head)}`,
225
+ });
226
+ if (Array.isArray(existing) && existing.length > 0) {
227
+ const pull = existing[0];
228
+ await githubJson({
229
+ apiUrl,
230
+ token: normalizedToken,
231
+ method: "PATCH",
232
+ path: `/repos/${owner}/${repo}/pulls/${pull.number}`,
233
+ body: { title, body },
234
+ });
235
+ await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label });
236
+ return {
237
+ action: "updated",
238
+ branchName,
239
+ pullNumber: pull.number,
240
+ pullUrl: pull.html_url,
241
+ sourceSha: normalizedSourceSha,
242
+ };
243
+ }
244
+
245
+ const commitSha = await createOrUpdateBranch({
246
+ apiUrl,
247
+ token: normalizedToken,
248
+ owner,
249
+ repo,
250
+ branchName,
251
+ sourceSha: normalizedSourceSha,
252
+ message: `buildchain release intent: ${productionReleaseChannel} ${normalizedSourceSha.slice(0, 12)}`,
253
+ });
254
+ const pull = await githubJson({
255
+ apiUrl,
256
+ token: normalizedToken,
257
+ method: "POST",
258
+ path: `/repos/${owner}/${repo}/pulls`,
259
+ body: {
260
+ title,
261
+ head: branchName,
262
+ base: "main",
263
+ body,
264
+ maintainer_can_modify: true,
265
+ },
266
+ });
267
+ await addLabel({ apiUrl, token: normalizedToken, owner, repo, pullNumber: pull.number, label });
268
+ return {
269
+ action: "created",
270
+ branchName,
271
+ commitSha,
272
+ pullNumber: pull.number,
273
+ pullUrl: pull.html_url,
274
+ sourceSha: normalizedSourceSha,
275
+ };
276
+ }
277
+
278
+ export async function webSurfaceProductionReleasePrCli(env = process.env) {
279
+ const stagingResult = parseJson(env.STAGING_APPLY_RESULT_JSON, "STAGING_APPLY_RESULT_JSON");
280
+ const sourceSha = optionalString(stagingResult.sourceSha) || requiredString(env.GITHUB_SHA, "GITHUB_SHA");
281
+ const result = await openProductionReleasePr({
282
+ apiUrl: env.GITHUB_API_URL || "https://api.github.com",
283
+ token: env.GITHUB_TOKEN,
284
+ repository: env.GITHUB_REPOSITORY,
285
+ sourceSha,
286
+ stagingResult,
287
+ productionReleaseLabel: env.PRODUCTION_RELEASE_LABEL || "buildchain-release",
288
+ productionReleaseHeadPrefix: env.PRODUCTION_RELEASE_HEAD_PREFIX || "release/",
289
+ productionReleaseChannel: env.PRODUCTION_RELEASE_CHANNEL || "production",
290
+ runId: env.GITHUB_RUN_ID,
291
+ serverUrl: env.GITHUB_SERVER_URL || "https://github.com",
292
+ releasePassportArtifact: env.RELEASE_PASSPORT_ARTIFACT || "buildchain-web-surface-staging-release-passport",
293
+ });
294
+ for (const [name, value] of Object.entries({
295
+ "production-release-pr-action": result.action,
296
+ "production-release-pr": String(result.pullNumber || ""),
297
+ "production-release-pr-url": result.pullUrl || "",
298
+ "production-release-branch": result.branchName || "",
299
+ "production-release-source-sha": result.sourceSha || "",
300
+ })) {
301
+ console.log(`${name}=${value}`);
302
+ if (env.GITHUB_OUTPUT) {
303
+ const fs = await import("node:fs");
304
+ fs.appendFileSync(env.GITHUB_OUTPUT, `${name}=${value}\n`);
305
+ }
306
+ }
307
+ return result;
308
+ }
309
+
310
+ if (import.meta.url === `file://${process.argv[1]}`) {
311
+ webSurfaceProductionReleasePrCli().catch((error) => {
312
+ console.error(error.stack || error.message);
313
+ process.exitCode = 1;
314
+ });
315
+ }