@kungfu-tech/buildchain 3.0.2-alpha.2 → 3.0.2-alpha.3

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.
@@ -94,9 +94,10 @@ jobs:
94
94
 
95
95
  | Preset | Platforms |
96
96
  | ----------------------- | ------------------------------------------------------------------------ |
97
- | `github-hosted` | `ubuntu-24.04`, `macos-latest`, `windows-2022` |
98
- | `kungfu-v4-self-hosted` | Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
99
- | `custom` | Requires `platforms-json` |
97
+ | `github-hosted` | `ubuntu-24.04`, `macos-latest`, `windows-2022` |
98
+ | `kungfu-v4-self-hosted` | Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
99
+ | `kungfu-v4-native` | Kungfu Linux x64, Linux ARM64, macOS ARM64, and Windows x64; Linux ARM64 uses GitHub-hosted `ubuntu-24.04-arm` |
100
+ | `custom` | Requires `platforms-json` |
100
101
 
101
102
  Callers can still provide a custom matrix with `platforms-json`. Each platform
102
103
  object has:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.2-alpha.2",
3
+ "version": "3.0.2-alpha.3",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -20,6 +20,7 @@
20
20
  "./anchored-version-material": "./packages/core/anchored-version-material.js",
21
21
  "./buildchain-contract": "./packages/core/buildchain-contract.js",
22
22
  "./candidate-timeline": "./packages/core/candidate-timeline.js",
23
+ "./channel-candidate": "./packages/core/channel-candidate.js",
23
24
  "./cache-evidence": "./packages/core/cache-evidence.js",
24
25
  "./controller-evidence": "./packages/core/controller-evidence.js",
25
26
  "./diagnostics": "./packages/core/diagnostics.js",
@@ -27,6 +27,7 @@ const DESCRIPTORS = Object.freeze([
27
27
  [".github/workflows/buildchain-patrol.yml", "governance-write"],
28
28
  [".github/workflows/buildchain-ref-promotion.yml", "governance-write"],
29
29
  [".github/workflows/buildchain-stable-candidate-patrol.yml", "governance-write"],
30
+ [".github/workflows/dev-alpha-candidate-patrol.yml", "governance-write"],
30
31
  [".github/workflows/dev-merge-queue-governance.yml", "governance-write"],
31
32
  [".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
32
33
  [".github/workflows/github-governance-audit.yml", "governance-write"],
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import crypto from "node:crypto";
4
+
5
+ export const CHANNEL_CANDIDATE_DECISION_SCHEMA =
6
+ "kungfu-buildchain-channel-candidate-decision/v1";
7
+
8
+ const SHA = /^[0-9a-f]{40}$/;
9
+
10
+ function canonical(value) {
11
+ if (Array.isArray(value)) return value.map(canonical);
12
+ if (value && typeof value === "object") {
13
+ return Object.fromEntries(
14
+ Object.entries(value)
15
+ .sort(([left], [right]) => left.localeCompare(right))
16
+ .map(([key, item]) => [key, canonical(item)]),
17
+ );
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function root(value) {
23
+ return `sha256:${crypto
24
+ .createHash("sha256")
25
+ .update(JSON.stringify(canonical(value)))
26
+ .digest("hex")}`;
27
+ }
28
+
29
+ function required(value, name) {
30
+ const normalized = String(value || "").trim();
31
+ if (!normalized) throw new Error(`${name} is required`);
32
+ return normalized;
33
+ }
34
+
35
+ function sha(value, name) {
36
+ const normalized = required(value, name).toLowerCase();
37
+ if (!SHA.test(normalized)) throw new Error(`${name} must be an exact 40-character SHA`);
38
+ return normalized;
39
+ }
40
+
41
+ function qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds }) {
42
+ if (!row || typeof row !== "object") throw new Error("workflow evidence is required");
43
+ const workflowPath = required(row.workflowPath, "workflowPath");
44
+ if (sha(row.headSha, `${workflowPath} headSha`) !== sourceSha) {
45
+ throw new Error(`${workflowPath} evidence does not bind source SHA ${sourceSha}`);
46
+ }
47
+ if (row.status !== "completed" || row.conclusion !== "success") {
48
+ throw new Error(`${workflowPath} is not a completed successful run`);
49
+ }
50
+ const completedAt = required(row.completedAt, `${workflowPath} completedAt`);
51
+ const ageSeconds = (Date.parse(now) - Date.parse(completedAt)) / 1000;
52
+ if (!Number.isFinite(ageSeconds) || ageSeconds < 0 || ageSeconds > maxAgeSeconds) {
53
+ throw new Error(`${workflowPath} evidence is stale or has an invalid completion time`);
54
+ }
55
+ const runId = Number(row.runId);
56
+ const runAttempt = Number(row.runAttempt || 1);
57
+ if (!Number.isSafeInteger(runId) || runId <= 0) throw new Error(`${workflowPath} runId is invalid`);
58
+ if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0) throw new Error(`${workflowPath} runAttempt is invalid`);
59
+ return {
60
+ workflowPath,
61
+ workflowName: required(row.workflowName, `${workflowPath} workflowName`),
62
+ runId,
63
+ runAttempt,
64
+ headSha: sourceSha,
65
+ status: "completed",
66
+ conclusion: "success",
67
+ completedAt,
68
+ url: required(row.url, `${workflowPath} url`),
69
+ };
70
+ }
71
+
72
+ export function channelCandidateSourceLockRef(targetBranch, sourceSha) {
73
+ const target = required(targetBranch, "targetBranch");
74
+ const exactSha = sha(sourceSha, "sourceSha");
75
+ return `buildchain/candidate/${target.replace(/[^A-Za-z0-9._-]+/g, "-")}/${exactSha.slice(0, 12)}`;
76
+ }
77
+
78
+ export function decideChannelCandidate(input) {
79
+ const repository = required(input.repository, "repository");
80
+ const sourceBranch = required(input.sourceBranch, "sourceBranch");
81
+ const targetBranch = required(input.targetBranch, "targetBranch");
82
+ if (sourceBranch === targetBranch) throw new Error("sourceBranch and targetBranch must differ");
83
+ const sourceSha = sha(input.sourceSha, "sourceSha");
84
+ const targetSha = sha(input.targetSha, "targetSha");
85
+ const now = required(input.now || new Date().toISOString(), "now");
86
+ const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
87
+ if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
88
+ throw new Error("maxAgeSeconds must be a positive integer");
89
+ }
90
+ const comparison = input.comparison || {};
91
+ if (comparison.status !== "ahead" || Number(comparison.aheadBy) <= 0) {
92
+ const reason = comparison.status === "identical" ? "target-already-current" : "source-does-not-lead-target";
93
+ return {
94
+ schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
95
+ eligible: false,
96
+ reason,
97
+ repository,
98
+ source: { branch: sourceBranch, sha: sourceSha },
99
+ target: { branch: targetBranch, sha: targetSha },
100
+ comparison: { status: String(comparison.status || "unknown"), aheadBy: Number(comparison.aheadBy || 0) },
101
+ decidedAt: now,
102
+ };
103
+ }
104
+ const rows = Array.isArray(input.workflowEvidence) ? input.workflowEvidence : [];
105
+ const expectedPaths = [...new Set((input.requiredWorkflowPaths || []).map(String))];
106
+ if (expectedPaths.length === 0) throw new Error("requiredWorkflowPaths must not be empty");
107
+ if (rows.length !== expectedPaths.length) {
108
+ throw new Error(`expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`);
109
+ }
110
+ const byPath = new Map();
111
+ for (const row of rows) {
112
+ const qualified = qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds });
113
+ if (byPath.has(qualified.workflowPath)) {
114
+ throw new Error(`duplicate workflow evidence: ${qualified.workflowPath}`);
115
+ }
116
+ byPath.set(qualified.workflowPath, qualified);
117
+ }
118
+ for (const workflowPath of expectedPaths) {
119
+ if (!byPath.has(workflowPath)) throw new Error(`missing workflow evidence: ${workflowPath}`);
120
+ }
121
+ const body = {
122
+ schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
123
+ eligible: true,
124
+ reason: "same-source-qualified",
125
+ repository,
126
+ source: { branch: sourceBranch, sha: sourceSha },
127
+ target: { branch: targetBranch, sha: targetSha },
128
+ comparison: { status: "ahead", aheadBy: Number(comparison.aheadBy) },
129
+ sourceLockRef: channelCandidateSourceLockRef(targetBranch, sourceSha),
130
+ workflowEvidence: expectedPaths.map((workflowPath) => byPath.get(workflowPath)),
131
+ policy: { maxAgeSeconds, requiredWorkflowPaths: expectedPaths },
132
+ decidedAt: now,
133
+ };
134
+ return { ...body, decisionRoot: root(body) };
135
+ }
@@ -100,6 +100,12 @@ export {
100
100
  normalizeCandidateTimelineEvent,
101
101
  } from "./candidate-timeline.js";
102
102
 
103
+ export {
104
+ CHANNEL_CANDIDATE_DECISION_SCHEMA,
105
+ channelCandidateSourceLockRef,
106
+ decideChannelCandidate,
107
+ } from "./channel-candidate.js";
108
+
103
109
  export {
104
110
  BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT,
105
111
  BUILDCHAIN_DIAGNOSTICS_CONTRACT,
@@ -31,6 +31,36 @@ export const RUNNER_PRESETS = Object.freeze({
31
31
  capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
32
32
  },
33
33
  ],
34
+ "kungfu-v4-native": [
35
+ {
36
+ id: "linux-x64",
37
+ name: "Linux x64",
38
+ platform: "linux",
39
+ runner: '["self-hosted","Linux","X64","kungfu-build-v4-linux-x64"]',
40
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
41
+ },
42
+ {
43
+ id: "linux-arm64",
44
+ name: "Linux ARM64",
45
+ platform: "linux",
46
+ runner: '["ubuntu-24.04-arm"]',
47
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
48
+ },
49
+ {
50
+ id: "macos-arm64",
51
+ name: "macOS ARM64",
52
+ platform: "macos",
53
+ runner: '["self-hosted","macOS","ARM64","kungfu-build-v4-macos-arm64"]',
54
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
55
+ },
56
+ {
57
+ id: "windows-x64",
58
+ name: "Windows x64",
59
+ platform: "windows",
60
+ runner: '["self-hosted","Windows","X64","kungfu-build-v4-windows-x64"]',
61
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
62
+ },
63
+ ],
34
64
  });
35
65
 
36
66
  export const LINUX_CONTAINER_PRESETS = Object.freeze({
@@ -46,6 +76,7 @@ const RUNNER_PRESET_ALIASES = Object.freeze({
46
76
  kungfu: "kungfu-v4-self-hosted",
47
77
  "kungfu-self-hosted": "kungfu-v4-self-hosted",
48
78
  "kungfu-v4": "kungfu-v4-self-hosted",
79
+ "kungfu-v4-four-platform": "kungfu-v4-native",
49
80
  });
50
81
 
51
82
  const LINUX_CONTAINER_PRESET_ALIASES = Object.freeze({
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { decideChannelCandidate } from "../packages/core/channel-candidate.js";
7
+
8
+ function text(value = "") {
9
+ return String(value ?? "").trim();
10
+ }
11
+
12
+ function bool(value, fallback = false) {
13
+ if (value === undefined || value === null || value === "") return fallback;
14
+ return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
15
+ }
16
+
17
+ function repository(value) {
18
+ const normalized = text(value);
19
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) throw new Error(`repository must be owner/repo, got ${value || "<empty>"}`);
20
+ return normalized;
21
+ }
22
+
23
+ function branch(value, name) {
24
+ const normalized = text(value).replace(/^refs\/heads\//, "");
25
+ if (!normalized || normalized.startsWith("-") || /[\s~^:?*[\\]/.test(normalized)) {
26
+ throw new Error(`${name} is not a valid branch name`);
27
+ }
28
+ return normalized;
29
+ }
30
+
31
+ function workflowPath(value, name) {
32
+ const normalized = text(value);
33
+ if (!/^\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml$/.test(normalized)) {
34
+ throw new Error(`${name} must be a repository workflow path`);
35
+ }
36
+ return normalized;
37
+ }
38
+
39
+ function integer(value, fallback) {
40
+ const parsed = Number(value);
41
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
42
+ }
43
+
44
+ export function normalizeDevAlphaPatrolOptions(options = {}) {
45
+ return {
46
+ repository: repository(options.repository ?? process.env.BUILDCHAIN_CHANNEL_PATROL_REPOSITORY ?? process.env.GITHUB_REPOSITORY),
47
+ sourceBranch: branch(options.sourceBranch ?? process.env.BUILDCHAIN_CHANNEL_PATROL_SOURCE_BRANCH ?? "dev/v4/v4.0", "sourceBranch"),
48
+ targetBranch: branch(options.targetBranch ?? process.env.BUILDCHAIN_CHANNEL_PATROL_TARGET_BRANCH ?? "alpha/v4/v4.0", "targetBranch"),
49
+ devWorkflowPath: workflowPath(options.devWorkflowPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DEV_WORKFLOW ?? ".github/workflows/dev-verify-patrol.yml", "devWorkflowPath"),
50
+ alphaWorkflowPath: workflowPath(options.alphaWorkflowPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_ALPHA_WORKFLOW ?? ".github/workflows/alpha-promotion-preflight.yml", "alphaWorkflowPath"),
51
+ maxAgeSeconds: integer(options.maxAgeSeconds ?? process.env.BUILDCHAIN_CHANNEL_PATROL_MAX_AGE_SECONDS, 7 * 24 * 60 * 60),
52
+ createPullRequest: bool(options.createPullRequest ?? process.env.BUILDCHAIN_CHANNEL_PATROL_CREATE_PR, false),
53
+ dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN, true),
54
+ now: text(options.now ?? process.env.BUILDCHAIN_CHANNEL_PATROL_NOW) || new Date().toISOString(),
55
+ outputPath: text(options.outputPath ?? process.env.BUILDCHAIN_CHANNEL_PATROL_OUTPUT_PATH) || ".buildchain/patrol/dev-alpha-candidate.json",
56
+ };
57
+ }
58
+
59
+ function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
60
+ const matching = runs
61
+ .filter((run) => run.path === workflowPathValue && run.head_sha === sourceSha)
62
+ .sort((left, right) => Number(right.id) - Number(left.id));
63
+ if (matching.length === 0) throw new Error(`missing completed same-SHA workflow run: ${workflowPathValue}`);
64
+ const run = matching[0];
65
+ return {
66
+ workflowPath: workflowPathValue,
67
+ workflowName: run.name,
68
+ runId: run.id,
69
+ runAttempt: run.run_attempt,
70
+ headSha: run.head_sha,
71
+ status: run.status,
72
+ conclusion: run.conclusion,
73
+ completedAt: run.updated_at,
74
+ url: run.html_url,
75
+ };
76
+ }
77
+
78
+ export async function runDevAlphaCandidatePatrol(optionsInput = {}, clientInput) {
79
+ const options = normalizeDevAlphaPatrolOptions(optionsInput);
80
+ if (options.sourceBranch === options.targetBranch) throw new Error("source and target branches must differ");
81
+ const client = clientInput || createGitHubChannelCandidateClient({
82
+ repository: options.repository,
83
+ token: process.env.GITHUB_TOKEN,
84
+ });
85
+ const [sourceSha, targetSha] = await Promise.all([
86
+ client.resolveBranch(options.sourceBranch),
87
+ client.resolveBranch(options.targetBranch),
88
+ ]);
89
+ const comparison = await client.compare(targetSha, sourceSha);
90
+ const requiredWorkflowPaths = [options.devWorkflowPath, options.alphaWorkflowPath];
91
+ let workflowEvidence = [];
92
+ if (comparison.status === "ahead" && Number(comparison.ahead_by) > 0) {
93
+ const runs = await client.listCompletedRuns(sourceSha);
94
+ workflowEvidence = requiredWorkflowPaths.map((workflow) =>
95
+ latestWorkflowEvidence(runs, workflow, sourceSha),
96
+ );
97
+ }
98
+ const decision = decideChannelCandidate({
99
+ repository: options.repository,
100
+ sourceBranch: options.sourceBranch,
101
+ targetBranch: options.targetBranch,
102
+ sourceSha,
103
+ targetSha,
104
+ comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
105
+ workflowEvidence,
106
+ requiredWorkflowPaths,
107
+ maxAgeSeconds: options.maxAgeSeconds,
108
+ now: options.now,
109
+ });
110
+ let pullRequest;
111
+ if (decision.eligible && options.createPullRequest && !options.dryRun) {
112
+ await client.ensureImmutableBranch(decision.sourceLockRef, sourceSha);
113
+ pullRequest = await client.ensurePullRequest({
114
+ head: decision.sourceLockRef,
115
+ base: options.targetBranch,
116
+ title: `Promote qualified ${options.sourceBranch} candidate ${sourceSha.slice(0, 12)} to ${options.targetBranch}`,
117
+ body: [
118
+ "Buildchain exact-source channel candidate.",
119
+ "",
120
+ `- Source branch: \`${options.sourceBranch}\``,
121
+ `- Source SHA: \`${sourceSha}\``,
122
+ `- Target branch/head: \`${options.targetBranch}\` / \`${targetSha}\``,
123
+ `- Decision root: \`${decision.decisionRoot}\``,
124
+ ...decision.workflowEvidence.map(
125
+ (row) => `- ${row.workflowName}: [run ${row.runId} attempt ${row.runAttempt}](${row.url})`,
126
+ ),
127
+ "",
128
+ "The source-lock branch must continue to point at the exact source SHA. This patrol never merges the PR, publishes a package, creates a tag, or creates a release.",
129
+ ].join("\n"),
130
+ });
131
+ }
132
+ return {
133
+ schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
134
+ dryRun: options.dryRun,
135
+ createPullRequest: options.createPullRequest,
136
+ decision,
137
+ pullRequest: pullRequest || null,
138
+ };
139
+ }
140
+
141
+ function encodeRef(value) {
142
+ return value.split("/").map(encodeURIComponent).join("/");
143
+ }
144
+
145
+ export function createGitHubChannelCandidateClient({ repository: repositoryInput, token, fetchImpl = globalThis.fetch }) {
146
+ const [owner, repo] = repository(repositoryInput).split("/");
147
+ const headers = {
148
+ accept: "application/vnd.github+json",
149
+ authorization: token ? `Bearer ${token}` : undefined,
150
+ "user-agent": "buildchain-dev-alpha-candidate-patrol",
151
+ "x-github-api-version": "2022-11-28",
152
+ };
153
+ async function api(requestPath, { method = "GET", body, allow404 = false } = {}) {
154
+ const response = await fetchImpl(`https://api.github.com${requestPath}`, {
155
+ method,
156
+ headers: Object.fromEntries(Object.entries(headers).filter(([, value]) => value)),
157
+ body: body === undefined ? undefined : JSON.stringify(body),
158
+ });
159
+ const raw = await response.text();
160
+ const payload = raw ? JSON.parse(raw) : undefined;
161
+ if (allow404 && response.status === 404) return undefined;
162
+ if (!response.ok) throw new Error(`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`);
163
+ return payload;
164
+ }
165
+ return {
166
+ async resolveBranch(ref) {
167
+ const payload = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`);
168
+ return text(payload.object?.sha);
169
+ },
170
+ async compare(baseSha, headSha) {
171
+ return api(`/repos/${owner}/${repo}/compare/${baseSha}...${headSha}`);
172
+ },
173
+ async listCompletedRuns(headSha) {
174
+ const payload = await api(`/repos/${owner}/${repo}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=completed&per_page=100`);
175
+ return payload.workflow_runs || [];
176
+ },
177
+ async ensureImmutableBranch(ref, sourceSha) {
178
+ const current = await api(`/repos/${owner}/${repo}/git/ref/heads/${encodeRef(ref)}`, { allow404: true });
179
+ if (current && current.object?.sha !== sourceSha) {
180
+ throw new Error(`source-lock branch ${ref} points to ${current.object?.sha}, not ${sourceSha}`);
181
+ }
182
+ if (current) return current;
183
+ return api(`/repos/${owner}/${repo}/git/refs`, {
184
+ method: "POST",
185
+ body: { ref: `refs/heads/${ref}`, sha: sourceSha },
186
+ });
187
+ },
188
+ async ensurePullRequest({ head, base, title, body }) {
189
+ const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);
190
+ return open[0] || api(`/repos/${owner}/${repo}/pulls`, {
191
+ method: "POST",
192
+ body: { head, base, title, body },
193
+ });
194
+ },
195
+ };
196
+ }
197
+
198
+ function markdown(result) {
199
+ return [
200
+ "## Buildchain Dev to Alpha candidate patrol",
201
+ "",
202
+ `Eligible: \`${result.decision.eligible}\` (${result.decision.reason})`,
203
+ `Source: \`${result.decision.source.branch}@${result.decision.source.sha}\``,
204
+ `Target: \`${result.decision.target.branch}@${result.decision.target.sha}\``,
205
+ `Dry run: \`${result.dryRun}\``,
206
+ `Pull request: ${result.pullRequest?.html_url || "not created"}`,
207
+ "",
208
+ ].join("\n");
209
+ }
210
+
211
+ async function main() {
212
+ const options = normalizeDevAlphaPatrolOptions();
213
+ const result = await runDevAlphaCandidatePatrol(options);
214
+ fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
215
+ fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
216
+ const summary = markdown(result);
217
+ if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
218
+ else process.stdout.write(summary);
219
+ if (process.env.GITHUB_OUTPUT) {
220
+ const outputs = {
221
+ "result-path": options.outputPath,
222
+ eligible: String(result.decision.eligible),
223
+ "selected-sha": result.decision.source.sha,
224
+ "source-lock-ref": result.decision.sourceLockRef || "",
225
+ "promotion-pr": result.pullRequest?.html_url || "",
226
+ };
227
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `${Object.entries(outputs).map(([key, value]) => `${key}=${value}`).join("\n")}\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
+ }
@@ -337,6 +337,7 @@ const manualMetaById = new Map(Object.entries({
337
337
  "release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
338
338
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
339
339
  "stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
340
+ "dev-alpha-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 137 },
340
341
  "observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
341
342
  "reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
342
343
  "lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
@@ -516,6 +517,7 @@ function nodeApiMeta(exportName) {
516
517
  "./homebrew": { group: "distribution-indexes", summary: "Homebrew tap fact collection, Formula rendering, update, and check APIs." },
517
518
  "./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
518
519
  "./candidate-timeline": { group: "observability-diagnostics", summary: "Source-bound candidate event normalization, per-attempt critical-path-safe aggregation, and compact reporting APIs." },
520
+ "./channel-candidate": { group: "governance-versioning", summary: "Exact-source channel candidate decisions, same-SHA workflow evidence validation, and deterministic source-lock reference APIs." },
519
521
  "./cache-evidence": { group: "observability-diagnostics", summary: "Content-addressed cache operation receipts and source/platform-bound evidence-set verification APIs." },
520
522
  "./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
521
523
  "./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
@@ -814,6 +816,7 @@ function buildSiteBundle() {
814
816
  "docs/reusable-build-surface.md",
815
817
  "docs/release-candidate.md",
816
818
  "docs/stable-candidate-patrol.md",
819
+ "docs/dev-alpha-candidate-patrol.md",
817
820
  "docs/observed-evidence-patrol.md",
818
821
  "docs/release-governance.md",
819
822
  "docs/release-passport.md",
@@ -900,6 +903,7 @@ function buildSiteBundle() {
900
903
  ["buildchain-patrol-weekly", "repository-patrol"],
901
904
  ["buildchain-patrol-monthly", "repository-patrol"],
902
905
  ["stable-candidate-patrol", "repository-patrol"],
906
+ ["dev-alpha-candidate-patrol", "repository-patrol"],
903
907
  ["buildchain-stable-candidate-patrol", "repository-patrol"],
904
908
  ["buildchain-stable-candidate-qualification", "repository-patrol"],
905
909
  ["patrol-daily", "repository-patrol"],