@kungfu-tech/buildchain 3.0.7-alpha.0 → 3.0.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.
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/dist/site/buildchain-contract.json +24 -24
- package/dist/site/buildchain-site.json +91 -30
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +6 -2
- package/dist/site/kfd-claims.json +122 -11
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +8 -7
- package/dist/site/node-api-registry.json +683 -105
- package/dist/site/page-registry.json +80 -19
- package/dist/site/public-surface-audit.json +98 -7
- package/dist/site/publication-authority-registry.json +81 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +119 -2
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +2 -2
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +277 -212
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-provider-plane.md +1 -1
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +4 -1
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-publication-authority.js +4 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +3 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/publication-rehearsal-runtime.js +13 -1
- package/packages/core/release-passport.js +130 -20
- package/scripts/assemble-self-publication-admission.mjs +1 -1
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/check-inventory.mjs +3 -1
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-site-bundle.mjs +23 -4
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +6 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
GitHubHousekeeperClient,
|
|
6
|
+
applyGitHubHousekeeperPlan,
|
|
7
|
+
collectGitHubHousekeeperInventory,
|
|
8
|
+
} from "../packages/core/engineering-housekeeper-github.js";
|
|
9
|
+
|
|
10
|
+
const VALID_MODES = new Set(["report", "apply"]);
|
|
11
|
+
const VALID_SCOPES = new Set(["branches", "pull-requests"]);
|
|
12
|
+
const DEFAULT_OUTPUT_DIRECTORY = ".buildchain/engineering-housekeeper";
|
|
13
|
+
|
|
14
|
+
function requiredString(value, field) {
|
|
15
|
+
const normalized = String(value || "").trim();
|
|
16
|
+
if (!normalized) throw new Error(`${field} is required`);
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function boolOption(value, fallback = false) {
|
|
21
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
22
|
+
if (typeof value === "boolean") return value;
|
|
23
|
+
const normalized = String(value).trim().toLowerCase();
|
|
24
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
25
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
26
|
+
throw new Error(`boolean input must be true or false, got: ${value}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function positiveInteger(value, fallback, field) {
|
|
30
|
+
const selected =
|
|
31
|
+
value === undefined || value === "" ? fallback : Number(value);
|
|
32
|
+
if (!Number.isInteger(selected) || selected < 1) {
|
|
33
|
+
throw new Error(`${field} must be a positive integer`);
|
|
34
|
+
}
|
|
35
|
+
return selected;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function splitPatterns(value, fallback) {
|
|
39
|
+
const normalized = String(value || "").trim();
|
|
40
|
+
if (!normalized) return [...fallback];
|
|
41
|
+
return [
|
|
42
|
+
...new Set(
|
|
43
|
+
normalized
|
|
44
|
+
.split(/[\n,]+/)
|
|
45
|
+
.map((entry) => entry.trim())
|
|
46
|
+
.filter(Boolean),
|
|
47
|
+
),
|
|
48
|
+
].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeRepository(value) {
|
|
52
|
+
const repository = requiredString(value, "repository");
|
|
53
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
|
|
54
|
+
throw new Error(`repository must be owner/repo, got: ${repository}`);
|
|
55
|
+
}
|
|
56
|
+
return repository;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeMode(value) {
|
|
60
|
+
const mode = String(value || "report")
|
|
61
|
+
.trim()
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
if (!VALID_MODES.has(mode)) {
|
|
64
|
+
throw new Error(`mode must be report or apply, got: ${value || "<empty>"}`);
|
|
65
|
+
}
|
|
66
|
+
return mode;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeHousekeeperWorkflowOptions(options = {}) {
|
|
70
|
+
const mode = normalizeMode(options.mode ?? process.env.HOUSEKEEPER_MODE);
|
|
71
|
+
const applyEnabled = boolOption(
|
|
72
|
+
options.applyEnabled ?? process.env.HOUSEKEEPER_APPLY_ENABLED,
|
|
73
|
+
false,
|
|
74
|
+
);
|
|
75
|
+
if (mode === "apply" && !applyEnabled) {
|
|
76
|
+
throw new Error("apply mode requires apply-enabled=true");
|
|
77
|
+
}
|
|
78
|
+
if (mode === "report" && applyEnabled) {
|
|
79
|
+
throw new Error("apply-enabled=true is only valid when mode=apply");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
mode,
|
|
83
|
+
applyEnabled,
|
|
84
|
+
repository: normalizeRepository(
|
|
85
|
+
options.repository ||
|
|
86
|
+
process.env.HOUSEKEEPER_REPOSITORY ||
|
|
87
|
+
process.env.GITHUB_REPOSITORY,
|
|
88
|
+
),
|
|
89
|
+
targetBranch: requiredString(
|
|
90
|
+
options.targetBranch || process.env.HOUSEKEEPER_TARGET_BRANCH,
|
|
91
|
+
"target-branch",
|
|
92
|
+
).replace(/^refs\/heads\//, ""),
|
|
93
|
+
staleDays: positiveInteger(
|
|
94
|
+
options.staleDays ?? process.env.HOUSEKEEPER_STALE_DAYS,
|
|
95
|
+
30,
|
|
96
|
+
"stale-days",
|
|
97
|
+
),
|
|
98
|
+
maxActions: positiveInteger(
|
|
99
|
+
options.maxActions ?? process.env.HOUSEKEEPER_MAX_ACTIONS,
|
|
100
|
+
20,
|
|
101
|
+
"max-actions",
|
|
102
|
+
),
|
|
103
|
+
protectedPatterns: splitPatterns(
|
|
104
|
+
options.protectedPatterns ?? process.env.HOUSEKEEPER_PROTECTED_PATTERNS,
|
|
105
|
+
["dev/**", "alpha/**", "release/**", "publish-gate/**"],
|
|
106
|
+
),
|
|
107
|
+
retainedPatterns: splitPatterns(
|
|
108
|
+
options.retainedPatterns ?? process.env.HOUSEKEEPER_RETAINED_PATTERNS,
|
|
109
|
+
["train/**", "authority/**"],
|
|
110
|
+
),
|
|
111
|
+
stalePullRequestLabel: String(
|
|
112
|
+
options.stalePullRequestLabel ??
|
|
113
|
+
process.env.HOUSEKEEPER_STALE_PR_LABEL ??
|
|
114
|
+
"",
|
|
115
|
+
).trim(),
|
|
116
|
+
observedAt: String(
|
|
117
|
+
options.observedAt ||
|
|
118
|
+
process.env.HOUSEKEEPER_OBSERVED_AT ||
|
|
119
|
+
new Date().toISOString(),
|
|
120
|
+
),
|
|
121
|
+
appliedAt: String(
|
|
122
|
+
options.appliedAt ||
|
|
123
|
+
process.env.HOUSEKEEPER_APPLIED_AT ||
|
|
124
|
+
new Date().toISOString(),
|
|
125
|
+
),
|
|
126
|
+
outputDirectory: path.resolve(
|
|
127
|
+
String(
|
|
128
|
+
options.outputDirectory ||
|
|
129
|
+
process.env.HOUSEKEEPER_OUTPUT_DIRECTORY ||
|
|
130
|
+
DEFAULT_OUTPUT_DIRECTORY,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function workflowPolicy(options) {
|
|
137
|
+
return {
|
|
138
|
+
protectedPatterns: options.protectedPatterns,
|
|
139
|
+
retainedPatterns: options.retainedPatterns,
|
|
140
|
+
pullRequests: {
|
|
141
|
+
reportStale: true,
|
|
142
|
+
label: options.stalePullRequestLabel,
|
|
143
|
+
autoClose: false,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function actionIdentity(action) {
|
|
149
|
+
return action.name
|
|
150
|
+
? `${action.kind}:${action.name}`
|
|
151
|
+
: `${action.kind}:#${action.number}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function decisionFor(entry) {
|
|
155
|
+
if (entry.kind === "branch") return entry.decision;
|
|
156
|
+
return entry.actions.length > 0 ? entry.actions.join(",") : "report-only";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderHousekeeperWorkflowReport(
|
|
160
|
+
plan,
|
|
161
|
+
receipt,
|
|
162
|
+
{ mode, scope = "all" } = {},
|
|
163
|
+
) {
|
|
164
|
+
const lines = [
|
|
165
|
+
"## Engineering Housekeeper",
|
|
166
|
+
"",
|
|
167
|
+
`Mode: \`${mode || "report"}\``,
|
|
168
|
+
`Scope: \`${scope}\``,
|
|
169
|
+
`Repository: \`${plan.repository}\``,
|
|
170
|
+
`Target ref: \`${plan.target.name}@${plan.target.headOid}\``,
|
|
171
|
+
`Observed at: \`${plan.observedAt}\``,
|
|
172
|
+
`Plan root: \`${plan.planRoot}\``,
|
|
173
|
+
`Receipt root: \`${receipt.receiptRoot}\``,
|
|
174
|
+
"",
|
|
175
|
+
"### Decisions",
|
|
176
|
+
"",
|
|
177
|
+
"| Subject | Observed ref | Decision | Reason codes |",
|
|
178
|
+
"| --- | --- | --- | --- |",
|
|
179
|
+
];
|
|
180
|
+
for (const entry of plan.inventory) {
|
|
181
|
+
const subject =
|
|
182
|
+
entry.kind === "branch" ? entry.name : `PR #${entry.number}`;
|
|
183
|
+
lines.push(
|
|
184
|
+
`| \`${subject}\` | \`${entry.headOid}\` | ${decisionFor(entry)} | \`${entry.reasonCodes.join(",")}\` |`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (plan.inventory.length === 0)
|
|
188
|
+
lines.push("| - | - | retain | `inventory.empty` |");
|
|
189
|
+
lines.push(
|
|
190
|
+
"",
|
|
191
|
+
"### Outcomes",
|
|
192
|
+
"",
|
|
193
|
+
"| Action | Outcome | Details |",
|
|
194
|
+
"| --- | --- | --- |",
|
|
195
|
+
);
|
|
196
|
+
for (const outcome of receipt.outcomes) {
|
|
197
|
+
const details =
|
|
198
|
+
outcome.reasonCodes?.join(",") || outcome.providerError?.operation || "-";
|
|
199
|
+
lines.push(
|
|
200
|
+
`| \`${outcome.action}\` | ${outcome.status} | \`${details}\` |`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (receipt.outcomes.length === 0)
|
|
204
|
+
lines.push("| - | no-op | `no-actions-in-scope` |");
|
|
205
|
+
return `${lines.join("\n")}\n`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function writeJson(filePath, value) {
|
|
209
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
210
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function writeText(filePath, value) {
|
|
214
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
215
|
+
fs.writeFileSync(filePath, value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeOutputs(outputs, outputFile = process.env.GITHUB_OUTPUT) {
|
|
219
|
+
if (!outputFile) return;
|
|
220
|
+
const lines = Object.entries(outputs).map(
|
|
221
|
+
([key, value]) => `${key}=${String(value).replace(/\n/g, "%0A")}`,
|
|
222
|
+
);
|
|
223
|
+
fs.appendFileSync(outputFile, `${lines.join("\n")}\n`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function appendSummary(
|
|
227
|
+
markdown,
|
|
228
|
+
summaryFile = process.env.GITHUB_STEP_SUMMARY,
|
|
229
|
+
) {
|
|
230
|
+
if (summaryFile) fs.appendFileSync(summaryFile, markdown);
|
|
231
|
+
else process.stdout.write(markdown);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function createHousekeeperWorkflowPlan(
|
|
235
|
+
optionsInput = {},
|
|
236
|
+
clientInput,
|
|
237
|
+
) {
|
|
238
|
+
const options = normalizeHousekeeperWorkflowOptions(optionsInput);
|
|
239
|
+
const client =
|
|
240
|
+
clientInput ||
|
|
241
|
+
new GitHubHousekeeperClient({
|
|
242
|
+
token: requiredString(process.env.GITHUB_TOKEN, "GitHub token"),
|
|
243
|
+
});
|
|
244
|
+
const plan = await collectGitHubHousekeeperInventory({
|
|
245
|
+
client,
|
|
246
|
+
repository: options.repository,
|
|
247
|
+
targetBranch: options.targetBranch,
|
|
248
|
+
observedAt: options.observedAt,
|
|
249
|
+
staleDays: options.staleDays,
|
|
250
|
+
policy: workflowPolicy(options),
|
|
251
|
+
});
|
|
252
|
+
const receipt = await applyGitHubHousekeeperPlan({
|
|
253
|
+
client,
|
|
254
|
+
plan,
|
|
255
|
+
dryRun: true,
|
|
256
|
+
appliedAt: options.appliedAt,
|
|
257
|
+
staleDays: options.staleDays,
|
|
258
|
+
maxActions: options.maxActions,
|
|
259
|
+
});
|
|
260
|
+
return { options, plan, receipt };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function selectHousekeeperActions(plan, scope, maxActions) {
|
|
264
|
+
if (!VALID_SCOPES.has(scope))
|
|
265
|
+
throw new Error(`scope must be branches or pull-requests, got: ${scope}`);
|
|
266
|
+
return plan.actions
|
|
267
|
+
.slice(0, positiveInteger(maxActions, 20, "max-actions"))
|
|
268
|
+
.filter((action) =>
|
|
269
|
+
scope === "branches"
|
|
270
|
+
? action.kind === "delete-branch"
|
|
271
|
+
: action.kind.endsWith("-pull-request"),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function applyHousekeeperWorkflowScope({
|
|
276
|
+
options: optionsInput = {},
|
|
277
|
+
plan,
|
|
278
|
+
scope,
|
|
279
|
+
client: clientInput,
|
|
280
|
+
}) {
|
|
281
|
+
const options = normalizeHousekeeperWorkflowOptions(optionsInput);
|
|
282
|
+
if (options.mode !== "apply" || !options.applyEnabled) {
|
|
283
|
+
throw new Error("scope apply requires mode=apply and apply-enabled=true");
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
plan.repository !== options.repository ||
|
|
287
|
+
plan.target.name !== options.targetBranch
|
|
288
|
+
) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
"plan repository or target branch does not match current workflow inputs",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const client =
|
|
294
|
+
clientInput ||
|
|
295
|
+
new GitHubHousekeeperClient({
|
|
296
|
+
token: requiredString(process.env.GITHUB_TOKEN, "GitHub token"),
|
|
297
|
+
});
|
|
298
|
+
const scopedPlan = {
|
|
299
|
+
...plan,
|
|
300
|
+
actions: selectHousekeeperActions(plan, scope, options.maxActions),
|
|
301
|
+
};
|
|
302
|
+
const receipt = await applyGitHubHousekeeperPlan({
|
|
303
|
+
client,
|
|
304
|
+
plan: scopedPlan,
|
|
305
|
+
dryRun: false,
|
|
306
|
+
appliedAt: options.appliedAt,
|
|
307
|
+
staleDays: options.staleDays,
|
|
308
|
+
maxActions: Math.max(1, scopedPlan.actions.length),
|
|
309
|
+
});
|
|
310
|
+
return { options, plan, scopedPlan, receipt };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function planCommand() {
|
|
314
|
+
const result = await createHousekeeperWorkflowPlan();
|
|
315
|
+
const planPath = path.join(result.options.outputDirectory, "plan.json");
|
|
316
|
+
const reportPath = path.join(result.options.outputDirectory, "report.md");
|
|
317
|
+
const receiptPath = path.join(
|
|
318
|
+
result.options.outputDirectory,
|
|
319
|
+
"report-receipt.json",
|
|
320
|
+
);
|
|
321
|
+
const report = renderHousekeeperWorkflowReport(result.plan, result.receipt, {
|
|
322
|
+
mode: result.options.mode,
|
|
323
|
+
});
|
|
324
|
+
writeJson(planPath, result.plan);
|
|
325
|
+
writeJson(receiptPath, result.receipt);
|
|
326
|
+
writeText(reportPath, report);
|
|
327
|
+
appendSummary(report);
|
|
328
|
+
writeOutputs({
|
|
329
|
+
"plan-path": planPath,
|
|
330
|
+
"report-path": reportPath,
|
|
331
|
+
"report-receipt-path": receiptPath,
|
|
332
|
+
"plan-root": result.plan.planRoot,
|
|
333
|
+
"report-receipt-root": result.receipt.receiptRoot,
|
|
334
|
+
"action-count": result.plan.actions.length,
|
|
335
|
+
"branch-action-count": result.plan.actions.filter(
|
|
336
|
+
(action) => action.kind === "delete-branch",
|
|
337
|
+
).length,
|
|
338
|
+
"pull-request-action-count": result.plan.actions.filter((action) =>
|
|
339
|
+
action.kind.endsWith("-pull-request"),
|
|
340
|
+
).length,
|
|
341
|
+
outcome:
|
|
342
|
+
result.plan.actions.length === 0
|
|
343
|
+
? "no-actions"
|
|
344
|
+
: `${result.options.mode}-ready`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function applyCommand(scope) {
|
|
349
|
+
const planPath = path.resolve(
|
|
350
|
+
requiredString(process.env.HOUSEKEEPER_PLAN_PATH, "HOUSEKEEPER_PLAN_PATH"),
|
|
351
|
+
);
|
|
352
|
+
const plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
353
|
+
const result = await applyHousekeeperWorkflowScope({ plan, scope });
|
|
354
|
+
const receiptPath = path.join(
|
|
355
|
+
result.options.outputDirectory,
|
|
356
|
+
`${scope}-receipt.json`,
|
|
357
|
+
);
|
|
358
|
+
const reportPath = path.join(
|
|
359
|
+
result.options.outputDirectory,
|
|
360
|
+
`${scope}-report.md`,
|
|
361
|
+
);
|
|
362
|
+
const report = renderHousekeeperWorkflowReport(result.plan, result.receipt, {
|
|
363
|
+
mode: result.options.mode,
|
|
364
|
+
scope,
|
|
365
|
+
});
|
|
366
|
+
writeJson(receiptPath, result.receipt);
|
|
367
|
+
writeText(reportPath, report);
|
|
368
|
+
appendSummary(report);
|
|
369
|
+
writeOutputs({
|
|
370
|
+
"receipt-path": receiptPath,
|
|
371
|
+
"report-path": reportPath,
|
|
372
|
+
"receipt-root": result.receipt.receiptRoot,
|
|
373
|
+
"outcome-count": result.receipt.outcomes.length,
|
|
374
|
+
"selected-action-count": result.scopedPlan.actions.length,
|
|
375
|
+
outcome: result.scopedPlan.actions.length === 0 ? "no-actions" : "applied",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function main() {
|
|
380
|
+
const command = process.argv[2];
|
|
381
|
+
if (command === "plan") return planCommand();
|
|
382
|
+
if (command === "apply")
|
|
383
|
+
return applyCommand(requiredString(process.argv[3], "scope"));
|
|
384
|
+
throw new Error(
|
|
385
|
+
"usage: engineering-housekeeper-workflow.mjs plan | apply <branches|pull-requests>",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
390
|
+
main().catch((error) => {
|
|
391
|
+
console.error(error.stack || error.message);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
@@ -332,11 +332,16 @@ function capabilityGroup(id) {
|
|
|
332
332
|
return id;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
function publicSurfaceLifecycle({
|
|
335
|
+
function publicSurfaceLifecycle({
|
|
336
|
+
owner,
|
|
337
|
+
maturity,
|
|
338
|
+
nonDuplicationRationale,
|
|
339
|
+
introducedVersion = "pre-3.0.2-alpha.4",
|
|
340
|
+
}) {
|
|
336
341
|
return {
|
|
337
342
|
owner,
|
|
338
343
|
maturity,
|
|
339
|
-
introducedVersion
|
|
344
|
+
introducedVersion,
|
|
340
345
|
compatibilityPromise: "preserved-through-the-v3-major-line",
|
|
341
346
|
deprecationReplacement: "",
|
|
342
347
|
sunsetCondition: "explicit-breaking-change-review-in-a-future-major-line",
|
|
@@ -369,6 +374,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
369
374
|
"dev-qualification-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 136 },
|
|
370
375
|
"dev-alpha-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 137 },
|
|
371
376
|
"observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
|
|
377
|
+
"engineering-housekeeper": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer", "agent"], maturity: "preview", order: 145 },
|
|
372
378
|
"reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
|
|
373
379
|
"lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
|
|
374
380
|
"runtime-train-validation": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer"], maturity: "stable", order: 220 },
|
|
@@ -521,7 +527,7 @@ function workflowCapabilityGroup(entry) {
|
|
|
521
527
|
if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
|
|
522
528
|
if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
|
|
523
529
|
if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
|
|
524
|
-
if (entry.id.includes("patrol") || entry.id.includes("dev-pr-auto-merge") || entry.id.includes("dev-delivery-warrant") || entry.id.includes("buildchain-dev-delivery")) return capabilityGroup("governance-versioning");
|
|
530
|
+
if (entry.id.includes("patrol") || entry.id.includes("housekeeper") || entry.id.includes("dev-pr-auto-merge") || entry.id.includes("dev-delivery-warrant") || entry.id.includes("buildchain-dev-delivery")) return capabilityGroup("governance-versioning");
|
|
525
531
|
if (entry.status === "repository-internal" || entry.status === "compatibility-fixture") return capabilityGroup("api-cli-reference");
|
|
526
532
|
return capabilityGroup("api-cli-reference");
|
|
527
533
|
}
|
|
@@ -732,6 +738,7 @@ function buildSiteBundle() {
|
|
|
732
738
|
"docs/dev-qualification-patrol.md",
|
|
733
739
|
"docs/dev-alpha-candidate-patrol.md",
|
|
734
740
|
"docs/observed-evidence-patrol.md",
|
|
741
|
+
"docs/engineering-housekeeper.md",
|
|
735
742
|
"docs/release-governance.md",
|
|
736
743
|
"docs/release-passport.md",
|
|
737
744
|
"docs/controller-evidence.md",
|
|
@@ -791,6 +798,10 @@ function buildSiteBundle() {
|
|
|
791
798
|
["buildchain-patrol-daily", "repository-patrol"],
|
|
792
799
|
["buildchain-patrol-weekly", "repository-patrol"],
|
|
793
800
|
["buildchain-patrol-monthly", "repository-patrol"],
|
|
801
|
+
["engineering-housekeeper", "repository-patrol"],
|
|
802
|
+
["engineering-housekeeper-daily", "repository-patrol"],
|
|
803
|
+
["engineering-housekeeper-weekly", "repository-patrol"],
|
|
804
|
+
["engineering-housekeeper-monthly", "repository-patrol"],
|
|
794
805
|
["stable-candidate-patrol", "repository-patrol"],
|
|
795
806
|
["dev-qualification-patrol", "repository-patrol"],
|
|
796
807
|
["dev-alpha-candidate-patrol", "repository-patrol"],
|
|
@@ -808,8 +819,13 @@ function buildSiteBundle() {
|
|
|
808
819
|
["candidate-lab", "repository-internal"],
|
|
809
820
|
["build-surface-fixture", "repository-internal"],
|
|
810
821
|
["buildchain-stable-candidate-qualification", "repository-internal"],
|
|
822
|
+
["engineering-housekeeper", "preview"],
|
|
823
|
+
["engineering-housekeeper-daily", "preview"],
|
|
824
|
+
["engineering-housekeeper-weekly", "preview"],
|
|
825
|
+
["engineering-housekeeper-monthly", "preview"],
|
|
811
826
|
["self-hosted-runner-smoke", "compatibility-fixture"],
|
|
812
827
|
]);
|
|
828
|
+
const engineeringHousekeeper = entry.id.startsWith("engineering-housekeeper");
|
|
813
829
|
return {
|
|
814
830
|
...entry,
|
|
815
831
|
surface: surfaceById.get(entry.id) || (entry.path.includes("/.") ? "reusable-workflow" : "repository-workflow"),
|
|
@@ -821,7 +837,10 @@ function buildSiteBundle() {
|
|
|
821
837
|
...publicSurfaceLifecycle({
|
|
822
838
|
owner: "buildchain-workflows",
|
|
823
839
|
maturity: statusById.get(entry.id) || "active",
|
|
824
|
-
|
|
840
|
+
introducedVersion: engineeringHousekeeper ? packageJson.version : undefined,
|
|
841
|
+
nonDuplicationRationale: engineeringHousekeeper
|
|
842
|
+
? "One reusable policy and evidence boundary owns Engineering Housekeeper execution; scheduled callers contain cadence values only."
|
|
843
|
+
: "Existing workflow identity retained for caller compatibility and repository orchestration.",
|
|
825
844
|
}),
|
|
826
845
|
};
|
|
827
846
|
}),
|
|
@@ -82,6 +82,10 @@ export function inspectArtifactSigningRequests({
|
|
|
82
82
|
if (seen.has(key))
|
|
83
83
|
throw new Error(`duplicate artifact signing request: ${key}`);
|
|
84
84
|
seen.add(key);
|
|
85
|
+
const {
|
|
86
|
+
entitlementsProfile = "none",
|
|
87
|
+
entitlementsPaths = [],
|
|
88
|
+
} = request.signature;
|
|
85
89
|
const item = {
|
|
86
90
|
id: request.artifact.id,
|
|
87
91
|
slug: safeId(request.artifact.id),
|
|
@@ -104,6 +108,8 @@ export function inspectArtifactSigningRequests({
|
|
|
104
108
|
sourceSha: request.source.sha,
|
|
105
109
|
sourceTreeSha: request.source.treeSha,
|
|
106
110
|
transportFormat: request.artifact.transport?.format || "",
|
|
111
|
+
entitlementsProfile,
|
|
112
|
+
entitlementsPaths: entitlementsPaths.join(","),
|
|
107
113
|
};
|
|
108
114
|
if (request.signature.profile === "detached-signature-v1")
|
|
109
115
|
matrices.detached.push(item);
|
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import {
|
|
5
5
|
getVersionStrategy,
|
|
6
6
|
loadConfiguredAnchorManifest,
|
|
7
|
+
discoverConfiguredDerivedVersionMaterial,
|
|
7
8
|
} from "../packages/core/buildchain-config.js";
|
|
8
9
|
import {
|
|
9
10
|
alignMajorBootstrapReleaseImpact,
|
|
@@ -64,6 +65,10 @@ export function materializeSelfReleaseCandidateVersion({
|
|
|
64
65
|
throw new Error("self-release candidate requires declared version state");
|
|
65
66
|
}
|
|
66
67
|
const discoveredPaths = discovered.files.map((file) => file.path);
|
|
68
|
+
const derivedPaths = discoverConfiguredDerivedVersionMaterial(
|
|
69
|
+
cwd,
|
|
70
|
+
discovered.config,
|
|
71
|
+
).map((file) => file.path);
|
|
67
72
|
const versionStrategy = getVersionStrategy(discovered.config);
|
|
68
73
|
const anchorManifest = loadConfiguredAnchorManifest(cwd, discovered.config);
|
|
69
74
|
const lifecycleEnv = versionVerificationEnv(versionStrategy, anchorManifest, {
|
|
@@ -88,6 +93,7 @@ export function materializeSelfReleaseCandidateVersion({
|
|
|
88
93
|
allowedPaths: versionVerificationAllowedPathsForPromotion(
|
|
89
94
|
resolvedChannel,
|
|
90
95
|
discoveredPaths,
|
|
96
|
+
derivedPaths,
|
|
91
97
|
),
|
|
92
98
|
env: lifecycleEnv,
|
|
93
99
|
runLifecycleVerify: false,
|
|
@@ -7,6 +7,12 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
|
|
8
8
|
const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
|
|
9
9
|
const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
|
|
10
|
+
const RELEASE_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
11
|
+
const RELEASE_REDIRECT_HOSTS = new Set([
|
|
12
|
+
"github.com",
|
|
13
|
+
"release-assets.githubusercontent.com",
|
|
14
|
+
"objects.githubusercontent.com",
|
|
15
|
+
]);
|
|
10
16
|
|
|
11
17
|
function requiredString(value, label) {
|
|
12
18
|
if (typeof value !== "string" || value.trim() === "") {
|
|
@@ -104,7 +110,7 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
104
110
|
}
|
|
105
111
|
if (
|
|
106
112
|
exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
|
|
107
|
-
expected.
|
|
113
|
+
expected.candidateSourceSha ||
|
|
108
114
|
!["alpha", "stable"].includes(bundle.channel)
|
|
109
115
|
) {
|
|
110
116
|
throw new Error("installer bundle release identity mismatch");
|
|
@@ -228,6 +234,7 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
228
234
|
return {
|
|
229
235
|
schema: bundle.schema,
|
|
230
236
|
bundleRoot,
|
|
237
|
+
sourceCommit: bundle.sourceCommit,
|
|
231
238
|
manifestDigest: bundle.manifestDigest,
|
|
232
239
|
channel: bundle.channel,
|
|
233
240
|
channelPayloadRoot: bundle.channelPayloadRoot,
|
|
@@ -238,10 +245,9 @@ function validateInstallerBundle(evidence, expected) {
|
|
|
238
245
|
assets: bundle.assets,
|
|
239
246
|
};
|
|
240
247
|
}
|
|
241
|
-
|
|
242
248
|
export function validatePublicationCommitEvidence(
|
|
243
249
|
evidence,
|
|
244
|
-
{ version, sourceSha, releaseSha, releaseTag } = {},
|
|
250
|
+
{ version, sourceSha, candidateSourceSha, releaseSha, releaseTag } = {},
|
|
245
251
|
) {
|
|
246
252
|
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
|
|
247
253
|
throw new Error("publication commit evidence must be an object");
|
|
@@ -264,6 +270,17 @@ export function validatePublicationCommitEvidence(
|
|
|
264
270
|
throw new Error(`publication commit evidence ${field} mismatch`);
|
|
265
271
|
}
|
|
266
272
|
}
|
|
273
|
+
expected.candidateSourceSha = exactSha(
|
|
274
|
+
candidateSourceSha ?? identity.candidateSourceSha ?? expected.sourceSha,
|
|
275
|
+
"expected candidateSourceSha",
|
|
276
|
+
);
|
|
277
|
+
if (
|
|
278
|
+
identity.candidateSourceSha !== undefined &&
|
|
279
|
+
exactSha(identity.candidateSourceSha, "identity.candidateSourceSha") !==
|
|
280
|
+
expected.candidateSourceSha
|
|
281
|
+
) {
|
|
282
|
+
throw new Error("publication commit evidence candidateSourceSha mismatch");
|
|
283
|
+
}
|
|
267
284
|
const publicUrl = publicHttps(evidence.publication?.url, "publication.url");
|
|
268
285
|
const payloadRoot = sha256Root(
|
|
269
286
|
evidence.publication?.payloadRoot,
|
|
@@ -313,15 +330,47 @@ export async function verifyInstallerBundleReadback(
|
|
|
313
330
|
if (typeof fetchImpl !== "function") {
|
|
314
331
|
throw new Error("installer bundle read-back requires fetch");
|
|
315
332
|
}
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
333
|
+
const fetchReleaseReadback = async (url, label) => {
|
|
334
|
+
let current = url;
|
|
335
|
+
for (let hop = 0; hop <= 3; hop += 1) {
|
|
336
|
+
const response = await fetchImpl(current, {
|
|
337
|
+
redirect: "manual",
|
|
338
|
+
cache: "no-store",
|
|
339
|
+
});
|
|
340
|
+
if (response.status === 200) return response;
|
|
341
|
+
if (!RELEASE_REDIRECT_STATUSES.has(response.status)) {
|
|
342
|
+
throw new Error(`${label} failed: HTTP ${response.status}`);
|
|
343
|
+
}
|
|
344
|
+
if (hop === 3) {
|
|
345
|
+
throw new Error(`${label} exceeded the bounded redirect limit`);
|
|
346
|
+
}
|
|
347
|
+
const location = response.headers?.get?.("location");
|
|
348
|
+
if (!location) {
|
|
349
|
+
throw new Error(`${label} redirect omitted Location`);
|
|
350
|
+
}
|
|
351
|
+
const redirect = new URL(location, current);
|
|
352
|
+
if (
|
|
353
|
+
redirect.protocol !== "https:" ||
|
|
354
|
+
redirect.username ||
|
|
355
|
+
redirect.password ||
|
|
356
|
+
!RELEASE_REDIRECT_HOSTS.has(redirect.hostname) ||
|
|
357
|
+
(redirect.hostname === "github.com" &&
|
|
358
|
+
!redirect.pathname.startsWith(
|
|
359
|
+
"/kungfu-systems/kungfu/releases/download/",
|
|
360
|
+
))
|
|
361
|
+
) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`${label} redirected outside trusted GitHub release storage`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
current = redirect.href;
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`${label} failed without a terminal response`);
|
|
369
|
+
};
|
|
370
|
+
const manifestResponse = await fetchReleaseReadback(
|
|
371
|
+
result.publicUrl,
|
|
372
|
+
"installer bundle manifest read-back",
|
|
373
|
+
);
|
|
325
374
|
const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
|
|
326
375
|
if (digest(manifestBytes) !== bundle.manifestDigest) {
|
|
327
376
|
throw new Error("installer bundle manifest digest mismatch");
|
|
@@ -336,7 +385,7 @@ export async function verifyInstallerBundleReadback(
|
|
|
336
385
|
semanticRoot(unsigned) !== bundle.bundleRoot ||
|
|
337
386
|
manifest.package?.name !== "@kungfu-tech/site" ||
|
|
338
387
|
typeof manifest.package?.version !== "string" ||
|
|
339
|
-
manifest.identity?.sourceCommit !== result.identity.
|
|
388
|
+
manifest.identity?.sourceCommit !== result.identity.candidateSourceSha ||
|
|
340
389
|
manifest.identity?.releaseSha !== result.identity.releaseSha ||
|
|
341
390
|
manifest.identity?.releaseTag !== result.identity.releaseTag ||
|
|
342
391
|
manifest.identity?.version !== result.identity.version ||
|
|
@@ -365,15 +414,10 @@ export async function verifyInstallerBundleReadback(
|
|
|
365
414
|
for (const asset of bundle.assets) {
|
|
366
415
|
let observation = byUrl.get(asset.releaseUrl);
|
|
367
416
|
if (!observation) {
|
|
368
|
-
const response = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (response.status !== 200) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`installer bundle asset read-back failed: HTTP ${response.status}`,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
417
|
+
const response = await fetchReleaseReadback(
|
|
418
|
+
asset.releaseUrl,
|
|
419
|
+
"installer bundle asset read-back",
|
|
420
|
+
);
|
|
377
421
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
378
422
|
observation = {
|
|
379
423
|
releaseUrl: asset.releaseUrl,
|
|
@@ -394,7 +438,7 @@ export async function verifyInstallerBundleReadback(
|
|
|
394
438
|
schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
|
|
395
439
|
bundleRoot: bundle.bundleRoot,
|
|
396
440
|
manifestDigest: bundle.manifestDigest,
|
|
397
|
-
sourceCommit: result.identity.
|
|
441
|
+
sourceCommit: result.identity.candidateSourceSha,
|
|
398
442
|
releaseTag: result.identity.releaseTag,
|
|
399
443
|
releasePassport: bundle.releasePassport,
|
|
400
444
|
observations,
|
|
@@ -409,6 +453,8 @@ async function main(args) {
|
|
|
409
453
|
if (value === "--evidence") options.evidence = args[++index];
|
|
410
454
|
else if (value === "--version") options.version = args[++index];
|
|
411
455
|
else if (value === "--source-sha") options.sourceSha = args[++index];
|
|
456
|
+
else if (value === "--candidate-source-sha")
|
|
457
|
+
options.candidateSourceSha = args[++index];
|
|
412
458
|
else if (value === "--release-sha") options.releaseSha = args[++index];
|
|
413
459
|
else if (value === "--release-tag") options.releaseTag = args[++index];
|
|
414
460
|
else throw new Error(`unknown argument: ${value}`);
|