@kungfu-tech/buildchain 3.0.2-alpha.2 → 3.0.2-alpha.4
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/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +34 -8
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +55 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -1
- package/dist/site/node-api-registry.json +16 -3
- package/dist/site/page-registry.json +29 -3
- package/dist/site/public-surface-audit.json +37 -4
- package/dist/site/publication-authority-registry.json +21 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +32 -0
- package/docs/dev-alpha-candidate-patrol.md +64 -0
- package/docs/reusable-build-surface.md +4 -3
- package/package.json +2 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/channel-candidate.js +186 -0
- package/packages/core/index.js +6 -0
- package/scripts/build-contract-core.mjs +31 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +446 -0
- package/scripts/generate-site-bundle.mjs +4 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: preview
|
|
3
|
+
period: ongoing
|
|
4
|
+
theme: dev-alpha-candidate-patrol
|
|
5
|
+
doc_type: architecture-and-usage
|
|
6
|
+
source_level: local-files
|
|
7
|
+
confidence: high
|
|
8
|
+
sensitivity: public
|
|
9
|
+
evidence_grade: A
|
|
10
|
+
review_state: self-reviewed
|
|
11
|
+
last_reviewed: 2026-07-26
|
|
12
|
+
ai_provenance:
|
|
13
|
+
model_family: GPT-5
|
|
14
|
+
product: Codex
|
|
15
|
+
generated_at: 2026-07-26
|
|
16
|
+
visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.
|
|
17
|
+
invisible_context_boundary: No credentials, private logs, or private configuration were used.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Dev to Alpha Candidate Patrol
|
|
21
|
+
|
|
22
|
+
Buildchain provides a reusable observation and PR controller for repositories
|
|
23
|
+
that promote a development branch into a protected Alpha branch. It does not
|
|
24
|
+
publish Alpha. The controller reads the exact heads of both branches, walks the
|
|
25
|
+
bounded development history from newest to oldest (stopping early at the Alpha
|
|
26
|
+
head), and selects the newest commit that satisfies all of these conditions:
|
|
27
|
+
|
|
28
|
+
- the source is strictly ahead of the recorded target head;
|
|
29
|
+
- the latest completed Dev Patrol for that exact commit SHA succeeded;
|
|
30
|
+
- the latest completed Alpha preflight for the same commit SHA succeeded; and
|
|
31
|
+
- both runs are within the caller's evidence age limit.
|
|
32
|
+
|
|
33
|
+
The selected commit can be behind the observed development head when newer
|
|
34
|
+
commits have not completed both workflows yet. The decision binds the observed
|
|
35
|
+
head, selected SHA, and count of skipped newer commits. This makes a slow native
|
|
36
|
+
verification lane live under continuous development without silently treating
|
|
37
|
+
an unqualified head as releasable.
|
|
38
|
+
|
|
39
|
+
History discovery is bounded to the newest 1000 development commits. The
|
|
40
|
+
controller then compares the selected SHA to the exact Alpha head before it can
|
|
41
|
+
be eligible, so a bounded scan cannot turn a commit outside the promotion
|
|
42
|
+
ancestry into a candidate.
|
|
43
|
+
|
|
44
|
+
The decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the
|
|
45
|
+
source and target branches and SHAs, comparison distance, workflow paths, run
|
|
46
|
+
identities and attempts, completion times, URLs, policy, and a canonical decision
|
|
47
|
+
root. Missing, stale, failed, duplicate, or source-mismatched evidence fails
|
|
48
|
+
closed.
|
|
49
|
+
|
|
50
|
+
## Reusable workflow
|
|
51
|
+
|
|
52
|
+
Call `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository
|
|
53
|
+
workflow. Start with `dry-run: true`. Once the repository has proven that its
|
|
54
|
+
two workflow names and branch topology produce exact same-SHA evidence, it may
|
|
55
|
+
set `create-pull-request: true` and `dry-run: false`.
|
|
56
|
+
|
|
57
|
+
Candidate mode creates one branch named from the target branch and the first 12
|
|
58
|
+
characters of the full source SHA. An existing branch must point to the same
|
|
59
|
+
full SHA or the run fails. The controller then creates or reuses one open pull
|
|
60
|
+
request from that immutable branch to the protected Alpha branch.
|
|
61
|
+
|
|
62
|
+
The workflow never moves the Alpha ref directly, merges or auto-merges the pull
|
|
63
|
+
request, publishes npm, creates a Git tag or GitHub Release, or changes branch
|
|
64
|
+
protection. Those remain repository-owned protected settlement actions.
|
|
@@ -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
|
-
| `
|
|
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.
|
|
3
|
+
"version": "3.0.2-alpha.4",
|
|
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,186 @@
|
|
|
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))
|
|
38
|
+
throw new Error(`${name} must be an exact 40-character SHA`);
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds }) {
|
|
43
|
+
if (!row || typeof row !== "object")
|
|
44
|
+
throw new Error("workflow evidence is required");
|
|
45
|
+
const workflowPath = required(row.workflowPath, "workflowPath");
|
|
46
|
+
if (sha(row.headSha, `${workflowPath} headSha`) !== sourceSha) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${workflowPath} evidence does not bind source SHA ${sourceSha}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (row.status !== "completed" || row.conclusion !== "success") {
|
|
52
|
+
throw new Error(`${workflowPath} is not a completed successful run`);
|
|
53
|
+
}
|
|
54
|
+
const completedAt = required(row.completedAt, `${workflowPath} completedAt`);
|
|
55
|
+
const ageSeconds = (Date.parse(now) - Date.parse(completedAt)) / 1000;
|
|
56
|
+
if (
|
|
57
|
+
!Number.isFinite(ageSeconds) ||
|
|
58
|
+
ageSeconds < 0 ||
|
|
59
|
+
ageSeconds > maxAgeSeconds
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${workflowPath} evidence is stale or has an invalid completion time`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const runId = Number(row.runId);
|
|
66
|
+
const runAttempt = Number(row.runAttempt || 1);
|
|
67
|
+
if (!Number.isSafeInteger(runId) || runId <= 0)
|
|
68
|
+
throw new Error(`${workflowPath} runId is invalid`);
|
|
69
|
+
if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0)
|
|
70
|
+
throw new Error(`${workflowPath} runAttempt is invalid`);
|
|
71
|
+
return {
|
|
72
|
+
workflowPath,
|
|
73
|
+
workflowName: required(row.workflowName, `${workflowPath} workflowName`),
|
|
74
|
+
runId,
|
|
75
|
+
runAttempt,
|
|
76
|
+
headSha: sourceSha,
|
|
77
|
+
status: "completed",
|
|
78
|
+
conclusion: "success",
|
|
79
|
+
completedAt,
|
|
80
|
+
url: required(row.url, `${workflowPath} url`),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function channelCandidateSourceLockRef(targetBranch, sourceSha) {
|
|
85
|
+
const target = required(targetBranch, "targetBranch");
|
|
86
|
+
const exactSha = sha(sourceSha, "sourceSha");
|
|
87
|
+
return `buildchain/candidate/${target.replace(/[^A-Za-z0-9._-]+/g, "-")}/${exactSha.slice(0, 12)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function decideChannelCandidate(input) {
|
|
91
|
+
const repository = required(input.repository, "repository");
|
|
92
|
+
const sourceBranch = required(input.sourceBranch, "sourceBranch");
|
|
93
|
+
const targetBranch = required(input.targetBranch, "targetBranch");
|
|
94
|
+
if (sourceBranch === targetBranch)
|
|
95
|
+
throw new Error("sourceBranch and targetBranch must differ");
|
|
96
|
+
const sourceSha = sha(input.sourceSha, "sourceSha");
|
|
97
|
+
const targetSha = sha(input.targetSha, "targetSha");
|
|
98
|
+
const selection = input.selection
|
|
99
|
+
? {
|
|
100
|
+
mode: required(input.selection.mode, "selection.mode"),
|
|
101
|
+
observedSourceHeadSha: sha(
|
|
102
|
+
input.selection.observedSourceHeadSha,
|
|
103
|
+
"selection.observedSourceHeadSha",
|
|
104
|
+
),
|
|
105
|
+
skippedNewerCommitCount: Number(
|
|
106
|
+
input.selection.skippedNewerCommitCount || 0,
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
: undefined;
|
|
110
|
+
if (
|
|
111
|
+
selection &&
|
|
112
|
+
(!Number.isSafeInteger(selection.skippedNewerCommitCount) ||
|
|
113
|
+
selection.skippedNewerCommitCount < 0)
|
|
114
|
+
) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"selection.skippedNewerCommitCount must be a non-negative integer",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const now = required(input.now || new Date().toISOString(), "now");
|
|
120
|
+
const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
|
|
121
|
+
if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
|
|
122
|
+
throw new Error("maxAgeSeconds must be a positive integer");
|
|
123
|
+
}
|
|
124
|
+
const comparison = input.comparison || {};
|
|
125
|
+
if (comparison.status !== "ahead" || Number(comparison.aheadBy) <= 0) {
|
|
126
|
+
const reason =
|
|
127
|
+
comparison.status === "identical"
|
|
128
|
+
? "target-already-current"
|
|
129
|
+
: "source-does-not-lead-target";
|
|
130
|
+
return {
|
|
131
|
+
schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
|
|
132
|
+
eligible: false,
|
|
133
|
+
reason,
|
|
134
|
+
repository,
|
|
135
|
+
source: { branch: sourceBranch, sha: sourceSha },
|
|
136
|
+
target: { branch: targetBranch, sha: targetSha },
|
|
137
|
+
comparison: {
|
|
138
|
+
status: String(comparison.status || "unknown"),
|
|
139
|
+
aheadBy: Number(comparison.aheadBy || 0),
|
|
140
|
+
},
|
|
141
|
+
decidedAt: now,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const rows = Array.isArray(input.workflowEvidence)
|
|
145
|
+
? input.workflowEvidence
|
|
146
|
+
: [];
|
|
147
|
+
const expectedPaths = [
|
|
148
|
+
...new Set((input.requiredWorkflowPaths || []).map(String)),
|
|
149
|
+
];
|
|
150
|
+
if (expectedPaths.length === 0)
|
|
151
|
+
throw new Error("requiredWorkflowPaths must not be empty");
|
|
152
|
+
if (rows.length !== expectedPaths.length) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const byPath = new Map();
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
const qualified = qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds });
|
|
160
|
+
if (byPath.has(qualified.workflowPath)) {
|
|
161
|
+
throw new Error(`duplicate workflow evidence: ${qualified.workflowPath}`);
|
|
162
|
+
}
|
|
163
|
+
byPath.set(qualified.workflowPath, qualified);
|
|
164
|
+
}
|
|
165
|
+
for (const workflowPath of expectedPaths) {
|
|
166
|
+
if (!byPath.has(workflowPath))
|
|
167
|
+
throw new Error(`missing workflow evidence: ${workflowPath}`);
|
|
168
|
+
}
|
|
169
|
+
const body = {
|
|
170
|
+
schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
|
|
171
|
+
eligible: true,
|
|
172
|
+
reason: "same-source-qualified",
|
|
173
|
+
repository,
|
|
174
|
+
source: { branch: sourceBranch, sha: sourceSha },
|
|
175
|
+
target: { branch: targetBranch, sha: targetSha },
|
|
176
|
+
comparison: { status: "ahead", aheadBy: Number(comparison.aheadBy) },
|
|
177
|
+
...(selection ? { selection } : {}),
|
|
178
|
+
sourceLockRef: channelCandidateSourceLockRef(targetBranch, sourceSha),
|
|
179
|
+
workflowEvidence: expectedPaths.map((workflowPath) =>
|
|
180
|
+
byPath.get(workflowPath),
|
|
181
|
+
),
|
|
182
|
+
policy: { maxAgeSeconds, requiredWorkflowPaths: expectedPaths },
|
|
183
|
+
decidedAt: now,
|
|
184
|
+
};
|
|
185
|
+
return { ...body, decisionRoot: root(body) };
|
|
186
|
+
}
|
package/packages/core/index.js
CHANGED
|
@@ -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({
|