@kungfu-tech/buildchain 2.12.1 → 2.12.2
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/README.md +10 -5
- package/dist/site/buildchain-contract.json +6 -6
- package/dist/site/buildchain-site.json +73 -21
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/kfd-claims.json +59 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/page-registry.json +62 -11
- package/dist/site/public-surface-audit.json +40 -7
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +35 -3
- package/docs/MAP.md +2 -1
- package/docs/lifecycle-protocol.md +16 -0
- package/docs/release-governance.md +7 -0
- package/docs/shifu-gate-profiles.md +163 -0
- package/docs/stable-candidate-patrol.md +23 -3
- package/package.json +1 -1
- package/packages/core/release-candidate.js +42 -0
- package/scripts/build-contract-core.mjs +21 -4
- package/scripts/check-inventory.mjs +4 -0
- package/scripts/gate-profile-core.mjs +413 -0
- package/scripts/generate-release-candidate-passport.mjs +4 -0
- package/scripts/shifu-gate-profile.mjs +351 -0
- package/scripts/stable-candidate-patrol.mjs +4 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
resolveRunnerMatrix,
|
|
8
|
+
writeGitHubOutputs,
|
|
9
|
+
} from "./build-contract-core.mjs";
|
|
10
|
+
import {
|
|
11
|
+
createGateAggregate,
|
|
12
|
+
createGateExecutionMatrix,
|
|
13
|
+
normalizeGatePlatform,
|
|
14
|
+
} from "./gate-profile-core.mjs";
|
|
15
|
+
|
|
16
|
+
function readArg(name, fallback = "") {
|
|
17
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
18
|
+
return index === -1 ? fallback : process.argv[index + 1] || "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseJson(value, label) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(value);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`${label} must be valid JSON: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJson(file, label = file) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`could not read ${label}: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJson(file, value) {
|
|
38
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
39
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commandForPlatform(commandJson, platform) {
|
|
43
|
+
const parsed = parseJson(commandJson, "gate-command-json");
|
|
44
|
+
const argv = Array.isArray(parsed) ? parsed : parsed?.[platform];
|
|
45
|
+
if (
|
|
46
|
+
!Array.isArray(argv) ||
|
|
47
|
+
argv.length === 0 ||
|
|
48
|
+
argv.some((item) => typeof item !== "string" || !item)
|
|
49
|
+
) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`gate-command-json requires a non-empty argv array for ${platform}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return argv;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function gateEnvironment() {
|
|
58
|
+
const parsed = parseJson(
|
|
59
|
+
process.env.BUILDCHAIN_GATE_ENVIRONMENT_JSON || "{}",
|
|
60
|
+
"gate-environment-json",
|
|
61
|
+
);
|
|
62
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error("gate-environment-json must be a JSON object");
|
|
64
|
+
}
|
|
65
|
+
const entries = Object.entries(parsed).map(([name, value]) => {
|
|
66
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
67
|
+
throw new Error(`invalid Gate environment name: ${name}`);
|
|
68
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Gate environment ${name} must be a string, number, or boolean`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return [name, String(value)];
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
...process.env,
|
|
77
|
+
...Object.fromEntries(entries),
|
|
78
|
+
...(process.env.BUILDCHAIN_SHIFU_CACHE_PROFILE_REF
|
|
79
|
+
? {
|
|
80
|
+
SHIFU_CACHE_PROFILE_REF:
|
|
81
|
+
process.env.BUILDCHAIN_SHIFU_CACHE_PROFILE_REF,
|
|
82
|
+
}
|
|
83
|
+
: {}),
|
|
84
|
+
...(process.env.BUILDCHAIN_SHIFU_CACHE_PROFILE_DIGEST
|
|
85
|
+
? {
|
|
86
|
+
SHIFU_CACHE_PROFILE_DIGEST:
|
|
87
|
+
process.env.BUILDCHAIN_SHIFU_CACHE_PROFILE_DIGEST,
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cmdQuote(value) {
|
|
94
|
+
if (/^[A-Za-z0-9_./:\\-]+$/u.test(value)) return value;
|
|
95
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function runArgv(
|
|
99
|
+
argv,
|
|
100
|
+
args,
|
|
101
|
+
{ cwd, env = process.env, allowFailure = false } = {},
|
|
102
|
+
) {
|
|
103
|
+
const command = argv[0];
|
|
104
|
+
const commandArgs = [...argv.slice(1), ...args];
|
|
105
|
+
const windowsBatch =
|
|
106
|
+
process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
107
|
+
const result = windowsBatch
|
|
108
|
+
? spawnSync(
|
|
109
|
+
process.env.ComSpec || "cmd.exe",
|
|
110
|
+
["/d", "/s", "/c", [command, ...commandArgs].map(cmdQuote).join(" ")],
|
|
111
|
+
{
|
|
112
|
+
cwd,
|
|
113
|
+
env,
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
: spawnSync(command, commandArgs, {
|
|
119
|
+
cwd,
|
|
120
|
+
env,
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
123
|
+
});
|
|
124
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
125
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
126
|
+
if (result.error) throw result.error;
|
|
127
|
+
const status = result.status ?? 1;
|
|
128
|
+
if (!allowFailure && status !== 0)
|
|
129
|
+
throw new Error(`${command} exited with status ${status}`);
|
|
130
|
+
return { ...result, status };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function gateArgs(base, registry) {
|
|
134
|
+
return registry ? [...base, "--registry", registry] : base;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function planMode() {
|
|
138
|
+
const profile = process.env.BUILDCHAIN_GATE_PROFILE || "";
|
|
139
|
+
const includeAdvisory =
|
|
140
|
+
process.env.BUILDCHAIN_GATE_INCLUDE_ADVISORY === "true";
|
|
141
|
+
const commandJson =
|
|
142
|
+
process.env.BUILDCHAIN_GATE_PLAN_COMMAND_JSON ||
|
|
143
|
+
process.env.BUILDCHAIN_GATE_COMMAND_JSON ||
|
|
144
|
+
'["./shifu"]';
|
|
145
|
+
const registry = process.env.BUILDCHAIN_GATE_REGISTRY || "";
|
|
146
|
+
const cwd = path.resolve(
|
|
147
|
+
process.env.BUILDCHAIN_GATE_SOURCE_CWD || process.cwd(),
|
|
148
|
+
);
|
|
149
|
+
const outputRoot = path.resolve(
|
|
150
|
+
process.env.BUILDCHAIN_GATE_OUTPUT_ROOT || ".buildchain/gates/plan",
|
|
151
|
+
);
|
|
152
|
+
const env = gateEnvironment();
|
|
153
|
+
const resolvedRunners = resolveRunnerMatrix({
|
|
154
|
+
runnerPreset: process.env.BUILDCHAIN_RUNNER_PRESET || "github-hosted",
|
|
155
|
+
platformsJson: process.env.BUILDCHAIN_PLATFORMS_JSON || "",
|
|
156
|
+
});
|
|
157
|
+
const platforms = resolvedRunners.platforms.map(normalizeGatePlatform);
|
|
158
|
+
const plans = {};
|
|
159
|
+
for (const platform of platforms) {
|
|
160
|
+
const argv = commandForPlatform(
|
|
161
|
+
commandJson,
|
|
162
|
+
process.platform === "win32" ? "windows" : "linux",
|
|
163
|
+
);
|
|
164
|
+
const args = gateArgs(
|
|
165
|
+
[
|
|
166
|
+
"gate",
|
|
167
|
+
"plan",
|
|
168
|
+
profile,
|
|
169
|
+
"--platform",
|
|
170
|
+
platform.platform,
|
|
171
|
+
...(includeAdvisory ? ["--include-advisory"] : []),
|
|
172
|
+
"--json",
|
|
173
|
+
],
|
|
174
|
+
registry,
|
|
175
|
+
);
|
|
176
|
+
const result = runArgv(argv, args, { cwd, env });
|
|
177
|
+
const plan = parseJson(result.stdout, `Shifu gate plan for ${platform.id}`);
|
|
178
|
+
plans[platform.id] = plan;
|
|
179
|
+
writeJson(path.join(outputRoot, "plans", `${platform.id}.json`), plan);
|
|
180
|
+
}
|
|
181
|
+
const matrix = createGateExecutionMatrix({
|
|
182
|
+
profile,
|
|
183
|
+
includeAdvisory,
|
|
184
|
+
platforms,
|
|
185
|
+
plans,
|
|
186
|
+
});
|
|
187
|
+
const matrixPath = path.join(outputRoot, "matrix.json");
|
|
188
|
+
writeJson(matrixPath, matrix);
|
|
189
|
+
writeGitHubOutputs({
|
|
190
|
+
"gate-matrix-json": JSON.stringify(matrix.entries),
|
|
191
|
+
"gate-matrix-count": String(matrix.entries.length),
|
|
192
|
+
"gate-matrix-digest": matrix.digest,
|
|
193
|
+
"gate-matrix-path": matrixPath,
|
|
194
|
+
"gate-project-id": matrix.registry.projectId,
|
|
195
|
+
"gate-registry-digest": matrix.registry.digest,
|
|
196
|
+
});
|
|
197
|
+
return matrix;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function runMode() {
|
|
201
|
+
const entry = parseJson(
|
|
202
|
+
process.env.BUILDCHAIN_GATE_MATRIX_ENTRY_JSON || "",
|
|
203
|
+
"gate matrix entry",
|
|
204
|
+
);
|
|
205
|
+
const commandJson = process.env.BUILDCHAIN_GATE_COMMAND_JSON || '["./shifu"]';
|
|
206
|
+
const registry = process.env.BUILDCHAIN_GATE_REGISTRY || "";
|
|
207
|
+
const cwd = path.resolve(
|
|
208
|
+
process.env.BUILDCHAIN_GATE_SOURCE_CWD || process.cwd(),
|
|
209
|
+
);
|
|
210
|
+
const outputRoot = path.resolve(
|
|
211
|
+
process.env.BUILDCHAIN_GATE_OUTPUT_ROOT ||
|
|
212
|
+
`.buildchain/gates/executions/${entry.id}`,
|
|
213
|
+
);
|
|
214
|
+
const receiptPath = path.join(outputRoot, "receipt.json");
|
|
215
|
+
const validationPath = path.join(outputRoot, "validation.json");
|
|
216
|
+
const executionPath = path.join(outputRoot, "execution.json");
|
|
217
|
+
const env = gateEnvironment();
|
|
218
|
+
fs.mkdirSync(outputRoot, { recursive: true });
|
|
219
|
+
const argv = commandForPlatform(commandJson, entry.platform);
|
|
220
|
+
const runArgs = gateArgs(
|
|
221
|
+
[
|
|
222
|
+
"gate",
|
|
223
|
+
"run",
|
|
224
|
+
"--profile",
|
|
225
|
+
entry.profile,
|
|
226
|
+
...(entry.includeAdvisory ? ["--include-advisory"] : []),
|
|
227
|
+
...entry.capabilities.flatMap((capability) => [
|
|
228
|
+
"--capability",
|
|
229
|
+
capability,
|
|
230
|
+
]),
|
|
231
|
+
"--receipt",
|
|
232
|
+
receiptPath,
|
|
233
|
+
"--json",
|
|
234
|
+
],
|
|
235
|
+
registry,
|
|
236
|
+
);
|
|
237
|
+
const runResult = runArgv(argv, runArgs, { cwd, env, allowFailure: true });
|
|
238
|
+
let receipt = fs.existsSync(receiptPath)
|
|
239
|
+
? readJson(receiptPath, "Shifu gate receipt")
|
|
240
|
+
: null;
|
|
241
|
+
let validation = null;
|
|
242
|
+
let validationStatus = 1;
|
|
243
|
+
if (receipt) {
|
|
244
|
+
const validationResult = runArgv(
|
|
245
|
+
argv,
|
|
246
|
+
gateArgs(
|
|
247
|
+
["gate", "receipt", "validate", receiptPath, "--json"],
|
|
248
|
+
registry,
|
|
249
|
+
),
|
|
250
|
+
{ cwd, env, allowFailure: true },
|
|
251
|
+
);
|
|
252
|
+
validationStatus = validationResult.status;
|
|
253
|
+
if (validationResult.stdout.trim()) {
|
|
254
|
+
validation = parseJson(
|
|
255
|
+
validationResult.stdout,
|
|
256
|
+
"Shifu gate receipt validation",
|
|
257
|
+
);
|
|
258
|
+
writeJson(validationPath, validation);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const execution = {
|
|
262
|
+
platformId: entry.id,
|
|
263
|
+
runStatus: runResult.status,
|
|
264
|
+
validationStatus,
|
|
265
|
+
receipt,
|
|
266
|
+
validation,
|
|
267
|
+
};
|
|
268
|
+
writeJson(executionPath, execution);
|
|
269
|
+
writeGitHubOutputs({
|
|
270
|
+
"gate-platform-id": entry.id,
|
|
271
|
+
"gate-receipt-path": receipt ? receiptPath : "",
|
|
272
|
+
"gate-validation-path": validation ? validationPath : "",
|
|
273
|
+
"gate-execution-path": executionPath,
|
|
274
|
+
"gate-qualifying": String(
|
|
275
|
+
receipt?.qualifying === true && validation?.qualifying === true,
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
if (
|
|
279
|
+
runResult.status !== 0 ||
|
|
280
|
+
validationStatus !== 0 ||
|
|
281
|
+
validation?.qualifying !== true
|
|
282
|
+
) {
|
|
283
|
+
process.exitCode = 1;
|
|
284
|
+
}
|
|
285
|
+
return execution;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function findNamedFiles(root, basename) {
|
|
289
|
+
if (!fs.existsSync(root)) return [];
|
|
290
|
+
const stat = fs.statSync(root);
|
|
291
|
+
if (stat.isFile()) return path.basename(root) === basename ? [root] : [];
|
|
292
|
+
return fs
|
|
293
|
+
.readdirSync(root, { withFileTypes: true })
|
|
294
|
+
.flatMap((entry) => findNamedFiles(path.join(root, entry.name), basename));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function aggregateMode() {
|
|
298
|
+
const matrixPath = path.resolve(
|
|
299
|
+
process.env.BUILDCHAIN_GATE_MATRIX_PATH || "",
|
|
300
|
+
);
|
|
301
|
+
const inputRoot = path.resolve(
|
|
302
|
+
process.env.BUILDCHAIN_GATE_EXECUTION_INPUT || "",
|
|
303
|
+
);
|
|
304
|
+
const outputPath = path.resolve(
|
|
305
|
+
process.env.BUILDCHAIN_GATE_AGGREGATE_PATH ||
|
|
306
|
+
".buildchain/gates/gate-aggregate.json",
|
|
307
|
+
);
|
|
308
|
+
const sourceSha = process.env.BUILDCHAIN_GATE_SOURCE_SHA || "";
|
|
309
|
+
const matrix = readJson(matrixPath, "gate matrix");
|
|
310
|
+
const executions = new Map();
|
|
311
|
+
for (const file of findNamedFiles(inputRoot, "execution.json")) {
|
|
312
|
+
const execution = readJson(file, "gate execution");
|
|
313
|
+
if (!execution.platformId) throw new Error(`${file} is missing platformId`);
|
|
314
|
+
if (executions.has(execution.platformId))
|
|
315
|
+
throw new Error(`duplicate gate execution for ${execution.platformId}`);
|
|
316
|
+
executions.set(execution.platformId, execution);
|
|
317
|
+
}
|
|
318
|
+
const aggregate = createGateAggregate({ matrix, sourceSha, executions });
|
|
319
|
+
writeJson(outputPath, aggregate);
|
|
320
|
+
writeGitHubOutputs({
|
|
321
|
+
"gate-aggregate-path": outputPath,
|
|
322
|
+
"gate-aggregate-json": JSON.stringify(aggregate),
|
|
323
|
+
"gate-aggregate-digest": aggregate.digest,
|
|
324
|
+
"gate-aggregate-status": aggregate.status,
|
|
325
|
+
"gate-aggregate-qualifying": String(aggregate.qualifying),
|
|
326
|
+
});
|
|
327
|
+
if (!aggregate.qualifying) process.exitCode = 1;
|
|
328
|
+
return aggregate;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function shifuGateProfileCli() {
|
|
332
|
+
const mode = readArg("mode", process.env.BUILDCHAIN_GATE_MODE || "plan");
|
|
333
|
+
if (mode === "plan") return planMode();
|
|
334
|
+
if (mode === "run") return runMode();
|
|
335
|
+
if (mode === "aggregate") return aggregateMode();
|
|
336
|
+
throw new Error(`unsupported Shifu gate profile mode: ${mode}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
process.argv[1] &&
|
|
341
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
342
|
+
) {
|
|
343
|
+
try {
|
|
344
|
+
shifuGateProfileCli();
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(
|
|
347
|
+
`::error::${String(error.message || error).replace(/\r?\n/g, "%0A")}`,
|
|
348
|
+
);
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -302,6 +302,10 @@ export function createGitHubStableCandidateClient({ repository: repositoryInput,
|
|
|
302
302
|
const payload = raw ? JSON.parse(raw) : undefined;
|
|
303
303
|
if (allow404 && response.status === 404) return undefined;
|
|
304
304
|
if (!response.ok) throw new Error(`GitHub API ${method} ${requestPath} failed with ${response.status}: ${payload?.message || raw}`);
|
|
305
|
+
if (requestPath === "/graphql" && Array.isArray(payload?.errors) && payload.errors.length > 0) {
|
|
306
|
+
const messages = payload.errors.map((error) => text(error?.message)).filter(Boolean);
|
|
307
|
+
throw new Error(`GitHub GraphQL ${method} ${requestPath} failed: ${messages.join("; ") || "unknown GraphQL error"}`);
|
|
308
|
+
}
|
|
305
309
|
return payload;
|
|
306
310
|
}
|
|
307
311
|
return {
|