@kungfu-tech/buildchain 3.0.7 → 3.0.8

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 (45) hide show
  1. package/contracts/engineering-housekeeper-v1.schema.json +11 -1
  2. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +10 -0
  3. package/contracts/release-cut-v1.schema.json +70 -0
  4. package/contracts/release-train-transition-v1.schema.json +85 -0
  5. package/contracts/release-train-v1.schema.json +82 -0
  6. package/dist/site/buildchain-contract.json +302 -16
  7. package/dist/site/buildchain-site.json +83 -27
  8. package/dist/site/capability-registry.json +4 -3
  9. package/dist/site/kfd-claims.json +45 -8
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +22 -6
  12. package/dist/site/node-api-registry.json +1218 -489
  13. package/dist/site/page-registry.json +65 -17
  14. package/dist/site/public-surface-audit.json +13 -8
  15. package/dist/site/publication-authority-registry.json +2 -4
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +1 -0
  18. package/dist/site/site-manifest.json +18 -10
  19. package/dist/site/workflow-registry.json +10 -9
  20. package/docs/MAP.md +1 -1
  21. package/docs/aws-us-elastic-runner-burst-plane.md +20 -7
  22. package/docs/engineering-housekeeper.md +61 -28
  23. package/docs/node-api-reference.md +109 -77
  24. package/docs/release-governance.md +13 -10
  25. package/docs/release-train.md +102 -0
  26. package/docs/reusable-build-surface.md +16 -6
  27. package/docs/site-bundle-contract.md +5 -1
  28. package/docs/versioning.md +1 -0
  29. package/package.json +2 -1
  30. package/packages/core/buildchain-agent-manuals.js +1 -0
  31. package/packages/core/buildchain-compatibility-proof.js +577 -0
  32. package/packages/core/buildchain-contract.js +46 -181
  33. package/packages/core/engineering-housekeeper-github-client.js +18 -1
  34. package/packages/core/engineering-housekeeper-github.js +247 -41
  35. package/packages/core/engineering-housekeeper.js +15 -0
  36. package/packages/core/release-train.js +520 -0
  37. package/scripts/aws-macos-jit-controller-core.mjs +33 -11
  38. package/scripts/aws-macos-jit-controller-runtime.mjs +208 -0
  39. package/scripts/aws-macos-jit-controller.mjs +50 -37
  40. package/scripts/aws-macos-jit-core.mjs +41 -3
  41. package/scripts/buildchain-contract-lock.mjs +6 -0
  42. package/scripts/dispatch-artifact-signing-authority.mjs +4 -7
  43. package/scripts/engineering-housekeeper-workflow.mjs +13 -6
  44. package/scripts/generate-site-bundle.mjs +2 -0
  45. package/scripts/site-capability-metadata.mjs +1 -0
@@ -80,3 +80,211 @@ export function assertDryRun(result, label) {
80
80
  throw new Error(`${label} did not return DryRunOperation`);
81
81
  }
82
82
  }
83
+
84
+ export function assertAllowedPolicySimulation(
85
+ plan,
86
+ profile,
87
+ principalArn,
88
+ actionName,
89
+ ) {
90
+ if (!/^arn:aws:iam::\d{12}:(?:user|role)\/.+/.test(principalArn || "")) {
91
+ throw new Error(
92
+ "AWS allocation policy simulation requires an IAM user or role principal ARN",
93
+ );
94
+ }
95
+ const contextEntries = [
96
+ `ContextKeyName=aws:RequestedRegion,ContextKeyValues=${plan.aws.region},ContextKeyType=string`,
97
+ ...plan.aws.hostTags.map(
98
+ ({ Key, Value }) =>
99
+ `ContextKeyName=aws:RequestTag/${Key},ContextKeyValues=${Value},ContextKeyType=string`,
100
+ ),
101
+ ];
102
+ const evaluation = awsJson(
103
+ plan,
104
+ profile,
105
+ [
106
+ "iam",
107
+ "simulate-principal-policy",
108
+ "--policy-source-arn",
109
+ principalArn,
110
+ "--action-names",
111
+ actionName,
112
+ "--resource-arns",
113
+ "*",
114
+ "--context-entries",
115
+ ...contextEntries,
116
+ "--output",
117
+ "json",
118
+ ],
119
+ `${actionName} IAM policy simulation`,
120
+ ).EvaluationResults?.[0];
121
+ if (
122
+ evaluation?.EvalActionName !== actionName ||
123
+ evaluation?.EvalDecision !== "allowed" ||
124
+ (evaluation.MissingContextValues || []).length !== 0
125
+ ) {
126
+ throw new Error(`${actionName} IAM policy simulation did not allow allocation`);
127
+ }
128
+ return {
129
+ actionName,
130
+ decision: evaluation.EvalDecision,
131
+ principalArn,
132
+ };
133
+ }
134
+
135
+ function sameJson(left, right) {
136
+ return JSON.stringify(left) === JSON.stringify(right);
137
+ }
138
+
139
+ function sameDimensionSet(observed, expected) {
140
+ return (
141
+ Array.isArray(observed) &&
142
+ observed.length === expected.length &&
143
+ expected.every((entry) =>
144
+ observed.some((candidate) => sameJson(candidate, entry)),
145
+ )
146
+ );
147
+ }
148
+
149
+ export function assertMacosBudgetLaunchGate(plan, profile) {
150
+ const identity = awsJson(
151
+ plan,
152
+ profile,
153
+ ["sts", "get-caller-identity", "--output", "json"],
154
+ "AWS account identity preflight",
155
+ );
156
+ if (identity.Account !== plan.account.id) {
157
+ throw new Error("AWS account identity mismatch");
158
+ }
159
+ const workflow = ghJson(
160
+ [
161
+ "api",
162
+ `repos/${plan.repository}/actions/workflows/${plan.github.workflowId}`,
163
+ ],
164
+ undefined,
165
+ "GitHub macOS workflow preflight",
166
+ );
167
+ if (workflow.state !== plan.github.requiredState) {
168
+ throw new Error(
169
+ `macOS workflow must be ${plan.github.requiredState}; observed ${workflow.state || "unknown"}`,
170
+ );
171
+ }
172
+ const stack = awsJson(
173
+ plan,
174
+ profile,
175
+ [
176
+ "cloudformation",
177
+ "describe-stacks",
178
+ "--stack-name",
179
+ plan.aws.controlPlaneStack,
180
+ "--output",
181
+ "json",
182
+ ],
183
+ "macOS control-plane stack preflight",
184
+ ).Stacks?.[0];
185
+ if (!stack || !/(?:CREATE|UPDATE)_COMPLETE$/.test(stack.StackStatus || "")) {
186
+ throw new Error("macOS control-plane stack is not complete");
187
+ }
188
+ const topicArn = (stack.Outputs || []).find(
189
+ (entry) => entry.OutputKey === "KillSwitchTopic",
190
+ )?.OutputValue;
191
+ if (!/^arn:aws:sns:/.test(String(topicArn || ""))) {
192
+ throw new Error("macOS Budget kill-switch topic readback failed");
193
+ }
194
+ const budget = awsJson(
195
+ plan,
196
+ profile,
197
+ [
198
+ "budgets",
199
+ "describe-budget",
200
+ "--account-id",
201
+ plan.account.id,
202
+ "--budget-name",
203
+ plan.safety.budget.name,
204
+ "--show-filter-expression",
205
+ "--output",
206
+ "json",
207
+ ],
208
+ "macOS Budget preflight",
209
+ ).Budget;
210
+ const expectedDimensions = [
211
+ ["USAGE_TYPE", plan.safety.budget.dimensionFilter.usageTypes],
212
+ ["OPERATION", [plan.safety.budget.dimensionFilter.operation]],
213
+ ["REGION", plan.safety.budget.dimensionFilter.regions],
214
+ ].map(([Key, Values]) => ({
215
+ Dimensions: { Key, Values, MatchOptions: ["EQUALS"] },
216
+ }));
217
+ if (
218
+ !budget ||
219
+ budget.BudgetName !== plan.safety.budget.name ||
220
+ Number(budget.BudgetLimit?.Amount) !== plan.safety.budget.limitUsd ||
221
+ budget.BudgetLimit?.Unit !== "USD" ||
222
+ budget.BudgetType !== "COST" ||
223
+ !sameJson(budget.Metrics, plan.safety.budget.metrics) ||
224
+ !sameDimensionSet(budget.FilterExpression?.And, expectedDimensions)
225
+ ) {
226
+ throw new Error("macOS Budget identity or dimension filter mismatch");
227
+ }
228
+ const notifications = awsJson(
229
+ plan,
230
+ profile,
231
+ [
232
+ "budgets",
233
+ "describe-notifications-for-budget",
234
+ "--account-id",
235
+ plan.account.id,
236
+ "--budget-name",
237
+ plan.safety.budget.name,
238
+ "--output",
239
+ "json",
240
+ ],
241
+ "macOS Budget notifications preflight",
242
+ ).Notifications;
243
+ const thresholds = (notifications || [])
244
+ .filter(
245
+ (entry) =>
246
+ entry.NotificationType === "ACTUAL" &&
247
+ (entry.ThresholdType || "PERCENTAGE") === "PERCENTAGE",
248
+ )
249
+ .map((entry) => Number(entry.Threshold))
250
+ .sort((left, right) => left - right);
251
+ if (!sameJson(thresholds, plan.safety.budget.requiredActualThresholds)) {
252
+ throw new Error("macOS Budget notification thresholds mismatch");
253
+ }
254
+ for (const notification of notifications) {
255
+ const subscribers = awsJson(
256
+ plan,
257
+ profile,
258
+ [
259
+ "budgets",
260
+ "describe-subscribers-for-notification",
261
+ "--account-id",
262
+ plan.account.id,
263
+ "--budget-name",
264
+ plan.safety.budget.name,
265
+ "--notification",
266
+ JSON.stringify(notification),
267
+ "--output",
268
+ "json",
269
+ ],
270
+ `macOS Budget ${notification.Threshold}% subscribers preflight`,
271
+ ).Subscribers;
272
+ if (
273
+ !(subscribers || []).some(
274
+ (entry) =>
275
+ entry.SubscriptionType === "SNS" && entry.Address === topicArn,
276
+ )
277
+ ) {
278
+ throw new Error("macOS Budget SNS subscriber mismatch");
279
+ }
280
+ }
281
+ return {
282
+ accountId: identity.Account,
283
+ principalArn: identity.Arn,
284
+ workflowState: workflow.state,
285
+ stackStatus: stack.StackStatus,
286
+ budgetName: budget.BudgetName,
287
+ budgetDimensionFilter: plan.safety.budget.dimensionFilter,
288
+ budgetThresholds: thresholds,
289
+ };
290
+ }
@@ -11,9 +11,12 @@ import {
11
11
  macosReleaseHostsArgs,
12
12
  macosRunInstancesArgs,
13
13
  } from "./aws-macos-jit-controller-core.mjs";
14
+ import { MACOS_EC2_JIT_REGIONS } from "./aws-macos-jit-core.mjs";
14
15
  import { executeMacosJitJob } from "./aws-macos-jit-job-controller.mjs";
15
16
  import {
16
17
  assertDryRun,
18
+ assertAllowedPolicySimulation,
19
+ assertMacosBudgetLaunchGate,
17
20
  assertOwnership,
18
21
  awsArgs,
19
22
  awsJson,
@@ -32,6 +35,7 @@ function flag(name) {
32
35
  }
33
36
 
34
37
  function assertCampaignLaunchPreflight(plan, profile) {
38
+ const launchGate = assertMacosBudgetLaunchGate(plan, profile);
35
39
  const commit = ghJson(
36
40
  ["api", `repos/${plan.repository}/commits/${plan.source.sha}`],
37
41
  undefined,
@@ -40,43 +44,51 @@ function assertCampaignLaunchPreflight(plan, profile) {
40
44
  if (String(commit.sha || "").toLowerCase() !== plan.source.sha) {
41
45
  throw new Error("GitHub did not resolve the exact campaign source SHA");
42
46
  }
43
- const activeHosts =
44
- awsJson(
45
- plan,
47
+ const regionalPlans = Object.keys(MACOS_EC2_JIT_REGIONS).map((region) => ({
48
+ ...plan,
49
+ aws: { ...plan.aws, region },
50
+ }));
51
+ const activeHosts = regionalPlans.flatMap(
52
+ (regionalPlan) =>
53
+ awsJson(
54
+ regionalPlan,
55
+ profile,
56
+ [
57
+ "ec2",
58
+ "describe-hosts",
59
+ "--filter",
60
+ "Name=tag:kungfu:plane,Values=aws-us-elastic-runner-burst",
61
+ "Name=tag:kungfu:provider,Values=macos-ec2-jit",
62
+ "Name=state,Values=available,pending,under-assessment",
63
+ "--output",
64
+ "json",
65
+ ],
66
+ `active macOS JIT host preflight in ${regionalPlan.aws.region}`,
67
+ ).Hosts || [],
68
+ );
69
+ if (activeHosts.length >= plan.safety.activeHostCeiling) {
70
+ throw new Error("macOS JIT active Dedicated Host ceiling is reached");
71
+ }
72
+ const instances = regionalPlans.flatMap((regionalPlan) => {
73
+ const active = awsJson(
74
+ regionalPlan,
46
75
  profile,
47
76
  [
48
77
  "ec2",
49
- "describe-hosts",
50
- "--filter",
78
+ "describe-instances",
79
+ "--filters",
51
80
  "Name=tag:kungfu:plane,Values=aws-us-elastic-runner-burst",
52
81
  "Name=tag:kungfu:provider,Values=macos-ec2-jit",
53
- "Name=state,Values=available,pending,under-assessment",
82
+ "Name=instance-state-name,Values=pending,running,stopping,stopped,shutting-down",
54
83
  "--output",
55
84
  "json",
56
85
  ],
57
- "active macOS JIT host preflight",
58
- ).Hosts || [];
59
- if (activeHosts.length >= plan.safety.activeHostCeiling) {
60
- throw new Error("macOS JIT active Dedicated Host ceiling is reached");
61
- }
62
- const active = awsJson(
63
- plan,
64
- profile,
65
- [
66
- "ec2",
67
- "describe-instances",
68
- "--filters",
69
- "Name=tag:kungfu:plane,Values=aws-us-elastic-runner-burst",
70
- "Name=tag:kungfu:provider,Values=macos-ec2-jit",
71
- "Name=instance-state-name,Values=pending,running,stopping,stopped,shutting-down",
72
- "--output",
73
- "json",
74
- ],
75
- "active macOS JIT instance preflight",
76
- );
77
- const instances = (active.Reservations || []).flatMap(
78
- (reservation) => reservation.Instances || [],
79
- );
86
+ `active macOS JIT instance preflight in ${regionalPlan.aws.region}`,
87
+ );
88
+ return (active.Reservations || []).flatMap(
89
+ (reservation) => reservation.Instances || [],
90
+ );
91
+ });
80
92
  if (instances.length >= plan.safety.activeInstanceCeiling) {
81
93
  throw new Error("macOS JIT active instance ceiling is reached");
82
94
  }
@@ -120,6 +132,7 @@ function assertCampaignLaunchPreflight(plan, profile) {
120
132
  );
121
133
  }
122
134
  return {
135
+ ...launchGate,
123
136
  exactCommit: plan.source.sha,
124
137
  activeHosts: activeHosts.length,
125
138
  activeInstances: instances.length,
@@ -136,12 +149,11 @@ export function executeMacosJitCampaignLaunch(plan, { profile = "" } = {}) {
136
149
  throw new Error("macOS JIT campaign launch plan contract is invalid");
137
150
  }
138
151
  const preflight = assertCampaignLaunchPreflight(plan, profile);
139
- assertDryRun(
140
- commandResult(
141
- "aws",
142
- awsArgs(plan, profile, macosAllocateHostsArgs(plan, { dryRun: true })),
143
- ),
144
- "EC2 AllocateHosts DryRun",
152
+ const allocationPermission = assertAllowedPolicySimulation(
153
+ plan,
154
+ profile,
155
+ preflight.principalArn,
156
+ "ec2:AllocateHosts",
145
157
  );
146
158
  const hostId = awsJson(
147
159
  plan,
@@ -223,8 +235,8 @@ export function executeMacosJitCampaignLaunch(plan, { profile = "" } = {}) {
223
235
  imageId: instance.ImageId,
224
236
  launchTime: new Date(instance.LaunchTime).toISOString(),
225
237
  },
226
- preflight,
227
- dryRuns: ["AllocateHosts:DryRunOperation", "RunInstances:DryRunOperation"],
238
+ preflight: { ...preflight, allocationPermission },
239
+ dryRuns: ["RunInstances:DryRunOperation"],
228
240
  planDigest: plan.digest,
229
241
  };
230
242
  }
@@ -462,6 +474,7 @@ export function executeMacosJitCampaignClose(plan, { profile = "" } = {}) {
462
474
  function commonValues(execute) {
463
475
  return {
464
476
  execute,
477
+ accountId: arg("account-id"),
465
478
  repository: arg("repository", "kungfu-systems/kungfu"),
466
479
  campaignId: arg("campaign-id"),
467
480
  sourceSha: arg("source-sha"),
@@ -2,13 +2,29 @@ import { digest } from "./aws-runner-burst-core.mjs";
2
2
 
3
3
  export const AWS_MACOS_JIT_CONTRACT = "kungfu-buildchain-aws-macos-jit/v1";
4
4
 
5
+ export const MACOS_EC2_JIT_REGIONS = Object.freeze({
6
+ "us-east-1": Object.freeze({
7
+ stack: "kungfu-buildchain-macos-jit",
8
+ }),
9
+ "us-east-2": Object.freeze({
10
+ stack: "kungfu-buildchain-macos-jit-us-east-2",
11
+ }),
12
+ });
13
+
5
14
  export const MACOS_EC2_JIT = Object.freeze({
6
15
  phase: "macos-ec2-jit",
7
16
  region: "us-east-1",
8
17
  repository: "kungfu-systems/kungfu",
18
+ workflowId: "323846928",
9
19
  stack: "kungfu-buildchain-macos-jit",
20
+ budgetName: "kungfu-buildchain-macos-jit-actual-spend",
21
+ budgetUsageTypes: Object.freeze([
22
+ "HostUsage:mac2",
23
+ "USE2-HostUsage:mac2",
24
+ ]),
25
+ budgetOperation: "RunInstances",
10
26
  instanceType: "mac2.metal",
11
- pricePerHourUsd: 0.6498,
27
+ pricePerHourUsd: 0.65,
12
28
  minimumHostAllocationHours: 24,
13
29
  maximumHostAllocationHours: 30,
14
30
  maxAcceptedHosts: 1,
@@ -23,6 +39,22 @@ export const MACOS_EC2_JIT = Object.freeze({
23
39
  jitParameterPrefix: "/kungfu/burst/macos/",
24
40
  });
25
41
 
42
+ export function macosJitRegionConfig(region) {
43
+ const resolved = String(region || MACOS_EC2_JIT.region).trim();
44
+ const config = MACOS_EC2_JIT_REGIONS[resolved];
45
+ if (!config) {
46
+ throw new Error(
47
+ `region must be one of ${Object.keys(MACOS_EC2_JIT_REGIONS).join(", ")}`,
48
+ );
49
+ }
50
+ return {
51
+ ...config,
52
+ budgetName: MACOS_EC2_JIT.budgetName,
53
+ budgetUsageTypes: MACOS_EC2_JIT.budgetUsageTypes,
54
+ budgetRegions: Object.keys(MACOS_EC2_JIT_REGIONS),
55
+ };
56
+ }
57
+
26
58
  function exactSha(value, label) {
27
59
  const normalized = String(value || "")
28
60
  .trim()
@@ -323,8 +355,14 @@ export function verifyMacosEc2JitQualification({
323
355
  }
324
356
  let allocationHours = 0;
325
357
  try {
326
- const allocatedAt = iso(hostLifecycle.allocatedAt, "hostLifecycle.allocatedAt");
327
- const releasedAt = iso(hostLifecycle.releasedAt, "hostLifecycle.releasedAt");
358
+ const allocatedAt = iso(
359
+ hostLifecycle.allocatedAt,
360
+ "hostLifecycle.allocatedAt",
361
+ );
362
+ const releasedAt = iso(
363
+ hostLifecycle.releasedAt,
364
+ "hostLifecycle.releasedAt",
365
+ );
328
366
  allocationHours = elapsedHours(allocatedAt, releasedAt);
329
367
  if (
330
368
  hostLifecycle.status !== "passed" ||
@@ -106,6 +106,9 @@ export function checkBuildchainContractLock({
106
106
  `- Runtime SHA: \`${runtimeSha || "(unknown)"}\``,
107
107
  `- Contract digest: \`${current.contractDigest}\``,
108
108
  `- Compatibility digest: \`${current.compatibilityDigest}\``,
109
+ `- Compatibility proof registry: \`${current.compatibilityProofRegistryRoot || "(legacy)"}\``,
110
+ `- Verification receipt: \`${evaluation.receiptRoot || "(not-required)"}\``,
111
+ evaluation.usedProofRoots?.length ? `- Used compatibility proofs: ${evaluation.usedProofRoots.map((root) => `\`${root}\``).join(", ")}` : "",
109
112
  evaluation.reasons?.length ? `- Reasons: ${evaluation.reasons.join("; ")}` : "",
110
113
  shouldIssue ? `- Drift issue body: \`${issueBodyPath}\`` : "",
111
114
  ].filter(Boolean).join("\n"));
@@ -117,6 +120,9 @@ export function checkBuildchainContractLock({
117
120
  "contract-lock-issue-body-file": shouldIssue ? issueBodyPath : "",
118
121
  "contract-digest": current.contractDigest,
119
122
  "contract-compatibility-digest": current.compatibilityDigest,
123
+ "contract-compatibility-proof-registry-root": current.compatibilityProofRegistryRoot || "",
124
+ "contract-compatibility-verification-receipt-root": evaluation.receiptRoot || "",
125
+ "contract-compatibility-proof-roots": (evaluation.usedProofRoots || []).join(","),
120
126
  "accepted-contract-digest": evaluation.accepted?.contractDigest || "",
121
127
  "accepted-buildchain-sha": evaluation.accepted?.resolvedSha || "",
122
128
  "current-buildchain-sha": runtimeSha || "",
@@ -39,18 +39,15 @@ export async function resolveArtifactSigningAuthorityRuntime({
39
39
  const ref = resolveAuthorityDispatchRef(authorityRef);
40
40
  const encodedRef = ref.split("/").map(encodeURIComponent).join("/");
41
41
  const response = await requestImpl(
42
- `/repos/${authorityRepo}/git/ref/heads/${encodedRef}`,
42
+ `/repos/${authorityRepo}/commits/${encodedRef}`,
43
43
  { token: required(token, "Buildchain authority dispatch token") },
44
44
  );
45
- if (response?.object?.type !== "commit") {
45
+ const sha = String(response?.sha || "");
46
+ if (!/^[0-9a-f]{40}$/u.test(sha)) {
46
47
  throw new Error(
47
- "Buildchain signing authority ref does not resolve to a commit",
48
+ "Buildchain signing authority ref must resolve to an exact commit SHA",
48
49
  );
49
50
  }
50
- const sha = String(response.object.sha || "");
51
- if (!/^[0-9a-f]{40}$/u.test(sha)) {
52
- throw new Error("Buildchain signing authority runtime SHA must be exact");
53
- }
54
51
  return { ref, sha };
55
52
  }
56
53
 
@@ -86,10 +86,11 @@ export function normalizeHousekeeperWorkflowOptions(options = {}) {
86
86
  process.env.HOUSEKEEPER_REPOSITORY ||
87
87
  process.env.GITHUB_REPOSITORY,
88
88
  ),
89
- targetBranch: requiredString(
90
- options.targetBranch || process.env.HOUSEKEEPER_TARGET_BRANCH,
91
- "target-branch",
92
- ).replace(/^refs\/heads\//, ""),
89
+ targetBranch: String(
90
+ options.targetBranch || process.env.HOUSEKEEPER_TARGET_BRANCH || "",
91
+ )
92
+ .trim()
93
+ .replace(/^refs\/heads\//, ""),
93
94
  staleDays: positiveInteger(
94
95
  options.staleDays ?? process.env.HOUSEKEEPER_STALE_DAYS,
95
96
  30,
@@ -108,6 +109,11 @@ export function normalizeHousekeeperWorkflowOptions(options = {}) {
108
109
  options.retainedPatterns ?? process.env.HOUSEKEEPER_RETAINED_PATTERNS,
109
110
  ["train/**", "authority/**"],
110
111
  ),
112
+ temporaryBranchPatterns: splitPatterns(
113
+ options.temporaryBranchPatterns ??
114
+ process.env.HOUSEKEEPER_TEMPORARY_BRANCH_PATTERNS,
115
+ ["feature/**", "fix/**", "chore/**", "docs/**", "ci/**", "refactor/**"],
116
+ ),
111
117
  stalePullRequestLabel: String(
112
118
  options.stalePullRequestLabel ??
113
119
  process.env.HOUSEKEEPER_STALE_PR_LABEL ??
@@ -137,6 +143,7 @@ function workflowPolicy(options) {
137
143
  return {
138
144
  protectedPatterns: options.protectedPatterns,
139
145
  retainedPatterns: options.retainedPatterns,
146
+ temporaryBranchPatterns: options.temporaryBranchPatterns,
140
147
  pullRequests: {
141
148
  reportStale: true,
142
149
  label: options.stalePullRequestLabel,
@@ -167,7 +174,7 @@ export function renderHousekeeperWorkflowReport(
167
174
  `Mode: \`${mode || "report"}\``,
168
175
  `Scope: \`${scope}\``,
169
176
  `Repository: \`${plan.repository}\``,
170
- `Target ref: \`${plan.target.name}@${plan.target.headOid}\``,
177
+ `Primary mainline: \`${plan.target.name}@${plan.target.headOid}\``,
171
178
  `Observed at: \`${plan.observedAt}\``,
172
179
  `Plan root: \`${plan.planRoot}\``,
173
180
  `Receipt root: \`${receipt.receiptRoot}\``,
@@ -284,7 +291,7 @@ export async function applyHousekeeperWorkflowScope({
284
291
  }
285
292
  if (
286
293
  plan.repository !== options.repository ||
287
- plan.target.name !== options.targetBranch
294
+ (options.targetBranch && plan.target.name !== options.targetBranch)
288
295
  ) {
289
296
  throw new Error(
290
297
  "plan repository or target branch does not match current workflow inputs",
@@ -370,6 +370,7 @@ const manualMetaById = new Map(Object.entries({
370
370
  "publication-rehearsal": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent", "maintainer"], maturity: "preview", order: 124 },
371
371
  "release-activation-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 125 },
372
372
  "release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
373
+ "release-train": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 132 },
373
374
  "stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
374
375
  "dev-qualification-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 136 },
375
376
  "dev-alpha-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 137 },
@@ -734,6 +735,7 @@ function buildSiteBundle() {
734
735
  "docs/kfd-support.md",
735
736
  "docs/reusable-build-surface.md",
736
737
  "docs/release-candidate.md",
738
+ "docs/release-train.md",
737
739
  "docs/stable-candidate-patrol.md",
738
740
  "docs/dev-qualification-patrol.md",
739
741
  "docs/dev-alpha-candidate-patrol.md",
@@ -202,6 +202,7 @@ export function nodeApiMeta(exportName) {
202
202
  "./release-passport-contract": { group: "release-passport-trust", summary: "Standalone release passport JSON Schema, ownership/check manifest, and structural validation APIs." },
203
203
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
204
204
  "./release-candidate-recovery": { group: "release-passport-trust", summary: "Fail-closed verification and immutable receipts for reusing a successful sealed candidate run without rebuilding product payloads." },
205
+ "./release-train": { group: "governance-versioning", summary: "Frozen Release Cut identity, rooted Release Train transitions, Dev observations, supersession, replay, and legacy read compatibility APIs." },
205
206
  "./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
206
207
  "./release-propagation": { group: "site-and-propagation", summary: "Release propagation graph, plan, and exact upstream lock APIs." },
207
208
  "./release-activation-transaction": { group: "release-passport-trust", summary: "Ordered cross-repository activation, exact receipt-set binding, retry, abort, rollback, and shadow-rehearsal APIs." },