@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.
Files changed (67) hide show
  1. package/actions/promote-buildchain-ref/README.md +10 -0
  2. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  3. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  4. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  5. package/dist/site/buildchain-contract.json +24 -24
  6. package/dist/site/buildchain-site.json +91 -30
  7. package/dist/site/capability-registry.json +3 -3
  8. package/dist/site/controller-registry.json +6 -2
  9. package/dist/site/kfd-claims.json +122 -11
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +8 -7
  12. package/dist/site/node-api-registry.json +683 -105
  13. package/dist/site/page-registry.json +80 -19
  14. package/dist/site/public-surface-audit.json +98 -7
  15. package/dist/site/publication-authority-registry.json +81 -1
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +2 -0
  18. package/dist/site/site-manifest.json +11 -11
  19. package/dist/site/workflow-registry.json +119 -2
  20. package/docs/MAP.md +1 -0
  21. package/docs/auditable-demo.md +2 -2
  22. package/docs/dev-delivery-warrant.md +49 -4
  23. package/docs/engineering-housekeeper.md +138 -0
  24. package/docs/lifecycle-protocol.md +4 -2
  25. package/docs/node-api-reference.md +277 -212
  26. package/docs/release-governance.md +17 -2
  27. package/docs/release-tail-provider-plane.md +1 -1
  28. package/docs/reusable-build-surface.md +11 -0
  29. package/package.json +4 -1
  30. package/packages/core/artifact-signing.js +61 -0
  31. package/packages/core/buildchain-config.js +66 -6
  32. package/packages/core/buildchain-publication-authority.js +4 -0
  33. package/packages/core/controller-evidence.js +2 -1
  34. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  35. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  36. package/packages/core/dev-delivery-warrant.js +15 -6
  37. package/packages/core/diagnostics.js +8 -3
  38. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  39. package/packages/core/engineering-housekeeper-github.js +501 -0
  40. package/packages/core/engineering-housekeeper.js +259 -0
  41. package/packages/core/index.js +3 -0
  42. package/packages/core/kfd-gate.js +45 -15
  43. package/packages/core/publication-rehearsal-runtime.js +13 -1
  44. package/packages/core/release-passport.js +130 -20
  45. package/scripts/assemble-self-publication-admission.mjs +1 -1
  46. package/scripts/audit-publication-control-plane.mjs +1 -1
  47. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  48. package/scripts/auditable-demo-platform.mjs +2 -2
  49. package/scripts/auditable-demo-renditions.mjs +1 -1
  50. package/scripts/auditable-demo.mjs +2 -2
  51. package/scripts/build-contract-core.mjs +8 -3
  52. package/scripts/build-standalone-binary.mjs +14 -3
  53. package/scripts/check-inventory.mjs +3 -1
  54. package/scripts/dev-delivery-warrant.mjs +31 -4
  55. package/scripts/dev-pr-auto-merge.mjs +30 -4
  56. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  57. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  58. package/scripts/generate-site-bundle.mjs +23 -4
  59. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  60. package/scripts/materialize-self-release-candidate-version.mjs +6 -0
  61. package/scripts/publication-commit-evidence.mjs +69 -23
  62. package/scripts/release-candidate-resolver.mjs +16 -10
  63. package/scripts/resume-from-candidate-run.mjs +123 -9
  64. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  65. package/scripts/site-capability-metadata.mjs +2 -0
  66. package/scripts/web-surface-core.mjs +8 -2
  67. 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({ owner, maturity, nonDuplicationRationale }) {
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: "pre-3.0.2-alpha.4",
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
- nonDuplicationRationale: "Existing workflow identity retained for caller compatibility and repository orchestration.",
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.sourceSha ||
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 manifestResponse = await fetchImpl(result.publicUrl, {
317
- redirect: "manual",
318
- cache: "no-store",
319
- });
320
- if (manifestResponse.status !== 200) {
321
- throw new Error(
322
- `installer bundle manifest read-back failed: HTTP ${manifestResponse.status}`,
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.sourceSha ||
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 fetchImpl(asset.releaseUrl, {
369
- redirect: "manual",
370
- cache: "no-store",
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.sourceSha,
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}`);