@kungfu-tech/buildchain 3.0.5-alpha.7 → 3.0.5

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 (62) hide show
  1. package/README.md +0 -31
  2. package/actions/macos-credential-island/README.md +0 -8
  3. package/bin/buildchain.mjs +1 -1
  4. package/contracts/auditable-demo-media-profiles-v1.json +0 -61
  5. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
  6. package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
  7. package/dist/site/buildchain-contract.json +25 -86
  8. package/dist/site/buildchain-site.json +38 -64
  9. package/dist/site/capability-registry.json +1 -1
  10. package/dist/site/cli-registry.json +2 -6
  11. package/dist/site/controller-registry.json +3 -11
  12. package/dist/site/kfd-claims.json +11 -53
  13. package/dist/site/kfd-upstream-aggregate.json +1 -1
  14. package/dist/site/manual-registry.json +7 -7
  15. package/dist/site/node-api-registry.json +31 -213
  16. package/dist/site/page-registry.json +20 -50
  17. package/dist/site/public-surface-audit.json +8 -98
  18. package/dist/site/publication-authority-registry.json +1 -42
  19. package/dist/site/publication-registry.json +4 -4
  20. package/dist/site/site-manifest.json +11 -11
  21. package/dist/site/workflow-registry.json +2 -67
  22. package/docs/auditable-demo.md +7 -74
  23. package/docs/aws-us-elastic-runner-burst-plane.md +16 -151
  24. package/docs/cli-reference.md +2 -24
  25. package/docs/node-api-reference.md +72 -90
  26. package/docs/publish-transaction.md +5 -7
  27. package/docs/release-flow.md +0 -4
  28. package/docs/release-governance.md +0 -39
  29. package/docs/reusable-build-surface.md +0 -9
  30. package/package.json +1 -1
  31. package/packages/core/buildchain-contract.js +1 -36
  32. package/packages/core/buildchain-publication-authority.js +0 -2
  33. package/packages/core/public-surface-audit.js +35 -4
  34. package/packages/core/release-propagation-release.js +18 -61
  35. package/packages/core/release-propagation-work.js +3 -7
  36. package/packages/core/release-propagation.js +2 -12
  37. package/scripts/auditable-demo-renditions.mjs +2 -7
  38. package/scripts/auditable-demo.mjs +21 -27
  39. package/scripts/aws-windows-jit-controller-core.mjs +2 -20
  40. package/scripts/aws-windows-jit-controller.mjs +109 -184
  41. package/scripts/aws-windows-jit-core.mjs +3 -21
  42. package/scripts/aws-windows-jit.mjs +0 -2
  43. package/scripts/build-standalone-binary.mjs +0 -4
  44. package/scripts/buildchain-cli-help.mjs +1 -2
  45. package/scripts/capture-package-release-propagation.mjs +1 -24
  46. package/scripts/check-inventory.mjs +0 -5
  47. package/scripts/finalize-native-artifact-signing-result.mjs +15 -37
  48. package/scripts/generate-site-bundle.mjs +0 -2
  49. package/scripts/locked-source-checkout.mjs +3 -17
  50. package/scripts/release-propagation.mjs +2 -77
  51. package/contracts/auditable-demo-scenario-v1.schema.json +0 -153
  52. package/packages/core/release-propagation-pickup.js +0 -386
  53. package/packages/core/workflow-call-contract.js +0 -308
  54. package/packages/core/workflow-yaml-contract.js +0 -272
  55. package/scripts/auditable-demo-platform.mjs +0 -506
  56. package/scripts/aws-macos-jit-controller-core.mjs +0 -389
  57. package/scripts/aws-macos-jit-controller-runtime.mjs +0 -82
  58. package/scripts/aws-macos-jit-controller.mjs +0 -558
  59. package/scripts/aws-macos-jit-job-controller.mjs +0 -401
  60. package/scripts/aws-windows-jit-campaign-core.mjs +0 -296
  61. package/scripts/aws-windows-jit-campaign.mjs +0 -202
  62. package/scripts/workflow-call-contract.mjs +0 -133
@@ -1,386 +0,0 @@
1
- import {
2
- assertExactFields,
3
- assertPlainObject,
4
- assertString,
5
- normalizeChannel,
6
- optionalString,
7
- } from "./release-propagation-common.js";
8
- import { normalizeExecutionProfile } from "./release-propagation-execution-profile.js";
9
- import { assertCommitSha } from "./release-propagation-work-control.js";
10
- import {
11
- createReleasePropagationWork,
12
- planReleasePropagation,
13
- } from "./release-propagation.js";
14
- import { normalizeUpstreamRelease } from "./release-propagation-release.js";
15
-
16
- export const MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT =
17
- "kungfu-buildchain-manual-upstream-pickup";
18
- export const MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT =
19
- "kungfu-buildchain-manual-upstream-pickup-plan";
20
-
21
- function assertRepository(value, label) {
22
- const repository = assertString(value, label);
23
- if (!/^[^/\s]+\/[^/\s]+$/.test(repository)) {
24
- throw new Error(`${label} must be owner/repo`);
25
- }
26
- return repository;
27
- }
28
-
29
- function normalizeStringList(values, label) {
30
- if (!Array.isArray(values) || values.length === 0) {
31
- throw new Error(`${label} must be a non-empty array`);
32
- }
33
- const normalized = values.map((value, index) =>
34
- assertString(value, `${label}[${index}]`),
35
- );
36
- const canonical = [...new Set(normalized)].sort();
37
- if (JSON.stringify(normalized) !== JSON.stringify(canonical)) {
38
- throw new Error(`${label} must be sorted and unique`);
39
- }
40
- return canonical;
41
- }
42
-
43
- function normalizeRuntime(value) {
44
- const runtime = assertExactFields(
45
- value,
46
- ["package", "version"],
47
- "manual pickup runtime",
48
- );
49
- return {
50
- package: assertString(runtime.package, "manual pickup runtime.package"),
51
- version: assertString(runtime.version, "manual pickup runtime.version"),
52
- };
53
- }
54
-
55
- function normalizeDownstream(value) {
56
- const downstream = assertExactFields(
57
- value,
58
- ["id", "repository", "baseRef", "executionProfile"],
59
- "manual pickup downstream",
60
- );
61
- return {
62
- id: assertString(downstream.id, "manual pickup downstream.id"),
63
- repository: assertRepository(
64
- downstream.repository,
65
- "manual pickup downstream.repository",
66
- ),
67
- baseRef: assertString(
68
- downstream.baseRef,
69
- "manual pickup downstream.baseRef",
70
- ),
71
- executionProfile: normalizeExecutionProfile(
72
- downstream.executionProfile,
73
- "manual pickup downstream.executionProfile",
74
- ),
75
- };
76
- }
77
-
78
- function normalizeSource(value, index) {
79
- const label = `manual pickup sources[${index}]`;
80
- const source = assertExactFields(
81
- value,
82
- [
83
- "id",
84
- "repository",
85
- "package",
86
- "lockPath",
87
- "distTags",
88
- "workflowPaths",
89
- "workflowRefs",
90
- ],
91
- label,
92
- );
93
- const distTags = assertExactFields(
94
- source.distTags,
95
- ["alpha", "release"],
96
- `${label}.distTags`,
97
- );
98
- return {
99
- id: assertString(source.id, `${label}.id`),
100
- repository: assertRepository(source.repository, `${label}.repository`),
101
- package: assertString(source.package, `${label}.package`),
102
- lockPath: assertString(source.lockPath, `${label}.lockPath`),
103
- distTags: {
104
- alpha: assertString(distTags.alpha, `${label}.distTags.alpha`),
105
- release: assertString(distTags.release, `${label}.distTags.release`),
106
- },
107
- workflowPaths: normalizeStringList(
108
- source.workflowPaths,
109
- `${label}.workflowPaths`,
110
- ),
111
- workflowRefs: normalizeStringList(
112
- source.workflowRefs,
113
- `${label}.workflowRefs`,
114
- ),
115
- };
116
- }
117
-
118
- export function normalizeManualUpstreamPickupConfig(input) {
119
- const config = assertExactFields(
120
- input,
121
- ["schemaVersion", "contract", "runtime", "downstream", "sources"],
122
- "manual upstream pickup config",
123
- );
124
- if (
125
- config.schemaVersion !== 1 ||
126
- config.contract !== MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT
127
- ) {
128
- throw new Error(
129
- `manual upstream pickup config must use ${MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT} v1`,
130
- );
131
- }
132
- if (!Array.isArray(config.sources) || config.sources.length === 0) {
133
- throw new Error(
134
- "manual upstream pickup config sources must be a non-empty array",
135
- );
136
- }
137
- const sources = config.sources.map(normalizeSource);
138
- const ids = sources.map((source) => source.id);
139
- if (new Set(ids).size !== ids.length)
140
- throw new Error("manual pickup source ids must be unique");
141
- return {
142
- schemaVersion: 1,
143
- contract: MANUAL_UPSTREAM_PICKUP_CONFIG_CONTRACT,
144
- runtime: normalizeRuntime(config.runtime),
145
- downstream: normalizeDownstream(config.downstream),
146
- sources,
147
- };
148
- }
149
-
150
- function decodePayload(attestation, label) {
151
- const encoded = attestation?.bundle?.dsseEnvelope?.payload;
152
- if (!encoded) throw new Error(`${label} is missing a DSSE payload`);
153
- try {
154
- return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
155
- } catch {
156
- throw new Error(`${label} DSSE payload is not valid JSON`);
157
- }
158
- }
159
-
160
- function repositoryFromUrl(value) {
161
- return String(value || "")
162
- .replace(/^git\+/, "")
163
- .replace(/^https:\/\/github\.com\//, "")
164
- .replace(/\.git$/, "");
165
- }
166
-
167
- export function resolveNpmRegistryRelease({
168
- source: sourceInput,
169
- channel,
170
- packageMetadata,
171
- attestations,
172
- }) {
173
- const source = normalizeSource(sourceInput, 0);
174
- const normalizedChannel = normalizeChannel(channel, "manual pickup channel");
175
- const metadata = assertPlainObject(packageMetadata, "npm package metadata");
176
- if (metadata.name !== source.package)
177
- throw new Error("npm package name disagrees with manual pickup source");
178
- if (
179
- repositoryFromUrl(metadata.repository?.url || metadata.repository) !==
180
- source.repository
181
- ) {
182
- throw new Error(
183
- "npm package repository disagrees with manual pickup source",
184
- );
185
- }
186
- const version = assertString(
187
- metadata.version,
188
- "npm package metadata.version",
189
- );
190
- const integrity = assertString(
191
- metadata.dist?.integrity,
192
- "npm package metadata.dist.integrity",
193
- );
194
- const attestationUrl = assertString(
195
- metadata.dist?.attestations?.url,
196
- "npm package metadata.dist.attestations.url",
197
- );
198
- const response = assertPlainObject(attestations, "npm attestations response");
199
- const slsa = (response.attestations || []).find(
200
- (entry) => entry.predicateType === "https://slsa.dev/provenance/v1",
201
- );
202
- if (!slsa) throw new Error("npm package does not expose SLSA provenance v1");
203
- const statement = decodePayload(slsa, "npm SLSA provenance");
204
- if (statement.predicateType !== "https://slsa.dev/provenance/v1") {
205
- throw new Error(
206
- "npm SLSA statement predicate type disagrees with its envelope",
207
- );
208
- }
209
- const expectedSubject = `pkg:npm/${encodeURIComponent(source.package)}@${version}`;
210
- const subject = (statement.subject || []).find(
211
- (entry) => entry.name === expectedSubject,
212
- );
213
- if (!subject?.digest?.sha512)
214
- throw new Error("npm SLSA provenance is missing the exact package subject");
215
- const workflow =
216
- statement.predicate?.buildDefinition?.externalParameters?.workflow || {};
217
- const workflowRepository = repositoryFromUrl(workflow.repository);
218
- if (workflowRepository !== source.repository) {
219
- throw new Error(
220
- "npm SLSA workflow repository disagrees with manual pickup source",
221
- );
222
- }
223
- if (!source.workflowPaths.includes(workflow.path)) {
224
- throw new Error(
225
- "npm SLSA workflow path is not admitted by the manual pickup source",
226
- );
227
- }
228
- if (!source.workflowRefs.includes(workflow.ref)) {
229
- throw new Error(
230
- "npm SLSA workflow ref is not admitted by the manual pickup source",
231
- );
232
- }
233
- const dependency = (
234
- statement.predicate?.buildDefinition?.resolvedDependencies || []
235
- ).find(
236
- (entry) =>
237
- repositoryFromUrl(String(entry.uri || "").split("@")[0]) ===
238
- source.repository,
239
- );
240
- const sourceSha = assertCommitSha(
241
- dependency?.digest?.gitCommit,
242
- "npm SLSA source commit",
243
- );
244
- const runUrl = assertString(
245
- statement.predicate?.runDetails?.metadata?.invocationId,
246
- "npm SLSA invocation URL",
247
- );
248
- return normalizeUpstreamRelease({
249
- repository: source.repository,
250
- channel: normalizedChannel,
251
- tag: "",
252
- tagTargetSha: "",
253
- sourceSha,
254
- package: {
255
- name: source.package,
256
- version,
257
- integrity,
258
- gitHead: optionalString(metadata.gitHead),
259
- },
260
- registryProvenance: {
261
- registry: "https://registry.npmjs.org",
262
- attestationUrl,
263
- predicateType: slsa.predicateType,
264
- subjectSha512: subject.digest.sha512,
265
- repository: source.repository,
266
- sourceSha,
267
- workflowPath: workflow.path,
268
- workflowRef: workflow.ref,
269
- runUrl,
270
- },
271
- });
272
- }
273
-
274
- function pickupGraph(config, source) {
275
- return {
276
- schemaVersion: 1,
277
- contract: "kungfu-buildchain-release-propagation-graph",
278
- nodes: [
279
- { id: source.id, repository: source.repository, package: source.package },
280
- {
281
- id: config.downstream.id,
282
- repository: config.downstream.repository,
283
- baseRef: config.downstream.baseRef,
284
- lockPath: source.lockPath,
285
- executionProfile: config.downstream.executionProfile,
286
- },
287
- ],
288
- edges: [
289
- {
290
- id: `${source.id}-manual-pickup`,
291
- from: source.id,
292
- to: config.downstream.id,
293
- channels: ["alpha", "release"],
294
- channelPolicy: "preserve",
295
- lockPath: source.lockPath,
296
- prBaseRef: config.downstream.baseRef,
297
- },
298
- ],
299
- };
300
- }
301
-
302
- export function createManualUpstreamPickupPlan({
303
- config: configInput,
304
- sourceId,
305
- channel,
306
- currentVersion,
307
- upstreamRelease,
308
- }) {
309
- const config = normalizeManualUpstreamPickupConfig(configInput);
310
- const source = config.sources.find((entry) => entry.id === sourceId);
311
- if (!source)
312
- throw new Error(`manual pickup source is not configured: ${sourceId}`);
313
- const release = normalizeUpstreamRelease(upstreamRelease);
314
- const normalizedChannel = normalizeChannel(channel, "manual pickup channel");
315
- if (
316
- release.repository !== source.repository ||
317
- release.package?.name !== source.package
318
- ) {
319
- throw new Error("manual pickup release disagrees with the selected source");
320
- }
321
- if (release.channel !== normalizedChannel)
322
- throw new Error("manual pickup release channel disagrees with the request");
323
- const installedVersion = assertString(
324
- currentVersion,
325
- "manual pickup currentVersion",
326
- );
327
- const status =
328
- installedVersion === release.package.version
329
- ? "current"
330
- : "update-available";
331
- const propagationPlan =
332
- status === "current"
333
- ? null
334
- : planReleasePropagation({
335
- graph: pickupGraph(config, source),
336
- upstreamRelease: release,
337
- sourceNode: source.id,
338
- });
339
- return {
340
- schemaVersion: 1,
341
- contract: MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT,
342
- invocation: "downstream-manual",
343
- automaticTrigger: false,
344
- runtime: config.runtime,
345
- source: {
346
- id: source.id,
347
- repository: source.repository,
348
- package: source.package,
349
- channel: normalizedChannel,
350
- distTag: source.distTags[normalizedChannel],
351
- },
352
- currentVersion: installedVersion,
353
- resolvedVersion: release.package.version,
354
- status,
355
- upstreamRelease: release,
356
- propagationPlan,
357
- };
358
- }
359
-
360
- export function createManualUpstreamPickupCapture({
361
- plan,
362
- expectedDownstreamBaseSha,
363
- }) {
364
- const pickup = assertPlainObject(plan, "manual pickup plan");
365
- if (
366
- pickup.contract !== MANUAL_UPSTREAM_PICKUP_PLAN_CONTRACT ||
367
- pickup.schemaVersion !== 1
368
- ) {
369
- throw new Error("manual pickup plan must use the v1 contract");
370
- }
371
- if (pickup.status === "current") {
372
- return { plan: pickup, work: null, nextAction: "none" };
373
- }
374
- if (pickup.status !== "update-available" || !pickup.propagationPlan) {
375
- throw new Error("manual pickup plan has an invalid status");
376
- }
377
- const expectedBaseSha = assertCommitSha(
378
- expectedDownstreamBaseSha,
379
- "expectedDownstreamBaseSha",
380
- );
381
- const work = createReleasePropagationWork({
382
- plan: pickup.propagationPlan,
383
- expectedDownstreamBaseSha: expectedBaseSha,
384
- });
385
- return { plan: pickup, work, nextAction: "claim" };
386
- }
@@ -1,308 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { parseWorkflowDocument } from "./workflow-yaml-contract.js";
3
-
4
- export const WORKFLOW_CALL_CONTRACT =
5
- "kungfu-buildchain-workflow-call-contract/v1";
6
- export const WORKFLOW_CALL_RECEIPT =
7
- "kungfu-buildchain-workflow-call-receipt/v1";
8
-
9
- function stableJson(value) {
10
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
11
- if (value && typeof value === "object") {
12
- return `{${Object.keys(value)
13
- .sort()
14
- .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
15
- .join(",")}}`;
16
- }
17
- return JSON.stringify(value);
18
- }
19
-
20
- function sha256(value) {
21
- return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
22
- }
23
-
24
- function exactSha(value) {
25
- return /^[0-9a-f]{40}$/.test(String(value || "").toLowerCase());
26
- }
27
-
28
- function coordinate(uses) {
29
- const match = String(uses || "").match(
30
- /^([^/]+\/[^/]+)\/(\.github\/workflows\/[^@]+)@([^@]+)$/,
31
- );
32
- return match ? { repository: match[1], path: match[2], ref: match[3] } : null;
33
- }
34
-
35
- function permissionLevel(value) {
36
- if (value === "write" || value === "write-all") return 2;
37
- if (value === "read" || value === "read-all") return 1;
38
- if (value === "none" || value === "{}") return 0;
39
- return -1;
40
- }
41
-
42
- function callerPermission(permissions, name) {
43
- return permissions[name] || permissions["*"] || "none";
44
- }
45
-
46
- function validatePermissions(caller, callee) {
47
- const failures = [];
48
- for (const [name, required] of Object.entries(callee)) {
49
- if (name === "*") continue;
50
- const available = callerPermission(caller, name);
51
- if (
52
- permissionLevel(required) < 0 ||
53
- permissionLevel(available) < permissionLevel(required)
54
- ) {
55
- failures.push({
56
- code: "permission-drift",
57
- message: `caller permission ${name}=${available} does not satisfy callee ${required}`,
58
- });
59
- }
60
- }
61
- return failures;
62
- }
63
-
64
- function validateInputs(job, reusable) {
65
- const failures = [];
66
- const declared = new Map(reusable.inputs.map((entry) => [entry.name, entry]));
67
- for (const [name, supplied] of Object.entries(job.with)) {
68
- const input = declared.get(name);
69
- if (!input) {
70
- failures.push({
71
- code: "undeclared-input",
72
- message: `caller supplies undeclared input: ${name}`,
73
- });
74
- continue;
75
- }
76
- if (supplied.kind !== "expression" && supplied.kind !== input.type) {
77
- failures.push({
78
- code: "input-type-drift",
79
- message: `caller input ${name} is ${supplied.kind}, callee requires ${input.type}`,
80
- });
81
- }
82
- }
83
- for (const input of reusable.inputs.filter((entry) => entry.required)) {
84
- if (!(input.name in job.with)) {
85
- failures.push({
86
- code: "missing-required-input",
87
- message: `caller omits required input: ${input.name}`,
88
- });
89
- }
90
- }
91
- return failures;
92
- }
93
-
94
- function validateSecrets(job, reusable) {
95
- if (job.secrets === "inherit") {
96
- return [
97
- {
98
- code: "secret-inheritance-not-exact",
99
- message:
100
- "secrets: inherit cannot prove an exact reusable-workflow secret contract",
101
- },
102
- ];
103
- }
104
- const failures = [];
105
- const declared = new Map(
106
- reusable.secrets.map((entry) => [entry.name, entry]),
107
- );
108
- for (const name of Object.keys(job.secrets)) {
109
- if (!declared.has(name)) {
110
- failures.push({
111
- code: "undeclared-secret",
112
- message: `caller supplies undeclared secret: ${name}`,
113
- });
114
- }
115
- }
116
- for (const secret of reusable.secrets.filter((entry) => entry.required)) {
117
- if (!(secret.name in job.secrets)) {
118
- failures.push({
119
- code: "missing-required-secret",
120
- message: `caller omits required secret: ${secret.name}`,
121
- });
122
- }
123
- }
124
- return failures;
125
- }
126
-
127
- function validateEvents(actual, trusted) {
128
- const trustedSet = new Set(trusted);
129
- return actual
130
- .filter((event) => !trustedSet.has(event))
131
- .map((event) => ({
132
- code: "untrusted-event-drift",
133
- message: `caller admits untrusted event class: ${event}`,
134
- }));
135
- }
136
-
137
- function interfaceProjection(reusable) {
138
- return {
139
- inputs: reusable.inputs,
140
- secrets: reusable.secrets,
141
- outputs: reusable.outputs,
142
- permissions: reusable.permissions,
143
- };
144
- }
145
-
146
- function validateCoordinate(job, { repository, workflowPath, sha }) {
147
- const failures = [];
148
- const pinned = coordinate(job?.uses);
149
- if (!pinned) {
150
- return [
151
- {
152
- code: "callee-coordinate-invalid",
153
- message:
154
- "caller uses must name owner/repo/.github/workflows/file.yml@ref",
155
- },
156
- ];
157
- }
158
- if (!exactSha(pinned.ref)) {
159
- failures.push({
160
- code: "floating-callee-ref",
161
- message: `callee ref is not an exact commit SHA: ${pinned.ref}`,
162
- });
163
- }
164
- if (pinned.repository !== repository || pinned.path !== workflowPath) {
165
- failures.push({
166
- code: "callee-coordinate-drift",
167
- message: `caller uses ${pinned.repository}/${pinned.path}, expected ${repository}/${workflowPath}`,
168
- });
169
- }
170
- if (pinned.ref.toLowerCase() !== String(sha || "").toLowerCase()) {
171
- failures.push({
172
- code: "stale-pinned-ref",
173
- message: `caller pin ${pinned.ref} does not match checked callee ${sha}`,
174
- });
175
- }
176
- return failures;
177
- }
178
-
179
- export function evaluateWorkflowCallContract({
180
- callerText,
181
- calleeText,
182
- callerRepository,
183
- callerWorkflowPath,
184
- callerSha,
185
- callerTree,
186
- callerSourceState = "clean",
187
- calleeRepository,
188
- calleeWorkflowPath,
189
- calleeSha,
190
- jobId,
191
- trustedEventClasses = [],
192
- expectedContractRoot = "",
193
- } = {}) {
194
- const caller = parseWorkflowDocument(callerText);
195
- const callee = parseWorkflowDocument(calleeText);
196
- const job = caller.callJobs.find((entry) => entry.id === jobId);
197
- const failures = [];
198
- if (!job)
199
- failures.push({
200
- code: "call-job-missing",
201
- message: `reusable-workflow call job is missing: ${jobId}`,
202
- });
203
- if (!callee.interface.reusable)
204
- failures.push({
205
- code: "callee-not-reusable",
206
- message: "callee does not declare on.workflow_call",
207
- });
208
-
209
- failures.push(
210
- ...validateCoordinate(job, {
211
- repository: calleeRepository,
212
- workflowPath: calleeWorkflowPath,
213
- sha: calleeSha,
214
- }),
215
- );
216
- if (!exactSha(calleeSha))
217
- failures.push({
218
- code: "callee-sha-invalid",
219
- message: "callee SHA must be an exact 40-character commit",
220
- });
221
- if (!exactSha(callerSha) || !exactSha(callerTree))
222
- failures.push({
223
- code: "caller-source-invalid",
224
- message: "caller SHA and tree must be exact 40-character Git object ids",
225
- });
226
-
227
- if (job && callee.interface.reusable) {
228
- failures.push(...validateInputs(job, callee.interface));
229
- failures.push(...validateSecrets(job, callee.interface));
230
- failures.push(
231
- ...validatePermissions(job.permissions, callee.interface.permissions),
232
- );
233
- }
234
- failures.push(...validateEvents(caller.triggers, trustedEventClasses));
235
-
236
- const contract = {
237
- schema: WORKFLOW_CALL_CONTRACT,
238
- caller: {
239
- repository: callerRepository,
240
- workflowPath: callerWorkflowPath,
241
- workflowDigest: sha256(String(callerText || "")),
242
- jobId,
243
- eventClasses: caller.triggers,
244
- uses: job?.uses || "",
245
- inputs: job?.with || {},
246
- secretNames:
247
- job?.secrets === "inherit"
248
- ? ["*"]
249
- : Object.keys(job?.secrets || {}).sort(),
250
- permissions: job?.permissions || {},
251
- },
252
- callee: {
253
- repository: calleeRepository,
254
- workflowPath: calleeWorkflowPath,
255
- sha: String(calleeSha || "").toLowerCase(),
256
- workflowDigest: sha256(String(calleeText || "")),
257
- interface: interfaceProjection(callee.interface),
258
- },
259
- trustedEventClasses: [...new Set(trustedEventClasses)].sort(),
260
- };
261
- const contractRoot = sha256(stableJson(contract));
262
- if (expectedContractRoot && expectedContractRoot !== contractRoot) {
263
- failures.push({
264
- code: "contract-root-mismatch",
265
- message: `workflow call contract root ${contractRoot} does not match accepted ${expectedContractRoot}`,
266
- });
267
- }
268
- const receipt = {
269
- schema: WORKFLOW_CALL_RECEIPT,
270
- contractRoot,
271
- caller: {
272
- repository: callerRepository,
273
- sha: String(callerSha || "").toLowerCase(),
274
- tree: String(callerTree || "").toLowerCase(),
275
- sourceState: callerSourceState,
276
- workflowPath: callerWorkflowPath,
277
- workflowDigest: contract.caller.workflowDigest,
278
- },
279
- callee: {
280
- repository: calleeRepository,
281
- sha: String(calleeSha || "").toLowerCase(),
282
- workflowPath: calleeWorkflowPath,
283
- workflowDigest: contract.callee.workflowDigest,
284
- },
285
- eventClasses: caller.triggers,
286
- };
287
- return {
288
- schema: "kungfu-buildchain-workflow-call-check/v1",
289
- ok: failures.length === 0,
290
- status: failures.length === 0 ? "passed" : "failed",
291
- failures,
292
- contract,
293
- contractRoot,
294
- receipt,
295
- receiptRoot: sha256(stableJson(receipt)),
296
- receiptReusable: failures.length === 0 && callerSourceState === "clean",
297
- };
298
- }
299
-
300
- export function assertWorkflowCallContract(options) {
301
- const report = evaluateWorkflowCallContract(options);
302
- if (!report.ok) {
303
- throw new Error(
304
- `Reusable workflow call contract failed:\n${report.failures.map((entry) => `${entry.code}: ${entry.message}`).join("\n")}`,
305
- );
306
- }
307
- return report;
308
- }