@kungfu-tech/buildchain 2.12.1-alpha.3 → 2.12.1-alpha.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.
@@ -732,7 +732,8 @@ for (const requiredSnippet of [
732
732
  "../packages/core/logging.js",
733
733
  "standalone.cli-bundle.create",
734
734
  "BUILDCHAIN_EMBEDDED_PACKAGE_VERSION",
735
- "noExternal: [\"smol-toml\"]",
735
+ "BUILDCHAIN_EMBEDDED_ENTRYPOINT",
736
+ "noExternal: [\"smol-toml\",",
736
737
  "--macho-segment-name",
737
738
  "mainFormat: \"commonjs\"",
738
739
  "standalone.sea-blob.create",
@@ -833,6 +833,7 @@ function buildSiteBundle() {
833
833
  ["buildchain-patrol-monthly", "repository-patrol"],
834
834
  ["stable-candidate-patrol", "repository-patrol"],
835
835
  ["buildchain-stable-candidate-patrol", "repository-patrol"],
836
+ ["buildchain-stable-candidate-qualification", "repository-patrol"],
836
837
  ["patrol-daily", "repository-patrol"],
837
838
  ["patrol-weekly", "repository-patrol"],
838
839
  ["patrol-monthly", "repository-patrol"],
@@ -841,6 +842,7 @@ function buildSiteBundle() {
841
842
  ["release-propagation", "preview"],
842
843
  ["candidate-lab", "repository-internal"],
843
844
  ["build-surface-fixture", "repository-internal"],
845
+ ["buildchain-stable-candidate-qualification", "repository-internal"],
844
846
  ["self-hosted-runner-smoke", "compatibility-fixture"],
845
847
  ]);
846
848
  return {
@@ -477,7 +477,7 @@ function readArg(name, fallback = "") {
477
477
  return process.argv[index + 1] || "";
478
478
  }
479
479
 
480
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
480
+ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
481
481
  try {
482
482
  const result = initBuildchainRepo({
483
483
  cwd: readArg("cwd", process.cwd()),
@@ -179,7 +179,13 @@ function hasCommit(targetPath, sha, timeoutMs) {
179
179
  }
180
180
 
181
181
  function checkoutFetchedCommit(targetPath, sha, timeoutMs) {
182
- git(["checkout", "--force", "--detach", sha], { cwd: targetPath, timeoutMs });
182
+ // The locked Git tree is also the byte-level source of release evidence.
183
+ // Override runner-global autocrlf only at checkout time so this also works
184
+ // when a container workspace uses an external Git metadata pointer.
185
+ git(["-c", "core.autocrlf=false", "-c", "core.eol=lf", "checkout", "--force", "--detach", sha], {
186
+ cwd: targetPath,
187
+ timeoutMs,
188
+ });
183
189
  }
184
190
 
185
191
  function retryableGitFetchError(error) {
@@ -155,7 +155,7 @@ function usage() {
155
155
  `;
156
156
  }
157
157
 
158
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
158
+ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
159
159
  try {
160
160
  const argv = process.argv.slice(2);
161
161
  if (hasFlag(argv, "help") || hasFlag(argv, "h")) {
@@ -57,7 +57,7 @@ export function runPublicationArtifactCli(args = process.argv.slice(2)) {
57
57
  return result;
58
58
  }
59
59
 
60
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
60
+ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
61
61
  try {
62
62
  runPublicationArtifactCli();
63
63
  } catch (error) {
@@ -28,7 +28,7 @@ export function runPublicationPackageCli(args = process.argv.slice(2)) {
28
28
  return result;
29
29
  }
30
30
 
31
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
31
+ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
32
32
  try {
33
33
  runPublicationPackageCli();
34
34
  } catch (error) {
@@ -93,6 +93,6 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
93
93
  throw new Error(`unsupported release-propagation command: ${mode}`);
94
94
  }
95
95
 
96
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
96
+ if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
97
97
  runReleasePropagationCli();
98
98
  }
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ const DEFAULTS = {
4
+ buildWorkflowFile: "build-surface-fixture.yml",
5
+ buildWorkflowName: "Build Surface Fixture",
6
+ canaryRepository: "kungfu-systems/site-libkungfu-dev",
7
+ canaryWorkflowFile: "buildchain-stable-canary.yml",
8
+ canaryWorkflowName: "Buildchain Stable Canary",
9
+ canaryStatusContext: "buildchain-canary/site-libkungfu-dev",
10
+ pollAttempts: 80,
11
+ pollIntervalMs: 15_000,
12
+ };
13
+
14
+ function text(value = "") {
15
+ return String(value ?? "").trim();
16
+ }
17
+
18
+ function integer(value, fallback) {
19
+ const parsed = Number(value);
20
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
21
+ }
22
+
23
+ function bool(value, fallback = false) {
24
+ if (value === undefined || value === null || value === "") return fallback;
25
+ return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
26
+ }
27
+
28
+ function repository(value) {
29
+ const normalized = text(value);
30
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) {
31
+ throw new Error(`repository must be owner/repo, got ${value || "<empty>"}`);
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ export function normalizeStableCandidateQualificationOptions(options = {}) {
37
+ const candidateSha = text(options.candidateSha ?? process.env.BUILDCHAIN_QUALIFICATION_CANDIDATE_SHA);
38
+ if (!/^[0-9a-f]{40}$/i.test(candidateSha)) {
39
+ throw new Error(`candidate SHA must be 40 hexadecimal characters, got ${candidateSha || "<empty>"}`);
40
+ }
41
+ return {
42
+ repository: repository(options.repository ?? process.env.BUILDCHAIN_QUALIFICATION_REPOSITORY ?? process.env.GITHUB_REPOSITORY),
43
+ candidateSha,
44
+ buildWorkflowFile: text(options.buildWorkflowFile ?? process.env.BUILDCHAIN_QUALIFICATION_BUILD_WORKFLOW_FILE) || DEFAULTS.buildWorkflowFile,
45
+ buildWorkflowName: text(options.buildWorkflowName ?? process.env.BUILDCHAIN_QUALIFICATION_BUILD_WORKFLOW_NAME) || DEFAULTS.buildWorkflowName,
46
+ canaryRepository: repository(options.canaryRepository ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_REPOSITORY ?? DEFAULTS.canaryRepository),
47
+ canaryWorkflowFile: text(options.canaryWorkflowFile ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_WORKFLOW_FILE) || DEFAULTS.canaryWorkflowFile,
48
+ canaryWorkflowName: text(options.canaryWorkflowName ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_WORKFLOW_NAME) || DEFAULTS.canaryWorkflowName,
49
+ canaryStatusContext: text(options.canaryStatusContext ?? process.env.BUILDCHAIN_QUALIFICATION_CANARY_STATUS_CONTEXT) || DEFAULTS.canaryStatusContext,
50
+ pollAttempts: integer(options.pollAttempts ?? process.env.BUILDCHAIN_QUALIFICATION_POLL_ATTEMPTS, DEFAULTS.pollAttempts),
51
+ pollIntervalMs: integer(options.pollIntervalMs ?? process.env.BUILDCHAIN_QUALIFICATION_POLL_INTERVAL_MS, DEFAULTS.pollIntervalMs),
52
+ dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_QUALIFICATION_DRY_RUN, false),
53
+ };
54
+ }
55
+
56
+ function successful(run) {
57
+ return run?.status === "completed" && run?.conclusion === "success";
58
+ }
59
+
60
+ function active(run) {
61
+ return run && run.status !== "completed";
62
+ }
63
+
64
+ async function ensureWorkflowEvidence({ client, repository, workflowFile, workflowName, ref, headSha, runName, options }) {
65
+ let run = await client.findWorkflowRun({ repository, workflowFile, workflowName, headSha, runName });
66
+ if (successful(run)) return { state: "existing", run };
67
+
68
+ if (!active(run)) {
69
+ if (options.dryRun) return { state: "planned", run };
70
+ await client.dispatchWorkflow({ repository, workflowFile, ref, inputs: runName ? { buildchain_ref: headSha } : {} });
71
+ }
72
+
73
+ if (options.dryRun) return { state: "waiting", run };
74
+ run = await client.waitForWorkflowRun({
75
+ repository,
76
+ workflowFile,
77
+ workflowName,
78
+ headSha,
79
+ runName,
80
+ attempts: options.pollAttempts,
81
+ intervalMs: options.pollIntervalMs,
82
+ });
83
+ if (!successful(run)) {
84
+ throw new Error(`${repository} ${workflowName} did not succeed for ${headSha}: ${run?.conclusion || run?.status || "not-found"}`);
85
+ }
86
+ return { state: "produced", run };
87
+ }
88
+
89
+ export async function runStableCandidateQualification(optionsInput = {}, clientInput) {
90
+ const options = normalizeStableCandidateQualificationOptions(optionsInput);
91
+ const client = clientInput || createGitHubQualificationClient({ token: process.env.GITHUB_TOKEN });
92
+ const candidate = await client.resolveExactAlpha(options.repository, options.candidateSha);
93
+ if (!candidate) {
94
+ return { schemaVersion: 1, contract: "kungfu-buildchain-stable-candidate-qualification", status: "skipped", reason: "exact-alpha-release-not-found", candidateSha: options.candidateSha };
95
+ }
96
+
97
+ const build = await ensureWorkflowEvidence({
98
+ client,
99
+ repository: options.repository,
100
+ workflowFile: options.buildWorkflowFile,
101
+ workflowName: options.buildWorkflowName,
102
+ ref: candidate.tag,
103
+ headSha: options.candidateSha,
104
+ runName: "",
105
+ options,
106
+ });
107
+
108
+ let status = await client.findCommitStatus(options.repository, options.candidateSha, options.canaryStatusContext);
109
+ let canary = { state: status?.state === "success" ? "existing" : "pending", run: undefined };
110
+ if (status?.state !== "success") {
111
+ const canaryDefaultBranch = await client.defaultBranch(options.canaryRepository);
112
+ canary = await ensureWorkflowEvidence({
113
+ client,
114
+ repository: options.canaryRepository,
115
+ workflowFile: options.canaryWorkflowFile,
116
+ workflowName: options.canaryWorkflowName,
117
+ ref: canaryDefaultBranch,
118
+ headSha: options.candidateSha,
119
+ runName: `${options.canaryWorkflowName} / ${options.candidateSha}`,
120
+ options,
121
+ });
122
+ if (!options.dryRun) {
123
+ status = await client.createCommitStatus({
124
+ repository: options.repository,
125
+ sha: options.candidateSha,
126
+ context: options.canaryStatusContext,
127
+ targetUrl: canary.run.html_url,
128
+ description: `No-apply ${options.canaryRepository} canary passed`,
129
+ });
130
+ }
131
+ }
132
+
133
+ return {
134
+ schemaVersion: 1,
135
+ contract: "kungfu-buildchain-stable-candidate-qualification",
136
+ status: options.dryRun ? "planned" : "qualified-evidence-ready",
137
+ dryRun: options.dryRun,
138
+ candidate,
139
+ build: { state: build.state, url: build.run?.html_url || "" },
140
+ canary: { state: canary.state, url: canary.run?.html_url || status?.target_url || "" },
141
+ attestation: { context: options.canaryStatusContext, state: options.dryRun ? "planned" : status?.state || "" },
142
+ };
143
+ }
144
+
145
+ export function createGitHubQualificationClient({
146
+ token,
147
+ attestationToken = process.env.BUILDCHAIN_QUALIFICATION_ATTESTATION_TOKEN || token,
148
+ fetchImpl = globalThis.fetch,
149
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
150
+ }) {
151
+ const headersFor = (requestToken) => ({
152
+ accept: "application/vnd.github+json",
153
+ authorization: requestToken ? `Bearer ${requestToken}` : undefined,
154
+ "user-agent": "buildchain-stable-candidate-qualification",
155
+ "x-github-api-version": "2022-11-28",
156
+ });
157
+ async function api(requestPath, { method = "GET", body, requestToken = token } = {}) {
158
+ const response = await fetchImpl(`https://api.github.com${requestPath}`, {
159
+ method,
160
+ headers: Object.fromEntries(Object.entries(headersFor(requestToken)).filter(([, value]) => value)),
161
+ body: body === undefined ? undefined : JSON.stringify(body),
162
+ });
163
+ const raw = await response.text();
164
+ const payload = raw ? JSON.parse(raw) : undefined;
165
+ if (!response.ok) throw new Error(`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`);
166
+ return payload;
167
+ }
168
+ const encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
169
+ async function resolveTagSha(repositoryName, tag) {
170
+ const ref = await api(`/repos/${repositoryName}/git/ref/tags/${encodeRef(tag)}`);
171
+ if (ref.object?.type !== "tag") return ref.object?.sha || "";
172
+ const annotated = await api(`/repos/${repositoryName}/git/tags/${ref.object.sha}`);
173
+ return annotated.object?.sha || "";
174
+ }
175
+ async function matchingRun(query) {
176
+ const payload = await api(`/repos/${query.repository}/actions/workflows/${encodeURIComponent(query.workflowFile)}/runs?per_page=100`);
177
+ return (payload.workflow_runs || [])
178
+ .filter((run) => (!query.headSha || run.head_sha === query.headSha) && (!query.runName || run.display_title === query.runName || run.name === query.runName))
179
+ .sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)))[0];
180
+ }
181
+ return {
182
+ async resolveExactAlpha(repositoryName, candidateSha) {
183
+ const releases = await api(`/repos/${repositoryName}/releases?per_page=100`);
184
+ for (const release of releases) {
185
+ if (!release.prerelease || !/^v\d+\.\d+\.\d+-alpha\.\d+$/.test(release.tag_name || "")) continue;
186
+ const sha = await resolveTagSha(repositoryName, release.tag_name);
187
+ if (sha === candidateSha) return { version: release.tag_name.slice(1), tag: release.tag_name, sha, releaseUrl: release.html_url };
188
+ }
189
+ return undefined;
190
+ },
191
+ async defaultBranch(repositoryName) {
192
+ return (await api(`/repos/${repositoryName}`)).default_branch;
193
+ },
194
+ findWorkflowRun: matchingRun,
195
+ async dispatchWorkflow({ repository: repositoryName, workflowFile, ref, inputs }) {
196
+ await api(`/repos/${repositoryName}/actions/workflows/${encodeURIComponent(workflowFile)}/dispatches`, { method: "POST", body: { ref, inputs } });
197
+ },
198
+ async waitForWorkflowRun(query) {
199
+ let latest;
200
+ for (let attempt = 0; attempt < query.attempts; attempt += 1) {
201
+ latest = await matchingRun(query);
202
+ if (latest?.status === "completed") return latest;
203
+ await sleep(query.intervalMs);
204
+ }
205
+ return latest;
206
+ },
207
+ async findCommitStatus(repositoryName, sha, context) {
208
+ const statuses = await api(`/repos/${repositoryName}/commits/${sha}/statuses?per_page=100`);
209
+ return statuses.find((entry) => entry.context === context);
210
+ },
211
+ async createCommitStatus({ repository: repositoryName, sha, context, targetUrl, description }) {
212
+ return api(`/repos/${repositoryName}/statuses/${sha}`, {
213
+ method: "POST",
214
+ requestToken: attestationToken,
215
+ body: { state: "success", context, target_url: targetUrl, description },
216
+ });
217
+ },
218
+ };
219
+ }
220
+
221
+ async function main() {
222
+ const result = await runStableCandidateQualification();
223
+ const output = `${JSON.stringify(result, null, 2)}\n`;
224
+ process.stdout.write(output);
225
+ if (process.env.GITHUB_STEP_SUMMARY) {
226
+ const fs = await import("node:fs");
227
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `## Stable candidate qualification\n\n\`\`\`json\n${output}\`\`\`\n`);
228
+ }
229
+ }
230
+
231
+ if (import.meta.url === `file://${process.argv[1]}`) {
232
+ main().catch((error) => {
233
+ console.error(error.stack || error.message);
234
+ process.exit(1);
235
+ });
236
+ }