@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0

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 (83) hide show
  1. package/README.md +31 -0
  2. package/actions/macos-credential-island/README.md +8 -0
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +61 -0
  5. package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
  6. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  8. package/dist/site/buildchain-contract.json +107 -26
  9. package/dist/site/buildchain-site.json +121 -49
  10. package/dist/site/capability-registry.json +3 -3
  11. package/dist/site/cli-registry.json +9 -3
  12. package/dist/site/controller-registry.json +27 -3
  13. package/dist/site/kfd-claims.json +101 -15
  14. package/dist/site/kfd-upstream-aggregate.json +1 -1
  15. package/dist/site/manual-registry.json +10 -9
  16. package/dist/site/node-api-registry.json +540 -31
  17. package/dist/site/page-registry.json +105 -29
  18. package/dist/site/public-surface-audit.json +184 -11
  19. package/dist/site/publication-authority-registry.json +62 -1
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/site-manifest.json +13 -13
  22. package/dist/site/workflow-registry.json +118 -4
  23. package/docs/MAP.md +4 -1
  24. package/docs/auditable-demo.md +90 -7
  25. package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
  26. package/docs/cli-reference.md +35 -2
  27. package/docs/dev-alpha-candidate-patrol.md +14 -5
  28. package/docs/dev-qualification-patrol.md +108 -0
  29. package/docs/node-api-reference.md +123 -72
  30. package/docs/publish-transaction.md +7 -5
  31. package/docs/release-flow.md +4 -0
  32. package/docs/release-governance.md +39 -0
  33. package/docs/release-propagation.md +93 -0
  34. package/docs/reusable-build-surface.md +53 -12
  35. package/docs/stable-candidate-patrol.md +5 -2
  36. package/package.json +1 -1
  37. package/packages/core/buildchain-contract.js +36 -1
  38. package/packages/core/buildchain-publication-authority.js +3 -0
  39. package/packages/core/public-surface-audit.js +4 -35
  40. package/packages/core/release-propagation-agent-entry.js +185 -0
  41. package/packages/core/release-propagation-pickup.js +387 -0
  42. package/packages/core/release-propagation-push.js +293 -0
  43. package/packages/core/release-propagation-release.js +61 -18
  44. package/packages/core/release-propagation-stage-evidence.js +36 -0
  45. package/packages/core/release-propagation-work.js +8 -4
  46. package/packages/core/release-propagation.js +27 -2
  47. package/packages/core/workflow-call-contract.js +308 -0
  48. package/packages/core/workflow-yaml-contract.js +272 -0
  49. package/scripts/artifact-signing-controller-core.mjs +525 -0
  50. package/scripts/artifact-signing-controller.mjs +302 -0
  51. package/scripts/artifact-signing-delegation.mjs +51 -3
  52. package/scripts/auditable-demo-platform.mjs +564 -0
  53. package/scripts/auditable-demo-renditions.mjs +7 -2
  54. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  55. package/scripts/auditable-demo.mjs +27 -21
  56. package/scripts/aws-macos-jit-controller-core.mjs +389 -0
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
  58. package/scripts/aws-macos-jit-controller.mjs +558 -0
  59. package/scripts/aws-macos-jit-job-controller.mjs +401 -0
  60. package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
  61. package/scripts/aws-windows-jit-campaign.mjs +202 -0
  62. package/scripts/aws-windows-jit-controller-core.mjs +20 -2
  63. package/scripts/aws-windows-jit-controller.mjs +184 -109
  64. package/scripts/aws-windows-jit-core.mjs +21 -3
  65. package/scripts/aws-windows-jit.mjs +2 -0
  66. package/scripts/build-standalone-binary.mjs +6 -0
  67. package/scripts/buildchain-cli-help.mjs +2 -1
  68. package/scripts/capture-package-release-propagation.mjs +24 -1
  69. package/scripts/check-inventory.mjs +6 -0
  70. package/scripts/compiler-cache-evidence.mjs +90 -7
  71. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  72. package/scripts/dev-pr-auto-merge.mjs +4 -4
  73. package/scripts/dev-qualification-patrol.mjs +594 -0
  74. package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
  75. package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
  76. package/scripts/generate-site-bundle.mjs +5 -0
  77. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  78. package/scripts/locked-source-checkout.mjs +17 -3
  79. package/scripts/release-propagation.mjs +150 -2
  80. package/scripts/run-lifecycle-core.mjs +25 -0
  81. package/scripts/site-capability-metadata.mjs +1 -1
  82. package/scripts/stable-candidate-patrol.mjs +30 -4
  83. package/scripts/workflow-call-contract.mjs +133 -0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import {
6
7
  claimReleasePropagationWork,
@@ -15,6 +16,15 @@ import {
15
16
  resumeReleasePropagationWork,
16
17
  verifyReleasePropagationWork,
17
18
  writeReleasePropagationLock,
19
+ createManualUpstreamPickupCapture,
20
+ createManualUpstreamPickupPlan,
21
+ createReleasePropagationPushPlan,
22
+ executeReleasePropagationPush,
23
+ inspectReleasePropagationPushState,
24
+ normalizeManualUpstreamPickupConfig,
25
+ planSiteUpstreamAgentEntry,
26
+ RELEASE_PROPAGATION_FAILURE_MATRIX,
27
+ resolveNpmRegistryRelease,
18
28
  } from "../packages/core/release-propagation.js";
19
29
 
20
30
  function usage() {
@@ -61,6 +71,28 @@ function usage() {
61
71
  --completion-decision <json-or-path>
62
72
  --expected-work-root <sha256:...>
63
73
  [--output <file>] [--json]
74
+ buildchain release-propagation work push-plan --work <json-or-path>
75
+ --expected-work-root <sha256:...>
76
+ [--cwd <dir>] [--remote <name>]
77
+ [--output <file>] [--json]
78
+ buildchain release-propagation work push-branch --work <json-or-path>
79
+ --expected-work-root <sha256:...>
80
+ [--cwd <dir>] [--remote <name>]
81
+ --execute [--output <file>] [--json]
82
+ buildchain release-propagation entry plan --source-id <paper|kfd|buildchain|kungfu-core>
83
+ [--channel <alpha|release>]
84
+ [--handoff-work <json-or-path>]
85
+ [--output <file>] [--json]
86
+ buildchain release-propagation entry fault-matrix [--json]
87
+ buildchain release-propagation pickup plan --config <json-or-path>
88
+ --source-id <id> --channel <alpha|release>
89
+ --current-version <version>
90
+ [--output <file>] [--json]
91
+ buildchain release-propagation pickup create --config <json-or-path>
92
+ --source-id <id> --channel <alpha|release>
93
+ --current-version <version>
94
+ --expected-downstream-base-sha <git-sha>
95
+ [--output <file>] [--json]
64
96
  `;
65
97
  }
66
98
 
@@ -168,10 +200,115 @@ function runWorkCli(args) {
168
200
  emitWorkResult(workArgs, result, `release propagation work complete: ${result.contentRoot}`);
169
201
  return;
170
202
  }
203
+ if (mode === "push-plan" || mode === "push-branch") {
204
+ const work = readJsonFlag(workArgs, "work");
205
+ const cwd = readFlag(workArgs, "cwd", process.cwd());
206
+ const remote = readFlag(workArgs, "remote", "origin");
207
+ let result;
208
+ if (mode === "push-plan") {
209
+ const repositoryState = inspectReleasePropagationPushState({ work, cwd, remote });
210
+ result = createReleasePropagationPushPlan({
211
+ work,
212
+ expectedWorkRoot: readFlag(workArgs, "expected-work-root"),
213
+ repositoryState,
214
+ });
215
+ } else {
216
+ if (!hasFlag(workArgs, "execute")) throw new Error("work push-branch requires --execute");
217
+ result = executeReleasePropagationPush({
218
+ work,
219
+ expectedWorkRoot: readFlag(workArgs, "expected-work-root"),
220
+ cwd,
221
+ remote,
222
+ });
223
+ }
224
+ emitWorkResult(workArgs, result, mode === "push-plan"
225
+ ? `release propagation push plan: ${result.planRoot}`
226
+ : `release propagation push result: ${result.resultRoot}`);
227
+ return;
228
+ }
171
229
  throw new Error(`unsupported release-propagation work command: ${mode}`);
172
230
  }
173
231
 
174
- export function runReleasePropagationCli(argv = process.argv.slice(2)) {
232
+ function runEntryCli(args) {
233
+ const [mode = "", ...entryArgs] = args;
234
+ if (!mode || mode === "--help" || mode === "-h") {
235
+ process.stdout.write(usage());
236
+ return;
237
+ }
238
+ let result;
239
+ if (mode === "fault-matrix") {
240
+ result = RELEASE_PROPAGATION_FAILURE_MATRIX;
241
+ } else if (mode === "plan") {
242
+ result = planSiteUpstreamAgentEntry({
243
+ sourceId: readFlag(entryArgs, "source-id"),
244
+ channel: readFlag(entryArgs, "channel"),
245
+ handoffWork: entryArgs.includes("--handoff-work")
246
+ ? readJsonFlag(entryArgs, "handoff-work")
247
+ : null,
248
+ });
249
+ } else {
250
+ throw new Error(`unsupported release-propagation entry command: ${mode}`);
251
+ }
252
+ writeOutput(readFlag(entryArgs, "output", ""), result);
253
+ if (hasFlag(entryArgs, "json")) printJson(result);
254
+ else process.stdout.write(`${result.contract}\n`);
255
+ }
256
+
257
+ async function resolvePickupPlan(args) {
258
+ const config = normalizeManualUpstreamPickupConfig(readJsonFlag(args, "config"));
259
+ const sourceId = readFlag(args, "source-id");
260
+ const channel = readFlag(args, "channel");
261
+ const source = config.sources.find((entry) => entry.id === sourceId);
262
+ if (!source) throw new Error(`manual pickup source is not configured: ${sourceId}`);
263
+ const distTag = source.distTags[channel];
264
+ if (!distTag) throw new Error("manual pickup channel must be alpha or release");
265
+ const packageMetadata = args.includes("--package-metadata")
266
+ ? readJsonFlag(args, "package-metadata")
267
+ : JSON.parse(execFileSync("npm", ["view", `${source.package}@${distTag}`, "--json", "--registry=https://registry.npmjs.org/"], {
268
+ encoding: "utf8",
269
+ maxBuffer: 16 * 1024 * 1024,
270
+ }));
271
+ const attestationUrl = packageMetadata.dist?.attestations?.url;
272
+ if (!attestationUrl) throw new Error("published npm package does not expose an attestation URL");
273
+ let attestations;
274
+ if (args.includes("--attestations")) {
275
+ attestations = readJsonFlag(args, "attestations");
276
+ } else {
277
+ const response = await fetch(attestationUrl, { headers: { accept: "application/json" } });
278
+ if (!response.ok) throw new Error(`npm attestation lookup failed: HTTP ${response.status}`);
279
+ attestations = await response.json();
280
+ }
281
+ const upstreamRelease = resolveNpmRegistryRelease({ source, channel, packageMetadata, attestations });
282
+ return createManualUpstreamPickupPlan({
283
+ config,
284
+ sourceId,
285
+ channel,
286
+ currentVersion: readFlag(args, "current-version"),
287
+ upstreamRelease,
288
+ });
289
+ }
290
+
291
+ async function runPickupCli(args) {
292
+ const [mode = "", ...pickupArgs] = args;
293
+ if (!mode || mode === "--help" || mode === "-h") {
294
+ process.stdout.write(usage());
295
+ return;
296
+ }
297
+ if (mode !== "plan" && mode !== "create") throw new Error(`unsupported release-propagation pickup command: ${mode}`);
298
+ const plan = await resolvePickupPlan(pickupArgs);
299
+ const result = mode === "create"
300
+ ? createManualUpstreamPickupCapture({
301
+ plan,
302
+ expectedDownstreamBaseSha: readFlag(pickupArgs, "expected-downstream-base-sha"),
303
+ })
304
+ : plan;
305
+ writeOutput(readFlag(pickupArgs, "output", ""), result);
306
+ if (hasFlag(pickupArgs, "json")) printJson(result);
307
+ else if (mode === "plan") process.stdout.write(`manual upstream pickup: ${plan.source.id} ${plan.currentVersion} -> ${plan.resolvedVersion} (${plan.status})\n`);
308
+ else process.stdout.write(`manual upstream pickup next action: ${result.nextAction}\n`);
309
+ }
310
+
311
+ export async function runReleasePropagationCli(argv = process.argv.slice(2)) {
175
312
  const [mode = "", ...args] = argv;
176
313
  if (!mode || mode === "--help" || mode === "-h") {
177
314
  process.stdout.write(usage());
@@ -206,6 +343,14 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
206
343
  runWorkCli(args);
207
344
  return;
208
345
  }
346
+ if (mode === "entry") {
347
+ runEntryCli(args);
348
+ return;
349
+ }
350
+ if (mode === "pickup") {
351
+ await runPickupCli(args);
352
+ return;
353
+ }
209
354
  if (mode === "write-lock") {
210
355
  const plan = readReleasePropagationJson(readFlag(args, "plan"), {
211
356
  label: "--plan",
@@ -259,5 +404,8 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
259
404
  }
260
405
 
261
406
  if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
262
- runReleasePropagationCli();
407
+ runReleasePropagationCli().catch((error) => {
408
+ console.error(`buildchain release-propagation: ${error.message}`);
409
+ process.exitCode = 1;
410
+ });
263
411
  }
@@ -28,6 +28,7 @@ import {
28
28
  parseExpectedArtifactsJson,
29
29
  validateExpectedArtifacts,
30
30
  } from "./build-contract-core.mjs";
31
+ import { verifyCompilerCacheActivity } from "./compiler-cache-evidence.mjs";
31
32
 
32
33
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
33
34
  const buildchainCliCandidates = [
@@ -391,6 +392,22 @@ function writeDiagnosticsSidecarManifest(filePath, {
391
392
  return manifest;
392
393
  }
393
394
 
395
+ export function verifyBuildLifecycleCompilerCacheActivity({
396
+ stageName = "",
397
+ executed = false,
398
+ cwd = process.cwd(),
399
+ env = process.env,
400
+ verifier = verifyCompilerCacheActivity,
401
+ frameworkLog,
402
+ } = {}) {
403
+ if (stageName !== "build" || !executed) return undefined;
404
+ const activity = verifier({ cwd, env });
405
+ if (activity) {
406
+ frameworkLog?.info("compiler-cache.activity", { attributes: activity });
407
+ }
408
+ return activity;
409
+ }
410
+
394
411
  export function runLifecycle({
395
412
  cwd = process.cwd(),
396
413
  stageName = "",
@@ -646,6 +663,13 @@ export function runLifecycle({
646
663
  throw new Error(`required lifecycle stage did not run: ${stageName || "command"}`);
647
664
  }
648
665
 
666
+ const compilerCacheActivity = verifyBuildLifecycleCompilerCacheActivity({
667
+ stageName,
668
+ executed,
669
+ cwd: resolvedCwd,
670
+ frameworkLog,
671
+ });
672
+
649
673
  const shouldReadProcessSummary = Boolean(
650
674
  resolvedProcessSummaryPath
651
675
  && (fs.existsSync(resolvedProcessSummaryPath) || processSummaryRequired),
@@ -739,6 +763,7 @@ export function runLifecycle({
739
763
  fileCount: summary.fileCount,
740
764
  });
741
765
  observability.lifecycle = lifecycleObservability;
766
+ Object.assign(observability, { compilerCacheActivity });
742
767
  observability.diagnostics = {
743
768
  contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
744
769
  path: relativeDiagnosticsPath,
@@ -120,7 +120,7 @@ export function cliCommandMeta(id) {
120
120
  "release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
121
121
  "release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
122
122
  "release-governance": { group: "governance-versioning", purpose: "Reconcile a managed branch's Buildchain aggregate check to an already-tested pull request SHA." },
123
- "release-propagation": { group: "site-and-propagation", purpose: "Plan channel-preserving downstream release PRs and write exact upstream release locks." },
123
+ "release-propagation": { group: "site-and-propagation", purpose: "Route exact Paper, KFD, Buildchain, or Core Site updates through one resumable propagation Work." },
124
124
  "release-transaction": { group: "release-passport-trust", purpose: "Inspect, recover, finalize, or abort durable release transactions." },
125
125
  sample: { group: "observability-diagnostics", purpose: "Inspect sampler command families." },
126
126
  "sample-process-tree": { group: "observability-diagnostics", purpose: "Sample process-tree diagnostics for a wrapped command." },
@@ -23,6 +23,20 @@ function bool(value, fallback = false) {
23
23
  return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
24
24
  }
25
25
 
26
+ function autoMergeMethod(value, fallback = "merge") {
27
+ const normalized = text(value || fallback).toLowerCase();
28
+ if (!["merge", "squash", "rebase"].includes(normalized)) {
29
+ throw new Error(
30
+ `mergeMethod must be merge, squash, or rebase, got ${value || "<empty>"}`,
31
+ );
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ function shouldEnableAutoMerge(options, pullRequest) {
37
+ return options.autoMerge && !pullRequest.auto_merge;
38
+ }
39
+
26
40
  function list(value) {
27
41
  return [...new Set(String(value || "").split(/[\n,]+/).map((entry) => entry.trim()).filter(Boolean))];
28
42
  }
@@ -67,6 +81,7 @@ export function normalizeStableCandidatePatrolOptions(options = {}) {
67
81
  releaseNow: text(options.releaseNow ?? process.env.BUILDCHAIN_STABLE_PATROL_RELEASE_NOW).replace(/^v/, ""),
68
82
  autoPromote: bool(options.autoPromote ?? process.env.BUILDCHAIN_STABLE_PATROL_AUTO_PROMOTE, false),
69
83
  autoMerge: bool(options.autoMerge ?? process.env.BUILDCHAIN_STABLE_PATROL_AUTO_MERGE, false),
84
+ mergeMethod: autoMergeMethod(options.mergeMethod ?? process.env.BUILDCHAIN_STABLE_PATROL_MERGE_METHOD),
70
85
  dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_STABLE_PATROL_DRY_RUN, true),
71
86
  now: text(options.now ?? process.env.BUILDCHAIN_STABLE_PATROL_NOW) || new Date().toISOString(),
72
87
  outputPath: text(options.outputPath ?? process.env.BUILDCHAIN_STABLE_PATROL_OUTPUT_PATH) || ".buildchain/patrol/stable-candidate.json",
@@ -239,7 +254,9 @@ export async function runStableCandidatePatrol(optionsInput = {}, clientInput) {
239
254
  "The source-lock branch freezes the exact candidate; newer alpha publications do not alter this PR.",
240
255
  ].join("\n"),
241
256
  });
242
- if (options.autoMerge) await client.enableAutoMerge(pullRequest);
257
+ if (shouldEnableAutoMerge(options, pullRequest)) {
258
+ await client.enableAutoMerge(pullRequest, options.mergeMethod);
259
+ }
243
260
  promotion.pullRequest = pullRequest;
244
261
  const storedCandidate = ledger.candidates.find((entry) => entry.version === selection.candidate.version);
245
262
  storedCandidate.promotionRequest = {
@@ -434,9 +451,18 @@ export function createGitHubStableCandidateClient({ repository: repositoryInput,
434
451
  const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);
435
452
  return open[0] || api(`/repos/${owner}/${repo}/pulls`, { method: "POST", body: { head, base, title, body } });
436
453
  },
437
- async enableAutoMerge(pullRequest) {
438
- const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:MERGE}){pullRequest{url}}}`;
439
- return api("/graphql", { method: "POST", body: { query, variables: { id: pullRequest.node_id } } });
454
+ async enableAutoMerge(pullRequest, mergeMethod) {
455
+ const query = `mutation($id:ID!,$mergeMethod:PullRequestMergeMethod!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:$mergeMethod}){pullRequest{url}}}`;
456
+ return api("/graphql", {
457
+ method: "POST",
458
+ body: {
459
+ query,
460
+ variables: {
461
+ id: pullRequest.node_id,
462
+ mergeMethod: autoMergeMethod(mergeMethod).toUpperCase(),
463
+ },
464
+ },
465
+ });
440
466
  },
441
467
  async setVariable(name, value) {
442
468
  const current = await api(`/repos/${owner}/${repo}/actions/variables/${encodeURIComponent(name)}`, { allow404: true });
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { pathToFileURL } from "node:url";
6
+ import { evaluateWorkflowCallContract } from "../packages/core/workflow-call-contract.js";
7
+
8
+ function usage() {
9
+ return `usage: node scripts/workflow-call-contract.mjs check \\
10
+ --caller-workflow <path> --job <id> --caller-repository <owner/repo> \\
11
+ --callee-root <checkout> --callee-workflow <path> --callee-repository <owner/repo> \\
12
+ --trusted-event <event[:type]> [--trusted-event ...] \\
13
+ [--expected-contract-root sha256:...] [--allow-dirty] [--output <path>]`;
14
+ }
15
+
16
+ function parseArgs(argv) {
17
+ const options = { trustedEvents: [] };
18
+ for (let index = 0; index < argv.length; index += 1) {
19
+ const arg = argv[index];
20
+ if (arg === "--allow-dirty") {
21
+ options.allowDirty = true;
22
+ continue;
23
+ }
24
+ if (!arg.startsWith("--") || !argv[index + 1])
25
+ throw new Error(`invalid argument: ${arg}`);
26
+ const value = argv[index + 1];
27
+ index += 1;
28
+ if (arg === "--trusted-event") options.trustedEvents.push(value);
29
+ else
30
+ options[
31
+ arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())
32
+ ] = value;
33
+ }
34
+ return options;
35
+ }
36
+
37
+ function git(root, ...args) {
38
+ return execFileSync("git", ["-C", root, ...args], {
39
+ encoding: "utf8",
40
+ }).trim();
41
+ }
42
+
43
+ function required(options, names) {
44
+ const missing = names.filter((name) => !options[name]);
45
+ if (missing.length)
46
+ throw new Error(`missing required options: ${missing.join(", ")}`);
47
+ }
48
+
49
+ export function checkWorkflowCall(options) {
50
+ const callerRoot = path.resolve(options.callerRoot || process.cwd());
51
+ const calleeRoot = path.resolve(options.calleeRoot);
52
+ const callerStatus = git(
53
+ callerRoot,
54
+ "status",
55
+ "--porcelain",
56
+ "--untracked-files=no",
57
+ );
58
+ if (callerStatus && !options.allowDirty) {
59
+ throw new Error(
60
+ "caller checkout is dirty; use --allow-dirty only for local diagnostic validation",
61
+ );
62
+ }
63
+ const calleeStatus = git(
64
+ calleeRoot,
65
+ "status",
66
+ "--porcelain",
67
+ "--untracked-files=no",
68
+ );
69
+ if (calleeStatus) {
70
+ throw new Error("callee checkout is dirty; exact pinned-ref bytes are required");
71
+ }
72
+ const callerSha = git(callerRoot, "rev-parse", "HEAD");
73
+ const callerTree = git(callerRoot, "rev-parse", "HEAD^{tree}");
74
+ const calleeSha = git(calleeRoot, "rev-parse", "HEAD");
75
+ const report = evaluateWorkflowCallContract({
76
+ callerText: fs.readFileSync(
77
+ path.join(callerRoot, options.callerWorkflow),
78
+ "utf8",
79
+ ),
80
+ calleeText: fs.readFileSync(
81
+ path.join(calleeRoot, options.calleeWorkflow),
82
+ "utf8",
83
+ ),
84
+ callerRepository: options.callerRepository,
85
+ callerWorkflowPath: options.callerWorkflow,
86
+ callerSha,
87
+ callerTree,
88
+ callerSourceState: callerStatus ? "diagnostic-dirty" : "clean",
89
+ calleeRepository: options.calleeRepository,
90
+ calleeWorkflowPath: options.calleeWorkflow,
91
+ calleeSha,
92
+ jobId: options.job,
93
+ trustedEventClasses: options.trustedEvents,
94
+ expectedContractRoot: options.expectedContractRoot || "",
95
+ });
96
+ if (options.output) {
97
+ const output = path.resolve(callerRoot, options.output);
98
+ fs.mkdirSync(path.dirname(output), { recursive: true });
99
+ fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
100
+ }
101
+ return report;
102
+ }
103
+
104
+ function main(argv = process.argv.slice(2)) {
105
+ const command = argv.shift();
106
+ if (command !== "check") throw new Error(usage());
107
+ const options = parseArgs(argv);
108
+ required(options, [
109
+ "callerWorkflow",
110
+ "job",
111
+ "callerRepository",
112
+ "calleeRoot",
113
+ "calleeWorkflow",
114
+ "calleeRepository",
115
+ ]);
116
+ if (!options.trustedEvents.length)
117
+ throw new Error("at least one --trusted-event is required");
118
+ const report = checkWorkflowCall(options);
119
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
120
+ if (!report.ok) process.exitCode = 1;
121
+ }
122
+
123
+ if (
124
+ process.argv[1] &&
125
+ import.meta.url === pathToFileURL(process.argv[1]).href
126
+ ) {
127
+ try {
128
+ main();
129
+ } catch (error) {
130
+ console.error(`workflow call contract: ${error.message}`);
131
+ process.exitCode = 1;
132
+ }
133
+ }