@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -0,0 +1,374 @@
1
+ import { digest } from "./aws-runner-burst-core.mjs";
2
+
3
+ export const AWS_WINDOWS_JIT_CONTRACT = "kungfu-buildchain-aws-windows-jit/v1";
4
+
5
+ export const WINDOWS_EC2_JIT = Object.freeze({
6
+ phase: "windows-ec2-jit",
7
+ region: "us-east-1",
8
+ repository: "kungfu-systems/kungfu",
9
+ stack: "kungfu-buildchain-windows-jit",
10
+ instanceType: "c7i.4xlarge",
11
+ pricePerHourUsd: 1.45,
12
+ maximumInstanceLifetimeMinutes: 180,
13
+ maxConcurrentInstances: 2,
14
+ maxAcceptedInstances: 6,
15
+ budgetLimitUsd: 40,
16
+ minimumSmokeJobs: 1,
17
+ minimumFullJobs: 3,
18
+ maximumCleanupLatencySeconds: 900,
19
+ runnerVersion: "2.336.0",
20
+ runnerArchive: "actions-runner-win-x64-2.336.0.zip",
21
+ runnerArchiveSha256:
22
+ "d59123a43003e357b0805b5d0f611d0bd2f65ab67d51bd070dd4e7a0f685c162",
23
+ powerShellVersion: "7.6.4",
24
+ powerShellArchive: "PowerShell-7.6.4-win-x64.msi",
25
+ powerShellArchiveSha256:
26
+ "d11942df52fd12470169797abfa4781d9480efdc81000ba4fa55a5b921ed8dd0",
27
+ amiSsmParameter:
28
+ "/aws/service/ami-windows-latest/Windows_Server-2025-English-Full-Base",
29
+ labelPrefix: "aws-us-ec2-windows-jit-",
30
+ jitParameterPrefix: "/kungfu/burst/windows/",
31
+ });
32
+
33
+ function exactSha(value, label) {
34
+ const normalized = String(value || "")
35
+ .trim()
36
+ .toLowerCase();
37
+ if (!/^[0-9a-f]{40}$/.test(normalized)) {
38
+ throw new Error(`${label} must be an exact 40-character Git SHA`);
39
+ }
40
+ return normalized;
41
+ }
42
+
43
+ function finiteNumber(value, label) {
44
+ const parsed = Number(value);
45
+ if (!Number.isFinite(parsed) || parsed < 0) {
46
+ throw new Error(`${label} must be a non-negative finite number`);
47
+ }
48
+ return parsed;
49
+ }
50
+
51
+ function iso(value, label) {
52
+ const parsed = new Date(value || "");
53
+ if (!Number.isFinite(parsed.getTime())) {
54
+ throw new Error(`${label} must be an ISO timestamp`);
55
+ }
56
+ return parsed.toISOString();
57
+ }
58
+
59
+ export function windowsJitRunnerLabel(value) {
60
+ const label = String(value || "")
61
+ .trim()
62
+ .toLowerCase();
63
+ const pattern = new RegExp(
64
+ `^${WINDOWS_EC2_JIT.labelPrefix}[a-z0-9][a-z0-9-]{0,31}$`,
65
+ );
66
+ if (!pattern.test(label)) {
67
+ throw new Error(
68
+ `runner label must match ${WINDOWS_EC2_JIT.labelPrefix}<bounded-id>`,
69
+ );
70
+ }
71
+ return label;
72
+ }
73
+
74
+ export function windowsJitRunnerLabels(value) {
75
+ return ["self-hosted", "Windows", "X64", windowsJitRunnerLabel(value)];
76
+ }
77
+
78
+ export function renderWindowsJitBootstrap(template, values = {}) {
79
+ const runnerLabel = windowsJitRunnerLabel(values.runnerLabel);
80
+ const jitParameterName = String(values.jitParameterName || "").trim();
81
+ if (
82
+ !jitParameterName.startsWith(WINDOWS_EC2_JIT.jitParameterPrefix) ||
83
+ !/^\/[A-Za-z0-9._/-]+$/.test(jitParameterName)
84
+ ) {
85
+ throw new Error("jitParameterName must use the dedicated Windows prefix");
86
+ }
87
+ const bounded = {
88
+ REGION: String(values.region || WINDOWS_EC2_JIT.region),
89
+ JIT_PARAMETER_NAME: jitParameterName,
90
+ EVIDENCE_BUCKET: String(values.evidenceBucket || ""),
91
+ RUNNER_LABEL: runnerLabel,
92
+ SOURCE_SHA: exactSha(values.sourceSha, "sourceSha"),
93
+ GITHUB_RUN_ID: String(values.githubRunId || ""),
94
+ GITHUB_RUN_ATTEMPT: String(values.githubRunAttempt || ""),
95
+ AMI_ID: String(values.amiId || ""),
96
+ AMI_NAME: String(values.amiName || ""),
97
+ INSTANCE_TYPE: String(values.instanceType || WINDOWS_EC2_JIT.instanceType),
98
+ LAUNCHED_AT: iso(values.launchedAt, "launchedAt"),
99
+ };
100
+ const patterns = {
101
+ REGION: /^us-[a-z]+-\d$/,
102
+ EVIDENCE_BUCKET: /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/,
103
+ GITHUB_RUN_ID: /^\d+$/,
104
+ GITHUB_RUN_ATTEMPT: /^\d+$/,
105
+ AMI_ID: /^ami-[0-9a-f]+$/,
106
+ AMI_NAME: /^[A-Za-z0-9._-]+$/,
107
+ INSTANCE_TYPE: /^[a-z0-9.]+$/,
108
+ };
109
+ for (const [name, pattern] of Object.entries(patterns)) {
110
+ if (!pattern.test(bounded[name])) {
111
+ throw new Error(`${name.toLowerCase()} is invalid`);
112
+ }
113
+ }
114
+ let rendered = String(template || "");
115
+ for (const [name, value] of Object.entries(bounded)) {
116
+ rendered = rendered.replaceAll(`__${name}__`, value);
117
+ }
118
+ const unresolved = rendered.match(/__[A-Z0-9_]+__/g) || [];
119
+ if (unresolved.length) {
120
+ throw new Error(
121
+ `bootstrap has unresolved variables: ${unresolved.join(",")}`,
122
+ );
123
+ }
124
+ if (/encoded_jit_config|github_pat_|ghp_|gho_/.test(rendered)) {
125
+ throw new Error("bootstrap must not contain a GitHub credential");
126
+ }
127
+ return rendered;
128
+ }
129
+
130
+ export function windowsEc2JitPlan(overrides = {}) {
131
+ const config = { ...WINDOWS_EC2_JIT, ...overrides };
132
+ const money = (value) => Math.round(value * 100) / 100;
133
+ const perInstanceMaximumUsd = money(
134
+ (config.pricePerHourUsd * config.maximumInstanceLifetimeMinutes) / 60,
135
+ );
136
+ const maximumCommittedComputeUsd = money(
137
+ perInstanceMaximumUsd * config.maxAcceptedInstances,
138
+ );
139
+ const maximumRaceStopUsd = money(
140
+ perInstanceMaximumUsd * config.maxConcurrentInstances,
141
+ );
142
+ const maximumBoundedSpendUsd = money(
143
+ maximumCommittedComputeUsd + maximumRaceStopUsd,
144
+ );
145
+ if (maximumBoundedSpendUsd >= config.budgetLimitUsd) {
146
+ throw new Error(
147
+ `bounded Windows EC2 spend ${maximumBoundedSpendUsd.toFixed(2)} must remain below budget ${config.budgetLimitUsd.toFixed(2)}`,
148
+ );
149
+ }
150
+ const plan = {
151
+ schemaVersion: 1,
152
+ contract: AWS_WINDOWS_JIT_CONTRACT,
153
+ kind: "phase-plan",
154
+ config,
155
+ costEnvelope: {
156
+ currency: "USD",
157
+ perInstanceMaximumUsd,
158
+ maximumCommittedComputeUsd,
159
+ maximumRaceStopUsd,
160
+ maximumBoundedSpendUsd,
161
+ assumption:
162
+ "The reaper charges every accepted and race instance for the full three-hour fail-closed lifetime.",
163
+ },
164
+ invariants: {
165
+ repositoryScopedJit: true,
166
+ oneJobPerRunner: true,
167
+ noInboundSecurityGroupRules: true,
168
+ imdsv2Required: true,
169
+ encryptedDeleteOnTerminationRoot: true,
170
+ jitConfigurationDeletedAfterRead: true,
171
+ jitConfigurationLabels: [
172
+ "self-hosted",
173
+ "Windows",
174
+ "X64",
175
+ `${config.labelPrefix}<bounded-id>`,
176
+ ],
177
+ staticAwsCredentialsForbidden: true,
178
+ signingAndPublicationCredentialsForbidden: true,
179
+ zeroWarmCapacity: true,
180
+ },
181
+ };
182
+ return { ...plan, digest: digest(plan) };
183
+ }
184
+
185
+ export function createWindowsJitEvidence({
186
+ repository,
187
+ sourceSha,
188
+ sourceRef,
189
+ githubRunId,
190
+ githubRunAttempt,
191
+ githubJob,
192
+ runnerName,
193
+ runnerLabels = [],
194
+ instanceId,
195
+ instanceType,
196
+ amiId,
197
+ amiName,
198
+ availabilityZone,
199
+ launchedAt,
200
+ runnerStartedAt,
201
+ runnerExitedAt,
202
+ terminatedAt,
203
+ cleanupResult,
204
+ cacheMode = "off",
205
+ observedAt = new Date().toISOString(),
206
+ } = {}) {
207
+ if (repository !== WINDOWS_EC2_JIT.repository) {
208
+ throw new Error(`repository must be ${WINDOWS_EC2_JIT.repository}`);
209
+ }
210
+ if (instanceType !== WINDOWS_EC2_JIT.instanceType) {
211
+ throw new Error(`instanceType must be ${WINDOWS_EC2_JIT.instanceType}`);
212
+ }
213
+ if (!/^i-[0-9a-f]+$/.test(String(instanceId || ""))) {
214
+ throw new Error("instanceId must be an EC2 instance id");
215
+ }
216
+ if (!/^ami-[0-9a-f]+$/.test(String(amiId || ""))) {
217
+ throw new Error("amiId must be an EC2 AMI id");
218
+ }
219
+ const labels = [...new Set(runnerLabels.map((label) => String(label).trim()))]
220
+ .filter(Boolean)
221
+ .sort();
222
+ const customLabel = labels.find((label) =>
223
+ label.startsWith(WINDOWS_EC2_JIT.labelPrefix),
224
+ );
225
+ windowsJitRunnerLabel(customLabel);
226
+ for (const required of ["self-hosted", "Windows", "X64"]) {
227
+ if (!labels.includes(required)) {
228
+ throw new Error(`runnerLabels must include ${required}`);
229
+ }
230
+ }
231
+ const evidence = {
232
+ schemaVersion: 1,
233
+ contract: AWS_WINDOWS_JIT_CONTRACT,
234
+ kind: "runner-evidence",
235
+ phase: WINDOWS_EC2_JIT.phase,
236
+ provider: "aws-ec2",
237
+ repository,
238
+ source: {
239
+ sha: exactSha(sourceSha, "sourceSha"),
240
+ ref: String(sourceRef || "").trim(),
241
+ },
242
+ github: {
243
+ runId: String(githubRunId || "").trim(),
244
+ runAttempt: String(githubRunAttempt || "").trim(),
245
+ job: String(githubJob || "").trim(),
246
+ },
247
+ runner: {
248
+ name: String(runnerName || "").trim(),
249
+ labels,
250
+ version: WINDOWS_EC2_JIT.runnerVersion,
251
+ archiveSha256: WINDOWS_EC2_JIT.runnerArchiveSha256,
252
+ cacheMode: String(cacheMode || "off"),
253
+ },
254
+ aws: {
255
+ region: WINDOWS_EC2_JIT.region,
256
+ instanceId: String(instanceId),
257
+ instanceType,
258
+ amiId: String(amiId),
259
+ amiName: String(amiName || "").trim(),
260
+ availabilityZone: String(availabilityZone || "").trim(),
261
+ },
262
+ lifecycle: {
263
+ launchedAt: iso(launchedAt, "launchedAt"),
264
+ runnerStartedAt: iso(runnerStartedAt, "runnerStartedAt"),
265
+ runnerExitedAt: iso(runnerExitedAt, "runnerExitedAt"),
266
+ terminatedAt: terminatedAt ? iso(terminatedAt, "terminatedAt") : null,
267
+ cleanupResult: String(
268
+ cleanupResult || "runner-exit-termination-pending",
269
+ ).trim(),
270
+ },
271
+ observedAt: iso(observedAt, "observedAt"),
272
+ };
273
+ for (const [label, value] of Object.entries({
274
+ sourceRef: evidence.source.ref,
275
+ runId: evidence.github.runId,
276
+ runAttempt: evidence.github.runAttempt,
277
+ job: evidence.github.job,
278
+ runnerName: evidence.runner.name,
279
+ amiName: evidence.aws.amiName,
280
+ availabilityZone: evidence.aws.availabilityZone,
281
+ cleanupResult: evidence.lifecycle.cleanupResult,
282
+ })) {
283
+ if (!value) throw new Error(`${label} is required`);
284
+ }
285
+ return { ...evidence, digest: digest(evidence) };
286
+ }
287
+
288
+ export function verifyWindowsEc2JitQualification({
289
+ jobs = [],
290
+ cancellationCleanup = {},
291
+ timeoutCleanup = {},
292
+ registeredCloudRunners = [],
293
+ activeInstances = [],
294
+ disposableVolumes = [],
295
+ minCapacity = 0,
296
+ desiredCapacity = 0,
297
+ actualIncrementalSpendUsd,
298
+ observedAt = new Date().toISOString(),
299
+ } = {}) {
300
+ const plan = windowsEc2JitPlan();
301
+ const issues = [];
302
+ const accepted = jobs.filter(
303
+ (job) =>
304
+ job?.trusted === true &&
305
+ job?.exactSource === true &&
306
+ job?.status === "succeeded" &&
307
+ job?.oneJobJit === true,
308
+ );
309
+ const smokeJobs = accepted.filter((job) => job.kind === "smoke");
310
+ const fullJobs = accepted.filter((job) => job.kind === "full");
311
+ if (smokeJobs.length < plan.config.minimumSmokeJobs) {
312
+ issues.push("runner-profile-smoke-missing");
313
+ }
314
+ if (fullJobs.length < plan.config.minimumFullJobs) {
315
+ issues.push("insufficient-trusted-full-windows-jobs");
316
+ }
317
+ for (const [kind, cleanup] of Object.entries({
318
+ cancellation: cancellationCleanup,
319
+ timeout: timeoutCleanup,
320
+ })) {
321
+ const latency = finiteNumber(
322
+ cleanup?.cleanupLatencySeconds,
323
+ `${kind}Cleanup.cleanupLatencySeconds`,
324
+ );
325
+ if (
326
+ cleanup?.status !== "passed" ||
327
+ cleanup?.instanceTerminated !== true ||
328
+ cleanup?.runnerRemoved !== true ||
329
+ latency > plan.config.maximumCleanupLatencySeconds
330
+ ) {
331
+ issues.push(`${kind}-cleanup-not-proven`);
332
+ }
333
+ }
334
+ const spend = finiteNumber(
335
+ actualIncrementalSpendUsd,
336
+ "actualIncrementalSpendUsd",
337
+ );
338
+ if (spend >= plan.config.budgetLimitUsd) {
339
+ issues.push("actual-spend-not-below-budget");
340
+ }
341
+ if (Number(minCapacity) !== 0 || Number(desiredCapacity) !== 0) {
342
+ issues.push("provider-capacity-not-zero");
343
+ }
344
+ if (registeredCloudRunners.length) issues.push("registered-runners-remain");
345
+ if (activeInstances.length) issues.push("active-instances-remain");
346
+ if (disposableVolumes.length) issues.push("disposable-volumes-remain");
347
+ const result = {
348
+ schemaVersion: 1,
349
+ contract: AWS_WINDOWS_JIT_CONTRACT,
350
+ kind: "phase-verification",
351
+ phase: plan.config.phase,
352
+ status: issues.length ? "failed" : "passed",
353
+ qualifying: issues.length === 0,
354
+ metrics: {
355
+ acceptedJobs: accepted.length,
356
+ smokeJobs: smokeJobs.length,
357
+ fullJobs: fullJobs.length,
358
+ actualIncrementalSpendUsd: spend,
359
+ registeredCloudRunners: registeredCloudRunners.length,
360
+ activeInstances: activeInstances.length,
361
+ disposableVolumes: disposableVolumes.length,
362
+ minCapacity: Number(minCapacity),
363
+ desiredCapacity: Number(desiredCapacity),
364
+ },
365
+ cleanup: {
366
+ cancellation: cancellationCleanup,
367
+ timeout: timeoutCleanup,
368
+ },
369
+ issues,
370
+ planDigest: plan.digest,
371
+ observedAt: iso(observedAt, "observedAt"),
372
+ };
373
+ return { ...result, digest: digest(result) };
374
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import {
6
+ createWindowsJitEvidence,
7
+ renderWindowsJitBootstrap,
8
+ verifyWindowsEc2JitQualification,
9
+ windowsEc2JitPlan,
10
+ } from "./aws-windows-jit-core.mjs";
11
+
12
+ function arg(name, fallback = "") {
13
+ const index = process.argv.indexOf(`--${name}`);
14
+ return index === -1 ? fallback : process.argv[index + 1] || "";
15
+ }
16
+
17
+ function readJson(file) {
18
+ return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
19
+ }
20
+
21
+ function writeJson(file, value) {
22
+ const resolved = path.resolve(file);
23
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
24
+ fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`);
25
+ return resolved;
26
+ }
27
+
28
+ export function main() {
29
+ const mode = process.argv[2] || "plan";
30
+ if (mode === "plan") {
31
+ const result = windowsEc2JitPlan();
32
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
33
+ return result;
34
+ }
35
+ if (mode === "evidence") {
36
+ const result = createWindowsJitEvidence({
37
+ repository: process.env.GITHUB_REPOSITORY,
38
+ sourceSha:
39
+ process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || process.env.GITHUB_SHA,
40
+ sourceRef:
41
+ process.env.BUILDCHAIN_EXPECTED_SOURCE_REF || process.env.GITHUB_REF,
42
+ githubRunId: process.env.GITHUB_RUN_ID,
43
+ githubRunAttempt: process.env.GITHUB_RUN_ATTEMPT,
44
+ githubJob: process.env.GITHUB_JOB,
45
+ runnerName: process.env.RUNNER_NAME,
46
+ runnerLabels: JSON.parse(process.env.BUILDCHAIN_RUNNER_LABELS_JSON || "[]"),
47
+ instanceId: process.env.AWS_EC2_INSTANCE_ID,
48
+ instanceType: process.env.AWS_EC2_INSTANCE_TYPE,
49
+ amiId: process.env.AWS_EC2_AMI_ID,
50
+ amiName: process.env.AWS_EC2_AMI_NAME,
51
+ availabilityZone: process.env.AWS_EC2_AVAILABILITY_ZONE,
52
+ launchedAt: process.env.AWS_EC2_LAUNCHED_AT,
53
+ runnerStartedAt: process.env.AWS_EC2_RUNNER_STARTED_AT,
54
+ runnerExitedAt:
55
+ process.env.AWS_EC2_RUNNER_EXITED_AT || new Date().toISOString(),
56
+ terminatedAt: process.env.AWS_EC2_TERMINATED_AT,
57
+ cleanupResult:
58
+ process.env.AWS_EC2_CLEANUP_RESULT || "runner-exit-termination-pending",
59
+ cacheMode: process.env.BUILDCHAIN_CHECKOUT_CACHE_MODE || "off",
60
+ });
61
+ const output = writeJson(
62
+ process.env.BUILDCHAIN_WINDOWS_JIT_EVIDENCE_PATH ||
63
+ ".buildchain/aws-windows-jit.json",
64
+ result,
65
+ );
66
+ process.stdout.write(`${output}\n`);
67
+ return result;
68
+ }
69
+ if (mode === "render-bootstrap") {
70
+ const template = arg(
71
+ "template",
72
+ "infra/aws-us-elastic-runner-burst-plane/windows-jit-bootstrap.ps1",
73
+ );
74
+ const output = arg("output");
75
+ if (!output) throw new Error("render-bootstrap requires --output");
76
+ const result = renderWindowsJitBootstrap(
77
+ fs.readFileSync(path.resolve(template), "utf8"),
78
+ {
79
+ region: arg("region", "us-east-1"),
80
+ jitParameterName: arg("jit-parameter"),
81
+ evidenceBucket: arg("evidence-bucket"),
82
+ runnerLabel: arg("runner-label"),
83
+ sourceSha: arg("source-sha"),
84
+ githubRunId: arg("github-run-id"),
85
+ githubRunAttempt: arg("github-run-attempt", "1"),
86
+ amiId: arg("ami-id"),
87
+ amiName: arg("ami-name"),
88
+ instanceType: arg("instance-type", "c7i.4xlarge"),
89
+ launchedAt: arg("launched-at"),
90
+ },
91
+ );
92
+ const resolved = path.resolve(output);
93
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
94
+ fs.writeFileSync(resolved, result);
95
+ process.stdout.write(`${resolved}\n`);
96
+ return resolved;
97
+ }
98
+ if (mode === "verify") {
99
+ const input = arg("input");
100
+ if (!input) throw new Error("verify requires --input");
101
+ const result = verifyWindowsEc2JitQualification(readJson(input));
102
+ const output = arg("output");
103
+ if (output) writeJson(output, result);
104
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
105
+ if (!result.qualifying) process.exitCode = 1;
106
+ return result;
107
+ }
108
+ throw new Error(`unsupported aws-windows-jit mode: ${mode}`);
109
+ }
110
+
111
+ if (
112
+ process.argv[1] &&
113
+ import.meta.url === pathToFileURL(process.argv[1]).href
114
+ ) {
115
+ try {
116
+ main();
117
+ } catch (error) {
118
+ console.error(`::error::${error.message || error}`);
119
+ process.exitCode = 1;
120
+ }
121
+ }
@@ -1,6 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { macosJitRunnerLabel } from "./aws-macos-jit-core.mjs";
5
+ import { windowsJitRunnerLabel } from "./aws-windows-jit-core.mjs";
4
6
 
5
7
  export const RUNNER_PRESETS = Object.freeze({
6
8
  "github-hosted": [
@@ -61,6 +63,36 @@ export const RUNNER_PRESETS = Object.freeze({
61
63
  capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
62
64
  },
63
65
  ],
66
+ "aws-us-codebuild-linux": [
67
+ {
68
+ id: "linux-x64",
69
+ name: "Linux x64 (AWS CodeBuild burst)",
70
+ platform: "linux",
71
+ provider: "aws-codebuild",
72
+ runner: '["aws-codebuild-dynamic"]',
73
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
74
+ },
75
+ ],
76
+ "aws-us-ec2-windows-jit": [
77
+ {
78
+ id: "windows-x64",
79
+ name: "Windows x64 (AWS EC2 one-job JIT)",
80
+ platform: "windows",
81
+ provider: "aws-ec2-windows-jit",
82
+ runner: '["self-hosted","Windows","X64","aws-ec2-jit-dynamic"]',
83
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
84
+ },
85
+ ],
86
+ "aws-us-ec2-macos-jit": [
87
+ {
88
+ id: "macos-arm64",
89
+ name: "macOS ARM64 (AWS EC2 Dedicated Host one-job JIT)",
90
+ platform: "macos",
91
+ provider: "aws-ec2-macos-jit",
92
+ runner: '["self-hosted","macOS","ARM64","aws-ec2-jit-dynamic"]',
93
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
94
+ },
95
+ ],
64
96
  });
65
97
 
66
98
  export const LINUX_CONTAINER_PRESETS = Object.freeze({
@@ -759,6 +791,8 @@ function normalizePlatform(platform, index) {
759
791
  }
760
792
  const normalized = { id, name, runner };
761
793
  if (platform?.platform !== undefined) normalized.platform = String(platform.platform || "").trim();
794
+ if (platform?.provider !== undefined) normalized.provider = String(platform.provider || "").trim();
795
+ if (platform?.project !== undefined) normalized.project = String(platform.project || "").trim();
762
796
  normalized.capabilities = capabilities.sort();
763
797
  if (platform?.required === false) normalized.required = false;
764
798
  return normalized;
@@ -767,6 +801,9 @@ function normalizePlatform(platform, index) {
767
801
  export function resolveRunnerMatrix({
768
802
  runnerPreset = "github-hosted",
769
803
  platformsJson = "",
804
+ awsCodeBuildProject = "",
805
+ awsEc2WindowsRunnerLabel = "",
806
+ awsEc2MacosRunnerLabel = "",
770
807
  linuxContainerPreset = "",
771
808
  linuxContainerImage = "",
772
809
  } = {}) {
@@ -812,18 +849,44 @@ export function resolveRunnerMatrix({
812
849
  if (!platforms) {
813
850
  throw new Error(`unsupported runner-preset: ${preset}`);
814
851
  }
852
+ let resolvedPlatforms = platforms;
853
+ if (preset === "aws-us-codebuild-linux") {
854
+ const project = String(awsCodeBuildProject || "").trim();
855
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{1,149}$/.test(project)) {
856
+ throw new Error(
857
+ "runner-preset=aws-us-codebuild-linux requires a valid aws-codebuild-project",
858
+ );
859
+ }
860
+ resolvedPlatforms = platforms.map((platform) => ({ ...platform, project }));
861
+ }
862
+ if (preset === "aws-us-ec2-windows-jit") {
863
+ const runnerLabel = windowsJitRunnerLabel(awsEc2WindowsRunnerLabel);
864
+ resolvedPlatforms = platforms.map((platform) => ({
865
+ ...platform,
866
+ runnerLabel,
867
+ runner: JSON.stringify(["self-hosted", "Windows", "X64", runnerLabel]),
868
+ }));
869
+ }
870
+ if (preset === "aws-us-ec2-macos-jit") {
871
+ const runnerLabel = macosJitRunnerLabel(awsEc2MacosRunnerLabel);
872
+ resolvedPlatforms = platforms.map((platform) => ({
873
+ ...platform,
874
+ runnerLabel,
875
+ runner: JSON.stringify(["self-hosted", "macOS", "ARM64", runnerLabel]),
876
+ }));
877
+ }
815
878
  const containerPlatforms = linuxContainer.enabled
816
- ? platforms.filter(platformIsLinux)
879
+ ? resolvedPlatforms.filter(platformIsLinux)
817
880
  : [];
818
881
  const nativePlatforms = linuxContainer.enabled
819
- ? platforms.filter((platform) => !platformIsLinux(platform))
820
- : platforms;
882
+ ? resolvedPlatforms.filter((platform) => !platformIsLinux(platform))
883
+ : resolvedPlatforms;
821
884
  return {
822
885
  source: "runner-preset",
823
886
  runnerPreset: preset,
824
- platforms,
825
- platformsJson: JSON.stringify(platforms),
826
- platformCount: platforms.length,
887
+ platforms: resolvedPlatforms,
888
+ platformsJson: JSON.stringify(resolvedPlatforms),
889
+ platformCount: resolvedPlatforms.length,
827
890
  nativePlatforms,
828
891
  nativePlatformsJson: JSON.stringify(nativePlatforms),
829
892
  nativePlatformCount: nativePlatforms.length,
@@ -149,9 +149,29 @@ function packageVersion(cwd) {
149
149
  return packageJson.version;
150
150
  }
151
151
 
152
+ function sourceSha(cwd) {
153
+ const result = spawnSync("git", ["rev-parse", "HEAD"], {
154
+ cwd,
155
+ encoding: "utf8",
156
+ });
157
+ const value = String(result.stdout || "").trim();
158
+ if (result.status !== 0 || !/^[0-9a-f]{40}$/i.test(value)) {
159
+ throw new Error("standalone binary requires an exact Buildchain source SHA");
160
+ }
161
+ return value;
162
+ }
163
+
152
164
  function bundleCli({ cwd, tempDir, version, logger }) {
153
165
  const outDir = path.join(tempDir, "bundle");
154
166
  const configPath = path.join(tempDir, "tsup.config.mjs");
167
+ const embeddedContractWorld = fs.readFileSync(
168
+ path.join(cwd, "dist", "site", "buildchain-contract.json"),
169
+ "utf8",
170
+ );
171
+ const embeddedLicenseText = fs.readFileSync(
172
+ path.join(cwd, "LICENSE"),
173
+ "utf8",
174
+ );
155
175
  fs.writeFileSync(configPath, `export default {
156
176
  entry: {
157
177
  buildchain: ${JSON.stringify(path.join(cwd, "bin", "buildchain.mjs"))},
@@ -169,6 +189,9 @@ function bundleCli({ cwd, tempDir, version, logger }) {
169
189
  noExternal: ["smol-toml", /^@kungfu-tech\\/kfd(?:\\/|$)/],
170
190
  define: {
171
191
  "process.env.BUILDCHAIN_EMBEDDED_PACKAGE_VERSION": ${JSON.stringify(JSON.stringify(version || packageVersion(cwd)))},
192
+ "process.env.BUILDCHAIN_EMBEDDED_SOURCE_SHA": ${JSON.stringify(JSON.stringify(sourceSha(cwd)))},
193
+ "process.env.BUILDCHAIN_EMBEDDED_CONTRACT_WORLD": ${JSON.stringify(JSON.stringify(embeddedContractWorld))},
194
+ "process.env.BUILDCHAIN_EMBEDDED_LICENSE_TEXT": ${JSON.stringify(JSON.stringify(embeddedLicenseText))},
172
195
  "process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT": ${JSON.stringify(JSON.stringify("1"))},
173
196
  },
174
197
  };
@@ -131,7 +131,7 @@ export function writeBuildchainContractLock({
131
131
  output = env("BUILDCHAIN_CONTRACT_LOCK_PATH", BUILDCHAIN_CONTRACT_LOCK_PATH),
132
132
  currentContractPath = env("BUILDCHAIN_CONTRACT_CURRENT_PATH", "dist/site/buildchain-contract.json"),
133
133
  runtimeRoot = env("BUILDCHAIN_RUNTIME_ROOT", process.cwd()),
134
- buildchainRef = env("BUILDCHAIN_RUNTIME_REF", "v2"),
134
+ buildchainRef = env("BUILDCHAIN_RUNTIME_REF", "v3"),
135
135
  resolvedSha = env("BUILDCHAIN_RUNTIME_SHA"),
136
136
  compatibilityPolicy = env("BUILDCHAIN_CONTRACT_COMPATIBILITY_POLICY", "major-compatible"),
137
137
  acceptedAt = env("BUILDCHAIN_CONTRACT_ACCEPTED_AT") || new Date().toISOString(),
@@ -49,7 +49,7 @@ function normalizeRepository(value) {
49
49
  function normalizeTargetBranch(value) {
50
50
  const branch = String(value || process.env.GITHUB_REF_NAME || DEFAULT_TARGET_BRANCH).replace(/^refs\/heads\//, "");
51
51
  if (!/^dev\/v\d+\/v\d+\.\d+$/.test(branch)) {
52
- throw new Error(`target-branch must be a semver dev branch such as dev/v2/v2.N, got: ${branch || "<empty>"}`);
52
+ throw new Error(`target-branch must be a semver dev branch such as dev/v3/v3.N, got: ${branch || "<empty>"}`);
53
53
  }
54
54
  return branch;
55
55
  }