@async/pipeline 0.9.11 → 0.9.13
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.
- package/dist/internal/core/index.d.ts +35 -0
- package/dist/internal/core/index.d.ts.map +1 -1
- package/dist/internal/core/index.js +140 -4
- package/dist/internal/core/index.js.map +1 -1
- package/dist/internal/node/cli.d.ts.map +1 -1
- package/dist/internal/node/cli.js +118 -25
- package/dist/internal/node/cli.js.map +1 -1
- package/dist/internal/node/github.d.ts +146 -1
- package/dist/internal/node/github.d.ts.map +1 -1
- package/dist/internal/node/github.js +1045 -4
- package/dist/internal/node/github.js.map +1 -1
- package/package.json +1 -1
|
@@ -20,8 +20,8 @@ function defineActionRef(id, uses, sha, label) {
|
|
|
20
20
|
ref: `${uses}@${sha} # ${label}`
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
const ASYNC_ACTIONS_SHA = "
|
|
24
|
-
const ASYNC_ACTIONS_LABEL = "v0.1.
|
|
23
|
+
const ASYNC_ACTIONS_SHA = "87e033782ca1f84334d9e3a2543b0db064848fb7";
|
|
24
|
+
const ASYNC_ACTIONS_LABEL = "v0.1.14";
|
|
25
25
|
const ASYNC_RELEASE_COMMAND = "npx --yes github:async/release#3892d94a4890600d26b812052aa58dec98b05bfb";
|
|
26
26
|
const GENERATED_ACTIONS = [
|
|
27
27
|
defineActionRef("async.actions.setup", "async/actions/setup", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
|
|
@@ -37,6 +37,7 @@ const GENERATED_ACTIONS = [
|
|
|
37
37
|
defineActionRef("async.actions.source-impact", "async/actions/source-impact", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
|
|
38
38
|
defineActionRef("async.actions.cache", "async/actions/cache", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
|
|
39
39
|
defineActionRef("async.actions.attest", "async/actions/attest", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
|
|
40
|
+
defineActionRef("async.actions.contract", "async/actions/contract", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
|
|
40
41
|
defineActionRef("actions.checkout", "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd", "v6.0.2"),
|
|
41
42
|
defineActionRef("pnpm.setup", "pnpm/setup", "cf03a9b516e09bc5a90f041fc26fc930c9dc631b", "v1.0.0"),
|
|
42
43
|
defineActionRef("deno.setup", "denoland/setup-deno", "667a34cdef165d8d2b2e98dde39547c9daac7282", "v2.0.4"),
|
|
@@ -59,6 +60,7 @@ const ASYNC_AGENT_EVIDENCE_ACTION = actionRef("async.actions.agent-evidence");
|
|
|
59
60
|
const ASYNC_SOURCE_IMPACT_ACTION = actionRef("async.actions.source-impact");
|
|
60
61
|
const ASYNC_CACHE_ACTION = actionRef("async.actions.cache");
|
|
61
62
|
const ASYNC_ATTEST_ACTION = actionRef("async.actions.attest");
|
|
63
|
+
const ASYNC_CONTRACT_ACTION = actionRef("async.actions.contract");
|
|
62
64
|
const CHECKOUT_ACTION = actionRef("actions.checkout");
|
|
63
65
|
const PNPM_SETUP_ACTION = actionRef("pnpm.setup");
|
|
64
66
|
const DENO_SETUP_ACTION = actionRef("deno.setup");
|
|
@@ -103,6 +105,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
|
|
|
103
105
|
evidence: renderModel.evidence,
|
|
104
106
|
sourceImpact: renderModel.sourceImpact,
|
|
105
107
|
attest: renderModel.attest,
|
|
108
|
+
contract: renderModel.contract,
|
|
106
109
|
bridge: renderModel.bridge,
|
|
107
110
|
pages: renderModel.pages,
|
|
108
111
|
manualDispatchJobs: renderModel.manualDispatchJobs,
|
|
@@ -133,6 +136,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
|
|
|
133
136
|
evidence: renderModel.evidence,
|
|
134
137
|
sourceImpact: renderModel.sourceImpact,
|
|
135
138
|
attest: renderModel.attest,
|
|
139
|
+
contract: renderModel.contract,
|
|
136
140
|
bridge: renderModel.bridge,
|
|
137
141
|
pages: renderModel.pages,
|
|
138
142
|
manualDispatchJobs: renderModel.manualDispatchJobs
|
|
@@ -241,6 +245,962 @@ export function jobsForGitHubEvent(pipeline, context) {
|
|
|
241
245
|
}
|
|
242
246
|
return matches.sort((left, right) => left.id.localeCompare(right.id));
|
|
243
247
|
}
|
|
248
|
+
export async function planGitHubJobs(pipeline, options) {
|
|
249
|
+
const rendered = await renderGitHubWorkflow(pipeline, options);
|
|
250
|
+
const packageInfo = await readPackageInfo(options.cwd);
|
|
251
|
+
const model = buildRenderModel(pipeline, {
|
|
252
|
+
...packageInfo,
|
|
253
|
+
cwd: options.cwd,
|
|
254
|
+
configPath: relativePath(options.cwd, options.configPath),
|
|
255
|
+
workflowPath: rendered.workflowPath
|
|
256
|
+
});
|
|
257
|
+
const event = manifestEventFromOptions(options);
|
|
258
|
+
const network = options.network ?? "mock";
|
|
259
|
+
const candidates = buildManifestCandidates(pipeline, model, rendered, event, network);
|
|
260
|
+
const selected = options.job
|
|
261
|
+
? candidates.filter((candidate) => candidate.manifest.job.id === options.job)
|
|
262
|
+
: candidates.filter((candidate) => candidate.selected);
|
|
263
|
+
if (options.job && selected.length === 0) {
|
|
264
|
+
throw pipelineError("ASYNC_PIPELINE_GITHUB_PLAN_UNKNOWN_JOB", `Unknown generated GitHub job "${options.job}".`);
|
|
265
|
+
}
|
|
266
|
+
const selectedIds = new Set(selected.map((candidate) => candidate.manifest.job.id));
|
|
267
|
+
return {
|
|
268
|
+
version: 1,
|
|
269
|
+
generatedBy: "@async/pipeline",
|
|
270
|
+
workflow: rendered.workflowPath,
|
|
271
|
+
lock: rendered.lockPath,
|
|
272
|
+
event,
|
|
273
|
+
manifests: selected.map((candidate) => candidate.manifest),
|
|
274
|
+
skippedJobs: candidates
|
|
275
|
+
.filter((candidate) => !selectedIds.has(candidate.manifest.job.id))
|
|
276
|
+
.map((candidate) => ({
|
|
277
|
+
id: candidate.manifest.job.id,
|
|
278
|
+
reason: candidate.skipReason || (options.job ? "job_filter" : "event_filter"),
|
|
279
|
+
trigger: candidate.manifest.job.trigger
|
|
280
|
+
}))
|
|
281
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
export async function runGitHubLocalPlan(pipeline, options) {
|
|
285
|
+
const plan = await planGitHubJobs(pipeline, options);
|
|
286
|
+
if (plan.manifests.length === 0) {
|
|
287
|
+
return { status: "skipped", plan, receipts: [] };
|
|
288
|
+
}
|
|
289
|
+
const receipts = [];
|
|
290
|
+
for (const manifest of plan.manifests) {
|
|
291
|
+
receipts.push(await runGitHubLocalManifest(manifest, options.cwd, {
|
|
292
|
+
env: options.env ?? process.env,
|
|
293
|
+
dryRun: options.dryRun ?? false
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
const status = options.dryRun
|
|
297
|
+
? "planned"
|
|
298
|
+
: receipts.some((receipt) => receipt.status === "failed")
|
|
299
|
+
? "failed"
|
|
300
|
+
: "passed";
|
|
301
|
+
return { status, plan, receipts };
|
|
302
|
+
}
|
|
303
|
+
export async function runGitHubLocalManifest(manifest, cwd, options = {}) {
|
|
304
|
+
const env = options.env ?? process.env;
|
|
305
|
+
const dryRun = options.dryRun ?? false;
|
|
306
|
+
const stepReceipts = [];
|
|
307
|
+
const issues = [];
|
|
308
|
+
for (const step of manifest.steps) {
|
|
309
|
+
const stepIssues = validateLocalStep(manifest, step, env);
|
|
310
|
+
issues.push(...stepIssues.map((issue) => `${step.id}: ${issue}`));
|
|
311
|
+
const blockedByNetwork = step.local.networked && manifest.local.network === "deny";
|
|
312
|
+
const status = dryRun ? "planned" : stepIssues.length > 0 ? "failed" : "passed";
|
|
313
|
+
stepReceipts.push({
|
|
314
|
+
id: step.id,
|
|
315
|
+
name: step.name,
|
|
316
|
+
contract: step.local.contract,
|
|
317
|
+
status,
|
|
318
|
+
decision: dryRun
|
|
319
|
+
? "planned"
|
|
320
|
+
: blockedByNetwork
|
|
321
|
+
? "denied"
|
|
322
|
+
: manifest.local.network === "allow" && step.local.networked
|
|
323
|
+
? "allowed"
|
|
324
|
+
: step.local.mode === "shell"
|
|
325
|
+
? "simulated"
|
|
326
|
+
: "mocked",
|
|
327
|
+
issues: stepIssues
|
|
328
|
+
});
|
|
329
|
+
if (stepIssues.length > 0 && !dryRun)
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
const receipt = {
|
|
333
|
+
job: manifest.job.id,
|
|
334
|
+
status: dryRun ? "planned" : issues.length > 0 ? "failed" : "passed",
|
|
335
|
+
dryRun,
|
|
336
|
+
network: manifest.local.network,
|
|
337
|
+
artifacts: manifest.artifacts,
|
|
338
|
+
stepReceipts,
|
|
339
|
+
issues
|
|
340
|
+
};
|
|
341
|
+
if (!dryRun) {
|
|
342
|
+
const jobDir = resolve(cwd, manifest.local.stateDirectory);
|
|
343
|
+
await mkdir(join(jobDir, "steps"), { recursive: true });
|
|
344
|
+
await mkdir(join(jobDir, "outputs"), { recursive: true });
|
|
345
|
+
await mkdir(join(jobDir, "artifacts"), { recursive: true });
|
|
346
|
+
await writeFile(join(jobDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
347
|
+
for (const [index, step] of manifest.steps.entries()) {
|
|
348
|
+
await writeFile(join(jobDir, "steps", `${String(index + 1).padStart(2, "0")}-${step.id}.json`), `${JSON.stringify(step, null, 2)}\n`, "utf8");
|
|
349
|
+
}
|
|
350
|
+
for (const artifact of manifest.artifacts) {
|
|
351
|
+
await mkdir(join(jobDir, "artifacts", safeArtifactPart(artifact.name)), { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
receipt.manifestPath = relativePath(cwd, join(jobDir, "manifest.json"));
|
|
354
|
+
await writeFile(join(jobDir, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`, "utf8");
|
|
355
|
+
}
|
|
356
|
+
return receipt;
|
|
357
|
+
}
|
|
358
|
+
function manifestEventFromOptions(options) {
|
|
359
|
+
const name = options.eventName ?? "workflow_dispatch";
|
|
360
|
+
const selectedJob = options.selectedJob ?? (name === "workflow_dispatch" ? options.job : undefined);
|
|
361
|
+
return {
|
|
362
|
+
name,
|
|
363
|
+
...(options.eventAction ? { action: options.eventAction } : {}),
|
|
364
|
+
ref: options.ref ?? (name === "push" ? "refs/heads/main" : undefined),
|
|
365
|
+
sha: options.sha,
|
|
366
|
+
actor: options.actor,
|
|
367
|
+
schedule: options.schedule,
|
|
368
|
+
selectedJob,
|
|
369
|
+
pullRequest: name === "pull_request" || name === "pull_request_target"
|
|
370
|
+
? {
|
|
371
|
+
number: options.prNumber,
|
|
372
|
+
headRepo: options.headRepo,
|
|
373
|
+
headSha: options.headSha,
|
|
374
|
+
baseRef: options.baseRef
|
|
375
|
+
}
|
|
376
|
+
: undefined
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function eventContextFromManifestEvent(event) {
|
|
380
|
+
return {
|
|
381
|
+
eventName: event.name,
|
|
382
|
+
action: event.action,
|
|
383
|
+
ref: event.ref,
|
|
384
|
+
baseRef: event.pullRequest?.baseRef,
|
|
385
|
+
schedule: event.schedule,
|
|
386
|
+
selectedJob: event.selectedJob
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function buildManifestCandidates(pipeline, model, rendered, event, network) {
|
|
390
|
+
const selectedPipelineJobs = new Set(jobsForGitHubEvent(pipeline, eventContextFromManifestEvent(event)).map((job) => job.id));
|
|
391
|
+
const candidates = [];
|
|
392
|
+
for (const job of model.jobs) {
|
|
393
|
+
const selected = selectedPipelineJobs.has(job.id);
|
|
394
|
+
candidates.push({
|
|
395
|
+
manifest: buildPipelineJobManifest(model, rendered, event, job, network),
|
|
396
|
+
selected,
|
|
397
|
+
skipReason: selected ? "" : skipReasonForJob(event, job.trigger)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
for (const generated of buildGeneratedJobManifests(model, rendered, event, network, selectedPipelineJobs)) {
|
|
401
|
+
candidates.push(generated);
|
|
402
|
+
}
|
|
403
|
+
return candidates.sort((left, right) => left.manifest.job.id.localeCompare(right.manifest.job.id));
|
|
404
|
+
}
|
|
405
|
+
function buildPipelineJobManifest(model, rendered, event, job, network) {
|
|
406
|
+
const lifecyclePlan = resolveLifecycleJobPlan(model, job);
|
|
407
|
+
const permissions = manifestJobPermissions(model, job, lifecyclePlan);
|
|
408
|
+
const runnerMatrix = job.github?.runsOnMatrix;
|
|
409
|
+
const steps = [
|
|
410
|
+
checkoutStep(),
|
|
411
|
+
...setupManifestSteps(model),
|
|
412
|
+
...dependencyInstallManifestSteps(model),
|
|
413
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
414
|
+
...taskCacheManifestSteps(model, { kind: "job", id: job.id }),
|
|
415
|
+
...(lifecyclePlan ? lifecycleManifestSteps(model, job, lifecyclePlan) : [
|
|
416
|
+
runActionManifestStep("run-pipeline-job", "Run pipeline job", `${model.command} github check && ${model.command} run ${shellWord(job.id)}${job.execution ? ` --execution ${shellWord(job.execution)}` : ""}`, scopeTaskRunEnv(job.env, undefined), "run")
|
|
417
|
+
]),
|
|
418
|
+
...attestManifestSteps(model, lifecyclePlan, job.requires?.provenance === true),
|
|
419
|
+
...agentEvidenceManifestSteps(model, job),
|
|
420
|
+
...taskCacheSaveManifestSteps(model, { kind: "job", id: job.id }),
|
|
421
|
+
...(job.github?.pages ? [pagesActionManifestStep("upload-pages-artifact", "Upload Pages artifact", job.github.pages)] : []),
|
|
422
|
+
...evidenceCollectManifestSteps(model)
|
|
423
|
+
];
|
|
424
|
+
return makeJobManifest(model, rendered, event, {
|
|
425
|
+
id: job.id,
|
|
426
|
+
kind: "pipeline",
|
|
427
|
+
target: job.target,
|
|
428
|
+
runsOn: runnerMatrix && runnerMatrix.length > 0 ? "${{ matrix.runner }}" : job.github?.runsOn ?? "ubuntu-latest",
|
|
429
|
+
matrix: runnerMatrix ? runnerMatrix.map((runner, index) => ({ runner: Array.isArray(runner) ? runner : [runner], index })) : undefined,
|
|
430
|
+
permissions,
|
|
431
|
+
environment: job.environment ?? job.github?.environment ?? null,
|
|
432
|
+
concurrency: null,
|
|
433
|
+
if: job.if ?? null,
|
|
434
|
+
trigger: job.trigger,
|
|
435
|
+
steps,
|
|
436
|
+
network
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function buildGeneratedJobManifests(model, rendered, event, network, selectedPipelineJobs) {
|
|
440
|
+
const candidates = [];
|
|
441
|
+
if (model.pages.enabled && model.pages.target) {
|
|
442
|
+
const selected = generatedPagesSelected(model.pages, event);
|
|
443
|
+
candidates.push({
|
|
444
|
+
manifest: buildGeneratedPagesManifest(model, rendered, event, network),
|
|
445
|
+
selected,
|
|
446
|
+
skipReason: selected ? "" : skipReasonForGeneratedJob(event, [model.pages.job])
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (model.packagePreviews.enabled && model.packagePreviews.target) {
|
|
450
|
+
const selected = event.name === "pull_request";
|
|
451
|
+
candidates.push({
|
|
452
|
+
manifest: buildPackagePreviewManifest(model, rendered, event, network),
|
|
453
|
+
selected,
|
|
454
|
+
skipReason: selected ? "" : "event_filter"
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (model.bridge.actionsJob.enabled) {
|
|
458
|
+
const selected = bridgeSelected(model.bridge, event);
|
|
459
|
+
candidates.push({
|
|
460
|
+
manifest: buildBridgeManifest(model, rendered, event, network),
|
|
461
|
+
selected,
|
|
462
|
+
skipReason: selected ? "" : "event_filter"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (model.contract.enabled) {
|
|
466
|
+
const selected = contractSelected(model.contract, event);
|
|
467
|
+
candidates.push({
|
|
468
|
+
manifest: buildContractManifest(model, rendered, event, network),
|
|
469
|
+
selected,
|
|
470
|
+
skipReason: selected ? "" : skipReasonForGeneratedJob(event, [model.contract.job])
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (model.dependabotAutoMerge.enabled) {
|
|
474
|
+
const selected = event.name === "pull_request_target";
|
|
475
|
+
candidates.push({
|
|
476
|
+
manifest: buildDependabotManifest(model, rendered, event, network),
|
|
477
|
+
selected,
|
|
478
|
+
skipReason: selected ? "" : "event_filter"
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
for (const sourceJob of model.sourceImpactJobs) {
|
|
482
|
+
const selected = selectedPipelineJobs.has(sourceJob.job);
|
|
483
|
+
candidates.push({
|
|
484
|
+
manifest: buildSourceImpactPlanManifest(model, rendered, event, sourceJob, network),
|
|
485
|
+
selected,
|
|
486
|
+
skipReason: selected ? "" : "upstream_job_skipped"
|
|
487
|
+
});
|
|
488
|
+
candidates.push({
|
|
489
|
+
manifest: buildSourceImpactMatrixManifest(model, rendered, event, sourceJob, network),
|
|
490
|
+
selected,
|
|
491
|
+
skipReason: selected ? "" : "upstream_job_skipped"
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (model.evidence.enabled) {
|
|
495
|
+
const producerIds = new Set(evidenceProducerJobIds(model));
|
|
496
|
+
const selected = candidates.some((candidate) => candidate.selected && producerIds.has(candidate.manifest.job.id))
|
|
497
|
+
|| [...selectedPipelineJobs].some((jobId) => producerIds.has(jobId));
|
|
498
|
+
candidates.push({
|
|
499
|
+
manifest: buildEvidenceFanInManifest(model, rendered, event, network),
|
|
500
|
+
selected,
|
|
501
|
+
skipReason: selected ? "" : "no_evidence_producers"
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return candidates;
|
|
505
|
+
}
|
|
506
|
+
function makeJobManifest(model, rendered, event, options) {
|
|
507
|
+
const stateDirectory = `.async/github-local/jobs/${safeArtifactPart(options.id)}`;
|
|
508
|
+
return {
|
|
509
|
+
version: 1,
|
|
510
|
+
generatedBy: "@async/pipeline",
|
|
511
|
+
repo: "${{ github.repository }}",
|
|
512
|
+
workflow: rendered.workflowPath,
|
|
513
|
+
lock: rendered.lockPath,
|
|
514
|
+
event,
|
|
515
|
+
job: {
|
|
516
|
+
id: options.id,
|
|
517
|
+
name: options.id,
|
|
518
|
+
kind: options.kind,
|
|
519
|
+
target: options.target,
|
|
520
|
+
runsOn: options.runsOn,
|
|
521
|
+
...(options.matrix ? { matrix: options.matrix } : {}),
|
|
522
|
+
permissions: options.permissions,
|
|
523
|
+
environment: options.environment,
|
|
524
|
+
concurrency: options.concurrency,
|
|
525
|
+
if: options.if,
|
|
526
|
+
trigger: options.trigger
|
|
527
|
+
},
|
|
528
|
+
steps: options.steps,
|
|
529
|
+
trust: {
|
|
530
|
+
actionRefsPinned: ACTION_LOCKS.every((action) => /^[0-9a-f]{40}$/iu.test(action.sha)),
|
|
531
|
+
workflow: rendered.workflowPath,
|
|
532
|
+
lock: rendered.lockPath,
|
|
533
|
+
lifecycleFallbackReason: lifecycleFallbackReason(options.steps)
|
|
534
|
+
},
|
|
535
|
+
artifacts: artifactsForSteps(options.id, options.steps, model.evidence.retentionDays, model.evidence.ifNoFilesFound),
|
|
536
|
+
local: {
|
|
537
|
+
workspace: ".",
|
|
538
|
+
stateDirectory,
|
|
539
|
+
network: options.network,
|
|
540
|
+
permissionsMode: "enforced",
|
|
541
|
+
mocks: [
|
|
542
|
+
"setup",
|
|
543
|
+
"run",
|
|
544
|
+
"pages",
|
|
545
|
+
"preview",
|
|
546
|
+
"publish",
|
|
547
|
+
"storage-bridge",
|
|
548
|
+
"release",
|
|
549
|
+
"contract",
|
|
550
|
+
"hygiene",
|
|
551
|
+
"comment",
|
|
552
|
+
"evidence",
|
|
553
|
+
"agent-evidence",
|
|
554
|
+
"source-impact",
|
|
555
|
+
"cache",
|
|
556
|
+
"attest"
|
|
557
|
+
]
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
function lifecycleFallbackReason(steps) {
|
|
562
|
+
return steps.find((step) => step.local.fallbackReason)?.local.fallbackReason ?? null;
|
|
563
|
+
}
|
|
564
|
+
function manifestJobPermissions(model, job, lifecyclePlan) {
|
|
565
|
+
const grants = job.github?.permissions;
|
|
566
|
+
const idToken = grants?.idToken ?? (job.requires?.provenance || attestRequiresOidc(model, lifecyclePlan) ? "write" : undefined);
|
|
567
|
+
const issues = grants?.issues;
|
|
568
|
+
const packages = grants?.packages;
|
|
569
|
+
const pullRequests = grants?.pullRequests;
|
|
570
|
+
const contents = grants?.contents ?? ((idToken || issues || packages || pullRequests) ? "read" : "read");
|
|
571
|
+
return cleanPermissions({
|
|
572
|
+
contents,
|
|
573
|
+
...(idToken ? { "id-token": idToken } : {}),
|
|
574
|
+
...(issues ? { issues } : {}),
|
|
575
|
+
...(packages ? { packages } : {}),
|
|
576
|
+
...(pullRequests ? { "pull-requests": pullRequests } : {})
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function cleanPermissions(permissions) {
|
|
580
|
+
return Object.fromEntries(Object.entries(permissions).filter((entry) => entry[1] === "read" || entry[1] === "write"));
|
|
581
|
+
}
|
|
582
|
+
function checkoutStep() {
|
|
583
|
+
return actionManifestStep("checkout", "Checkout", CHECKOUT_ACTION, {}, "checkout");
|
|
584
|
+
}
|
|
585
|
+
function setupManifestSteps(model) {
|
|
586
|
+
const runtime = model.runtime.map((entry) => entry.spec);
|
|
587
|
+
if (model.setup === "async") {
|
|
588
|
+
return [
|
|
589
|
+
actionManifestStep("setup-async-runtimes", "Setup Async runtimes", ASYNC_SETUP_ACTION, {
|
|
590
|
+
"node-version": model.nodeVersion,
|
|
591
|
+
"pnpm-version": pnpmSetupVersion(model.packageManager, model.packageManagerVersion),
|
|
592
|
+
"npm-version": "11.16.0",
|
|
593
|
+
runtime: runtime.length > 1 ? runtime.join("\n") : runtime[0] ?? `node@${model.nodeVersion}`,
|
|
594
|
+
"package-manager": model.packageManager,
|
|
595
|
+
install: model.projectKind === "package",
|
|
596
|
+
"frozen-lockfile": true,
|
|
597
|
+
cache: model.dependencyCache,
|
|
598
|
+
...(model.dependencyCachePath ? { "dependency-cache-path": model.dependencyCachePath } : {})
|
|
599
|
+
}, "setup")
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
const steps = [actionManifestStep("setup-node", "Setup Node", SETUP_NODE_ACTION, { "node-version": model.nodeVersion }, "setup")];
|
|
603
|
+
if (model.projectKind === "deno") {
|
|
604
|
+
steps.push(actionManifestStep("setup-deno", "Setup Deno", DENO_SETUP_ACTION, { "deno-version": `v${DEFAULT_DENO_VERSION}.x` }, "setup"));
|
|
605
|
+
}
|
|
606
|
+
return steps;
|
|
607
|
+
}
|
|
608
|
+
function dependencyInstallManifestSteps(model) {
|
|
609
|
+
if (model.setup === "async")
|
|
610
|
+
return [];
|
|
611
|
+
return renderDependencyInstallSteps(model).map((_, index) => {
|
|
612
|
+
const command = model.projectKind === "deno"
|
|
613
|
+
? `deno install --frozen=${model.dependencyCachePath ? "true" : "false"}`
|
|
614
|
+
: `${model.packageManager} install --frozen-lockfile`;
|
|
615
|
+
return shellManifestStep(`install-dependencies-${index}`, "Install dependencies", command, "setup");
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function taskCacheManifestSteps(model, target) {
|
|
619
|
+
if (!model.taskCache)
|
|
620
|
+
return [];
|
|
621
|
+
const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
|
|
622
|
+
return [
|
|
623
|
+
shellManifestStep("write-task-cache-manifest", "Write task cache manifest", renderCacheManifestCommand(model, target, manifestPath, "read-only"), "cache"),
|
|
624
|
+
actionManifestStep("restore-async-task-cache", "Restore Async task cache", ASYNC_CACHE_ACTION, {
|
|
625
|
+
mode: "restore",
|
|
626
|
+
manifest: manifestPath,
|
|
627
|
+
trust: "read-only"
|
|
628
|
+
}, "cache")
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
function taskCacheSaveManifestSteps(model, target) {
|
|
632
|
+
if (!model.taskCache)
|
|
633
|
+
return [];
|
|
634
|
+
const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
|
|
635
|
+
return [
|
|
636
|
+
actionManifestStep("save-async-task-cache", "Save Async task cache", ASYNC_CACHE_ACTION, {
|
|
637
|
+
mode: "save",
|
|
638
|
+
manifest: manifestPath,
|
|
639
|
+
trust: "read-write"
|
|
640
|
+
}, "cache", { if: "${{ success() && github.event_name != 'pull_request' && steps.async-cache-restore.outputs.cache-hit != 'true' }}" })
|
|
641
|
+
];
|
|
642
|
+
}
|
|
643
|
+
function lifecycleManifestSteps(model, job, plan) {
|
|
644
|
+
const steps = [];
|
|
645
|
+
if (hasReleaseLifecycle(plan)) {
|
|
646
|
+
const packagePath = lifecyclePackagePath(plan) ?? ".";
|
|
647
|
+
steps.push(releaseDoctorManifestStep("plan-release-package", "Plan release package", "plan", packagePath), releaseDoctorManifestStep("inspect-release-package", "Inspect release package", "inspect", packagePath), releaseDoctorManifestStep("check-release-changelog", "Check release changelog", "changelog", packagePath), releaseDoctorManifestStep("render-release-notes", "Render release notes", "notes", packagePath));
|
|
648
|
+
}
|
|
649
|
+
for (const item of plan) {
|
|
650
|
+
if (item.kind === "run-task") {
|
|
651
|
+
steps.push(runActionManifestStep(`run-pipeline-task-${safeArtifactPart(item.taskId)}`, `Run pipeline task ${item.taskId}`, `${model.command} github check && ${model.command} run-task ${shellWord(item.taskId)}`, scopeTaskRunEnv(job.env, model.tasks[item.taskId]), "run", { "artifact-name": `async-pipeline-\${{ github.job }}-${safeArtifactPart(item.taskId)}-runs` }));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (item.kind === "preview") {
|
|
655
|
+
steps.push(previewManifestStep(item));
|
|
656
|
+
if (item.mode === "pr" && item.comment)
|
|
657
|
+
steps.push(commentManifestStep("comment-package-preview", "Comment package preview"));
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (item.kind === "release") {
|
|
661
|
+
steps.push(releaseDoctorManifestStep("run-release-doctor", "Run release doctor", "doctor", item.packagePath, true));
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
steps.push(publishManifestStep(item, job.requires?.provenance === true));
|
|
665
|
+
}
|
|
666
|
+
return steps;
|
|
667
|
+
}
|
|
668
|
+
function attestManifestSteps(model, lifecyclePlan, provenance) {
|
|
669
|
+
if (!model.attest.enabled || !hasReleaseLifecycle(lifecyclePlan))
|
|
670
|
+
return [];
|
|
671
|
+
const packagePath = model.attest.packagePath ?? lifecyclePackagePath(lifecyclePlan) ?? ".";
|
|
672
|
+
const steps = [
|
|
673
|
+
actionManifestStep("create-attestation-subject-manifest", "Create attestation subject manifest", ASYNC_ATTEST_ACTION, {
|
|
674
|
+
mode: "digest",
|
|
675
|
+
"package-path": packagePath,
|
|
676
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
677
|
+
"sbom-path": model.attest.sbomPath,
|
|
678
|
+
"require-npm-provenance": model.attest.requireNpmProvenance,
|
|
679
|
+
"tarball-scan": model.attest.tarballScan
|
|
680
|
+
}, "attest"),
|
|
681
|
+
actionManifestStep("write-attestation-sbom-evidence", "Write attestation SBOM evidence", ASYNC_ATTEST_ACTION, {
|
|
682
|
+
mode: "sbom",
|
|
683
|
+
"package-path": packagePath,
|
|
684
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
685
|
+
"sbom-path": model.attest.sbomPath
|
|
686
|
+
}, "attest")
|
|
687
|
+
];
|
|
688
|
+
if (model.attest.githubAttestation || provenance) {
|
|
689
|
+
steps.push(actionManifestStep("record-github-attestation-intent", "Record GitHub attestation intent", ASYNC_ATTEST_ACTION, {
|
|
690
|
+
mode: "attest",
|
|
691
|
+
"package-path": packagePath,
|
|
692
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
693
|
+
"github-attestation": true
|
|
694
|
+
}, "attest", { permissions: { "id-token": "write" }, networked: true, dangerous: true }));
|
|
695
|
+
}
|
|
696
|
+
return steps;
|
|
697
|
+
}
|
|
698
|
+
function agentEvidenceManifestSteps(model, job) {
|
|
699
|
+
const evidence = agentEvidenceForTargets(model.tasks, job.target);
|
|
700
|
+
if (!evidence.hasAgentStep)
|
|
701
|
+
return [];
|
|
702
|
+
const canComment = job.github?.permissions?.issues === "write" || job.github?.permissions?.pullRequests === "write";
|
|
703
|
+
const steps = [
|
|
704
|
+
actionManifestStep("bundle-agent-evidence", "Bundle agent evidence", ASYNC_AGENT_EVIDENCE_ACTION, {
|
|
705
|
+
mode: canComment ? "comment" : "bundle",
|
|
706
|
+
"run-directory": ".async/runs",
|
|
707
|
+
outputs: evidence.outputs,
|
|
708
|
+
"evidence-path": ".async/actions/agent-evidence/${{ github.job }}/manifest.json",
|
|
709
|
+
"bundle-path": ".async/actions/agent-evidence/${{ github.job }}/bundle.json",
|
|
710
|
+
"receipt-path": ".async/actions/receipts/${{ github.job }}-agent-evidence.json",
|
|
711
|
+
comment: canComment,
|
|
712
|
+
"comment-marker": "async-agent-evidence-${{ github.job }}"
|
|
713
|
+
}, "agent-evidence")
|
|
714
|
+
];
|
|
715
|
+
if (canComment) {
|
|
716
|
+
steps.push(commentManifestStep("comment-agent-evidence", "Comment agent evidence"));
|
|
717
|
+
}
|
|
718
|
+
return steps;
|
|
719
|
+
}
|
|
720
|
+
function evidenceCollectManifestSteps(model, options = {}) {
|
|
721
|
+
if (!model.evidence.enabled)
|
|
722
|
+
return [];
|
|
723
|
+
const paths = [...new Set([...model.evidence.paths, ...(options.extraPaths ?? [])])];
|
|
724
|
+
return [
|
|
725
|
+
actionManifestStep("collect-evidence-manifest", "Collect evidence manifest", ASYNC_EVIDENCE_ACTION, {
|
|
726
|
+
mode: "collect",
|
|
727
|
+
paths,
|
|
728
|
+
"receipt-paths": model.evidence.receiptPaths,
|
|
729
|
+
"manifest-path": ".async/evidence/${{ github.job }}/manifest.json",
|
|
730
|
+
"summary-path": ".async/evidence/${{ github.job }}/summary.md",
|
|
731
|
+
"artifact-name": `${model.evidence.artifactNamePrefix}-\${{ github.job }}`,
|
|
732
|
+
"retention-days": model.evidence.retentionDays,
|
|
733
|
+
"if-no-files-found": model.evidence.ifNoFilesFound,
|
|
734
|
+
"include-summary": model.evidence.includeSummary
|
|
735
|
+
}, "evidence", { if: "${{ always() }}" })
|
|
736
|
+
];
|
|
737
|
+
}
|
|
738
|
+
function contractActionInput(contract) {
|
|
739
|
+
return sortObject({
|
|
740
|
+
mode: contract.mode,
|
|
741
|
+
checks: contractChecks(contract).join(","),
|
|
742
|
+
"package-path": contract.packagePath,
|
|
743
|
+
...(contract.schema.enabled
|
|
744
|
+
? {
|
|
745
|
+
"schema-sources": contract.schema.sources.join("\n"),
|
|
746
|
+
"schema-output": contract.schema.output
|
|
747
|
+
}
|
|
748
|
+
: {}),
|
|
749
|
+
"evidence-dir": contract.evidenceDir,
|
|
750
|
+
annotations: contract.annotations,
|
|
751
|
+
"fail-on": contract.mode === "report" ? "advisory" : "blocking"
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function contractChecks(contract) {
|
|
755
|
+
const checks = [];
|
|
756
|
+
if (contract.api)
|
|
757
|
+
checks.push("api");
|
|
758
|
+
if (contract.claims)
|
|
759
|
+
checks.push("claims");
|
|
760
|
+
if (contract.schema.enabled)
|
|
761
|
+
checks.push("schema");
|
|
762
|
+
return checks;
|
|
763
|
+
}
|
|
764
|
+
function runActionManifestStep(id, name, command, env, contract, extraWith = {}) {
|
|
765
|
+
const networked = commandLooksNetworked(command);
|
|
766
|
+
return actionManifestStep(id, name, ASYNC_RUN_ACTION, {
|
|
767
|
+
command,
|
|
768
|
+
"check-generated": false,
|
|
769
|
+
"artifact-name": "async-pipeline-${{ github.job }}-runs",
|
|
770
|
+
...extraWith
|
|
771
|
+
}, contract, { env: manifestEnv(env), secrets: secretNamesFromEnv(env), networked, dangerous: networked });
|
|
772
|
+
}
|
|
773
|
+
function releaseDoctorManifestStep(id, name, mode, packagePath, live = false) {
|
|
774
|
+
return actionManifestStep(id, name, ASYNC_DOCTOR_ACTION, {
|
|
775
|
+
mode,
|
|
776
|
+
"package-path": packagePath,
|
|
777
|
+
"evidence-dir": ".async/release",
|
|
778
|
+
"release-command": ASYNC_RELEASE_COMMAND,
|
|
779
|
+
...(mode === "doctor" ? { network: live ? "live" : "mock" } : {})
|
|
780
|
+
}, "release", { networked: live, dangerous: live, secrets: live ? ["GITHUB_TOKEN"] : [] });
|
|
781
|
+
}
|
|
782
|
+
function previewManifestStep(preview) {
|
|
783
|
+
return actionManifestStep(`publish-${preview.mode}-package-preview`, `Publish ${preview.mode === "main" ? "main" : "PR"} package preview`, ASYNC_PREVIEW_ACTION, {
|
|
784
|
+
"package-path": preview.packagePath,
|
|
785
|
+
"target-registry": preview.registry,
|
|
786
|
+
...(preview.namespace ? { namespace: preview.namespace } : {}),
|
|
787
|
+
mode: preview.mode,
|
|
788
|
+
comment: preview.comment,
|
|
789
|
+
"token-env-name": preview.tokenEnv
|
|
790
|
+
}, "preview", { permissions: { packages: "write" }, secrets: [preview.tokenEnv], networked: true, dangerous: true });
|
|
791
|
+
}
|
|
792
|
+
function publishManifestStep(publish, provenance) {
|
|
793
|
+
const name = publish.mode === "github-release"
|
|
794
|
+
? "Create or update GitHub Release"
|
|
795
|
+
: publish.mode === "github-packages"
|
|
796
|
+
? "Publish GitHub Packages mirror"
|
|
797
|
+
: "Publish npm package";
|
|
798
|
+
return actionManifestStep(`publish-${publish.mode}`, name, ASYNC_PUBLISH_ACTION, {
|
|
799
|
+
"package-path": publish.packagePath,
|
|
800
|
+
mode: publish.mode,
|
|
801
|
+
registry: publish.registry,
|
|
802
|
+
"dist-tag": publish.distTag,
|
|
803
|
+
...(publish.mode === "npm" ? { "token-env-name": "NODE_AUTH_TOKEN", provenance } : {}),
|
|
804
|
+
...(publish.mode === "github-packages" ? { "token-env-name": "GITHUB_TOKEN" } : {}),
|
|
805
|
+
...(publish.mode === "github-release" ? { "notes-file": ".async/release/release-notes.md" } : {})
|
|
806
|
+
}, "publish", {
|
|
807
|
+
permissions: {
|
|
808
|
+
...(publish.mode === "github-packages" || publish.mode === "github-release" ? { packages: "write", contents: "write" } : {}),
|
|
809
|
+
...(provenance ? { "id-token": "write" } : {})
|
|
810
|
+
},
|
|
811
|
+
secrets: publish.mode === "npm" ? ["NODE_AUTH_TOKEN"] : ["GITHUB_TOKEN"],
|
|
812
|
+
networked: true,
|
|
813
|
+
dangerous: true
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function commentManifestStep(id, name) {
|
|
817
|
+
return actionManifestStep(id, name, ASYNC_COMMENT_ACTION, {
|
|
818
|
+
mode: "pr-comment",
|
|
819
|
+
repository: "${{ github.repository }}",
|
|
820
|
+
number: "${{ github.event.pull_request.number }}",
|
|
821
|
+
marker: "${{ steps.source.outputs.comment-marker }}",
|
|
822
|
+
body: "${{ steps.source.outputs.comment-body }}"
|
|
823
|
+
}, "comment", { permissions: { issues: "write" }, secrets: ["GITHUB_TOKEN"], networked: true });
|
|
824
|
+
}
|
|
825
|
+
function pagesActionManifestStep(id, name, pages) {
|
|
826
|
+
return actionManifestStep(id, name, ASYNC_PAGES_ACTION, {
|
|
827
|
+
mode: pages.build.kind,
|
|
828
|
+
...(pages.build.kind === "jekyll" ? { source: pages.build.source, destination: pages.build.destination ?? "./_site" } : {}),
|
|
829
|
+
...(pages.build.kind === "static" ? { path: pages.build.path } : {}),
|
|
830
|
+
...(pages.build.kind === "prerender" ? { path: pages.build.path, "validate-index": pages.build.validateIndex ?? true, "spa-fallback": pages.build.spaFallback ?? false } : {})
|
|
831
|
+
}, "pages", { permissions: { pages: "write", "id-token": "write" } });
|
|
832
|
+
}
|
|
833
|
+
function actionManifestStep(id, name, ref, input, contract, options = {}) {
|
|
834
|
+
const action = actionFromRef(ref);
|
|
835
|
+
return {
|
|
836
|
+
id: safeGeneratedJobId(id),
|
|
837
|
+
name,
|
|
838
|
+
uses: `${action.uses}@${action.sha}`,
|
|
839
|
+
label: action.label,
|
|
840
|
+
...(options.if ? { if: options.if } : {}),
|
|
841
|
+
with: sortObject(input),
|
|
842
|
+
...(options.env && Object.keys(options.env).length > 0 ? { env: options.env } : {}),
|
|
843
|
+
permissions: options.permissions ?? {},
|
|
844
|
+
secrets: [...new Set(options.secrets ?? [])].sort(),
|
|
845
|
+
local: {
|
|
846
|
+
contract,
|
|
847
|
+
mode: "action",
|
|
848
|
+
network: options.networked ? "mock" : "mock",
|
|
849
|
+
networked: options.networked ?? false,
|
|
850
|
+
dangerous: options.dangerous ?? false,
|
|
851
|
+
...(options.networked ? { mockReason: "networked action is mocked unless --network allow is selected" } : {}),
|
|
852
|
+
...(options.fallbackReason ? { fallbackReason: options.fallbackReason } : {})
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function shellManifestStep(id, name, command, contract) {
|
|
857
|
+
return {
|
|
858
|
+
id: safeGeneratedJobId(id),
|
|
859
|
+
name,
|
|
860
|
+
run: command,
|
|
861
|
+
secrets: [],
|
|
862
|
+
permissions: {},
|
|
863
|
+
local: {
|
|
864
|
+
contract,
|
|
865
|
+
mode: "shell",
|
|
866
|
+
network: "mock",
|
|
867
|
+
networked: commandLooksNetworked(command),
|
|
868
|
+
dangerous: commandLooksDangerous(command),
|
|
869
|
+
...(commandLooksNetworked(command) ? { mockReason: "networked shell command is simulated unless --network allow is selected" } : {})
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function actionFromRef(ref) {
|
|
874
|
+
const found = GENERATED_ACTIONS.find((action) => action.ref === ref);
|
|
875
|
+
if (found)
|
|
876
|
+
return found;
|
|
877
|
+
const [usesPart, rest] = ref.split("@");
|
|
878
|
+
const sha = rest?.split(/\s+/u)[0] ?? "";
|
|
879
|
+
return { id: usesPart ?? ref, uses: usesPart ?? ref, sha, label: "", ref };
|
|
880
|
+
}
|
|
881
|
+
function manifestEnv(env) {
|
|
882
|
+
return Object.fromEntries(Object.entries(env)
|
|
883
|
+
.map(([name, value]) => [name, renderGitHubEnvValue(value)])
|
|
884
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
885
|
+
.sort(([left], [right]) => left.localeCompare(right)));
|
|
886
|
+
}
|
|
887
|
+
function secretNamesFromEnv(env) {
|
|
888
|
+
const names = new Set();
|
|
889
|
+
for (const [envName, value] of Object.entries(env)) {
|
|
890
|
+
if (isSecretEnvValue(value)) {
|
|
891
|
+
names.add(typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ? value.name : envName);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return [...names].sort();
|
|
895
|
+
}
|
|
896
|
+
function artifactsForSteps(jobId, steps, retentionDays, ifNoFilesFound) {
|
|
897
|
+
const artifacts = [];
|
|
898
|
+
for (const step of steps) {
|
|
899
|
+
const artifactName = typeof step.with?.["artifact-name"] === "string" ? step.with["artifact-name"] : undefined;
|
|
900
|
+
if (artifactName) {
|
|
901
|
+
artifacts.push({
|
|
902
|
+
name: artifactName,
|
|
903
|
+
path: artifactPathForContract(step.local.contract),
|
|
904
|
+
mode: step.local.contract === "evidence" ? "upload" : "local",
|
|
905
|
+
producerJob: jobId,
|
|
906
|
+
retentionDays,
|
|
907
|
+
ifNoFilesFound
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return artifacts.sort((left, right) => left.name.localeCompare(right.name));
|
|
912
|
+
}
|
|
913
|
+
function artifactPathForContract(contract) {
|
|
914
|
+
if (contract === "evidence")
|
|
915
|
+
return ".async/evidence";
|
|
916
|
+
if (contract === "agent-evidence")
|
|
917
|
+
return ".async/actions/agent-evidence";
|
|
918
|
+
if (contract === "contract")
|
|
919
|
+
return ".async/contract";
|
|
920
|
+
if (contract === "pages")
|
|
921
|
+
return ".async/pages";
|
|
922
|
+
return ".async/runs";
|
|
923
|
+
}
|
|
924
|
+
function buildGeneratedPagesManifest(model, rendered, event, network) {
|
|
925
|
+
const pages = model.pages;
|
|
926
|
+
const steps = [
|
|
927
|
+
checkoutStep(),
|
|
928
|
+
...setupManifestSteps(model),
|
|
929
|
+
...dependencyInstallManifestSteps(model),
|
|
930
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
931
|
+
...taskCacheManifestSteps(model, { kind: "task", id: pages.target ?? "docs.site" }),
|
|
932
|
+
runActionManifestStep("run-pages-target", "Run Pages target", `${model.command} github check && ${model.command} run-task ${shellWord(pages.target ?? "docs.site")}`, {}, "run"),
|
|
933
|
+
...taskCacheSaveManifestSteps(model, { kind: "task", id: pages.target ?? "docs.site" }),
|
|
934
|
+
pagesActionManifestStep("upload-pages-artifact", "Upload Pages artifact", pages),
|
|
935
|
+
...evidenceCollectManifestSteps(model)
|
|
936
|
+
];
|
|
937
|
+
return makeJobManifest(model, rendered, event, {
|
|
938
|
+
id: pages.job,
|
|
939
|
+
kind: "generated",
|
|
940
|
+
target: pages.target ? [pages.target] : [],
|
|
941
|
+
runsOn: "ubuntu-latest",
|
|
942
|
+
permissions: { contents: "read", pages: "write", "id-token": "write" },
|
|
943
|
+
environment: pages.environment ?? null,
|
|
944
|
+
concurrency: null,
|
|
945
|
+
if: renderGeneratedPagesCondition(pages),
|
|
946
|
+
trigger: ["pull_request", "push", "workflow_dispatch"],
|
|
947
|
+
steps,
|
|
948
|
+
network
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function buildPackagePreviewManifest(model, rendered, event, network) {
|
|
952
|
+
const preview = model.packagePreviews;
|
|
953
|
+
const steps = [
|
|
954
|
+
checkoutStep(),
|
|
955
|
+
...setupManifestSteps(model),
|
|
956
|
+
...dependencyInstallManifestSteps(model),
|
|
957
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
958
|
+
...taskCacheManifestSteps(model, { kind: "task", id: preview.target ?? "pack", manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" }),
|
|
959
|
+
runActionManifestStep("run-package-preview-target", "Run package preview target", `${model.command} github check && ${model.command} run-task ${shellWord(preview.target ?? "pack")}`, {}, "run"),
|
|
960
|
+
...taskCacheSaveManifestSteps(model, { kind: "task", id: preview.target ?? "pack", manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" }),
|
|
961
|
+
actionManifestStep("publish-package-preview", "Publish package preview", ASYNC_PREVIEW_ACTION, {
|
|
962
|
+
"package-path": preview.package ?? ".",
|
|
963
|
+
"target-registry": preview.registry,
|
|
964
|
+
...(preview.namespace ? { namespace: preview.namespace } : {}),
|
|
965
|
+
mode: "pr",
|
|
966
|
+
comment: preview.comment,
|
|
967
|
+
"token-env-name": preview.tokenEnv
|
|
968
|
+
}, "preview", { permissions: { packages: "write" }, secrets: [preview.tokenEnv], networked: true, dangerous: true }),
|
|
969
|
+
...(preview.comment ? [commentManifestStep("comment-package-preview", "Comment package preview")] : []),
|
|
970
|
+
...evidenceCollectManifestSteps(model)
|
|
971
|
+
];
|
|
972
|
+
return makeJobManifest(model, rendered, event, {
|
|
973
|
+
id: "package-preview",
|
|
974
|
+
kind: "generated",
|
|
975
|
+
target: preview.target ? [preview.target] : [],
|
|
976
|
+
runsOn: "ubuntu-latest",
|
|
977
|
+
permissions: { contents: "read", issues: "write", packages: "write", "pull-requests": "write" },
|
|
978
|
+
environment: null,
|
|
979
|
+
concurrency: null,
|
|
980
|
+
if: "github.event_name == 'pull_request' && github.event.pull_request.draft == false",
|
|
981
|
+
trigger: ["pull_request"],
|
|
982
|
+
steps,
|
|
983
|
+
network
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
function buildBridgeManifest(model, rendered, event, network) {
|
|
987
|
+
const bridge = model.bridge;
|
|
988
|
+
const command = [
|
|
989
|
+
"npx",
|
|
990
|
+
"--yes",
|
|
991
|
+
`@async/github-app@${bridge.packageVersion}`,
|
|
992
|
+
"actions",
|
|
993
|
+
"pull",
|
|
994
|
+
"--branch-prefix",
|
|
995
|
+
bridge.branchPrefix,
|
|
996
|
+
"--pull-request",
|
|
997
|
+
String(bridge.pullRequest),
|
|
998
|
+
...bridge.allowedPaths.flatMap((path) => ["--allowed-path", path])
|
|
999
|
+
].map(shellWord).join(" ");
|
|
1000
|
+
const steps = [
|
|
1001
|
+
checkoutStep(),
|
|
1002
|
+
...setupManifestSteps(model),
|
|
1003
|
+
...dependencyInstallManifestSteps(model),
|
|
1004
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
1005
|
+
runActionManifestStep("check-generated-workflow", "Check generated workflow", `${model.command} github check`, {}, "run"),
|
|
1006
|
+
runActionManifestStep("pull-and-apply-async-bridge-change-sets", "Pull and apply Async bridge change sets", command, {
|
|
1007
|
+
ASYNC_PROJECT_URL: { kind: "async-pipeline.env.var", name: bridge.endpointVar },
|
|
1008
|
+
ASYNC_PROJECT_TOKEN: { kind: "async-pipeline.env.secret", name: bridge.tokenEnv },
|
|
1009
|
+
GITHUB_TOKEN: { kind: "async-pipeline.env.secret", name: "GITHUB_TOKEN" }
|
|
1010
|
+
}, "storage-bridge"),
|
|
1011
|
+
...evidenceCollectManifestSteps(model)
|
|
1012
|
+
];
|
|
1013
|
+
return makeJobManifest(model, rendered, event, {
|
|
1014
|
+
id: bridge.job,
|
|
1015
|
+
kind: "generated",
|
|
1016
|
+
target: [],
|
|
1017
|
+
runsOn: "ubuntu-latest",
|
|
1018
|
+
permissions: { contents: "write", "pull-requests": "write" },
|
|
1019
|
+
environment: null,
|
|
1020
|
+
concurrency: "async-bridge-${{ github.repository }}",
|
|
1021
|
+
if: renderBridgeCondition(bridge),
|
|
1022
|
+
trigger: ["schedule", "workflow_dispatch"],
|
|
1023
|
+
steps,
|
|
1024
|
+
network
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function buildContractManifest(model, rendered, event, network) {
|
|
1028
|
+
const contract = model.contract;
|
|
1029
|
+
const steps = [
|
|
1030
|
+
checkoutStep(),
|
|
1031
|
+
...setupManifestSteps(model),
|
|
1032
|
+
...dependencyInstallManifestSteps(model),
|
|
1033
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
1034
|
+
actionManifestStep("run-contract-evidence", "Run contract evidence", ASYNC_CONTRACT_ACTION, contractActionInput(contract), "contract"),
|
|
1035
|
+
...evidenceCollectManifestSteps(model, { extraPaths: [contract.evidenceDir] })
|
|
1036
|
+
];
|
|
1037
|
+
return makeJobManifest(model, rendered, event, {
|
|
1038
|
+
id: contract.job,
|
|
1039
|
+
kind: "generated",
|
|
1040
|
+
target: [],
|
|
1041
|
+
runsOn: "ubuntu-latest",
|
|
1042
|
+
permissions: { contents: "read" },
|
|
1043
|
+
environment: null,
|
|
1044
|
+
concurrency: null,
|
|
1045
|
+
if: renderContractCondition(contract),
|
|
1046
|
+
trigger: contract.mode === "release" ? ["release", "workflow_dispatch"] : ["pull_request", "workflow_dispatch"],
|
|
1047
|
+
steps,
|
|
1048
|
+
network
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
function buildEvidenceFanInManifest(model, rendered, event, network) {
|
|
1052
|
+
return makeJobManifest(model, rendered, event, {
|
|
1053
|
+
id: model.evidence.job,
|
|
1054
|
+
kind: "generated",
|
|
1055
|
+
target: [],
|
|
1056
|
+
runsOn: "ubuntu-latest",
|
|
1057
|
+
permissions: { contents: "read" },
|
|
1058
|
+
environment: null,
|
|
1059
|
+
concurrency: null,
|
|
1060
|
+
if: "always()",
|
|
1061
|
+
trigger: ["fan-in"],
|
|
1062
|
+
steps: [
|
|
1063
|
+
actionManifestStep("merge-evidence-manifests", "Merge evidence manifests", ASYNC_EVIDENCE_ACTION, {
|
|
1064
|
+
mode: "merge",
|
|
1065
|
+
"artifact-pattern": `${model.evidence.artifactNamePrefix}-*`,
|
|
1066
|
+
"manifest-path": ".async/evidence/index.json",
|
|
1067
|
+
"summary-path": ".async/evidence/index.md",
|
|
1068
|
+
"artifact-name": `${model.evidence.artifactNamePrefix}-index`,
|
|
1069
|
+
"retention-days": model.evidence.retentionDays,
|
|
1070
|
+
"if-no-files-found": model.evidence.ifNoFilesFound,
|
|
1071
|
+
"include-summary": model.evidence.includeSummary
|
|
1072
|
+
}, "evidence")
|
|
1073
|
+
],
|
|
1074
|
+
network
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
function buildDependabotManifest(model, rendered, event, network) {
|
|
1078
|
+
return makeJobManifest(model, rendered, event, {
|
|
1079
|
+
id: "dependabot-auto-merge",
|
|
1080
|
+
kind: "generated",
|
|
1081
|
+
target: [],
|
|
1082
|
+
runsOn: "ubuntu-latest",
|
|
1083
|
+
permissions: { contents: "write", "pull-requests": "write" },
|
|
1084
|
+
environment: null,
|
|
1085
|
+
concurrency: null,
|
|
1086
|
+
if: "github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.draft == false",
|
|
1087
|
+
trigger: ["pull_request_target"],
|
|
1088
|
+
steps: [
|
|
1089
|
+
actionManifestStep("fetch-dependabot-metadata", "Fetch Dependabot metadata", DEPENDABOT_FETCH_METADATA_ACTION, { "github-token": "${{ secrets.GITHUB_TOKEN }}" }, "dependabot", { secrets: ["GITHUB_TOKEN"], networked: true }),
|
|
1090
|
+
actionManifestStep("merge-validated-dependabot-pr", "Merge validated Dependabot PR", ASYNC_DEPENDABOT_MERGE_ACTION, { "allowed-ecosystems": model.dependabotAutoMerge.ecosystems }, "dependabot", { permissions: { contents: "write", "pull-requests": "write" }, secrets: ["GITHUB_TOKEN"], networked: true, dangerous: true })
|
|
1091
|
+
],
|
|
1092
|
+
network
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
function buildSourceImpactPlanManifest(model, rendered, event, sourceJob, network) {
|
|
1096
|
+
return makeJobManifest(model, rendered, event, {
|
|
1097
|
+
id: sourceJob.planJob,
|
|
1098
|
+
kind: "generated",
|
|
1099
|
+
target: [],
|
|
1100
|
+
runsOn: sourceJob.github?.runsOn ?? "ubuntu-latest",
|
|
1101
|
+
permissions: { contents: "read" },
|
|
1102
|
+
environment: null,
|
|
1103
|
+
concurrency: null,
|
|
1104
|
+
if: sourceJob.if ?? null,
|
|
1105
|
+
trigger: ["source-impact"],
|
|
1106
|
+
steps: [
|
|
1107
|
+
checkoutStep(),
|
|
1108
|
+
...setupManifestSteps(model),
|
|
1109
|
+
shellManifestStep("write-generated-source-plan", "Write generated source plan", `write ${sourceJob.planPath}`, "source-impact"),
|
|
1110
|
+
actionManifestStep("plan-source-impact-matrix", "Plan source impact matrix", ASYNC_SOURCE_IMPACT_ACTION, {
|
|
1111
|
+
mode: "matrix",
|
|
1112
|
+
"plan-path": sourceJob.planPath,
|
|
1113
|
+
"matrix-path": `.async/actions/source-impact/${safeArtifactPart(sourceJob.job)}-matrix.json`
|
|
1114
|
+
}, "source-impact")
|
|
1115
|
+
],
|
|
1116
|
+
network
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
function buildSourceImpactMatrixManifest(model, rendered, event, sourceJob, network) {
|
|
1120
|
+
return makeJobManifest(model, rendered, event, {
|
|
1121
|
+
id: sourceJob.matrixJob,
|
|
1122
|
+
kind: "generated",
|
|
1123
|
+
target: [sourceJob.job],
|
|
1124
|
+
runsOn: sourceJob.github?.runsOn ?? "ubuntu-latest",
|
|
1125
|
+
matrix: sourceJob.plan.matrix.include.map((entry, index) => ({ runner: [String(entry.source)], index })),
|
|
1126
|
+
permissions: { contents: "read" },
|
|
1127
|
+
environment: null,
|
|
1128
|
+
concurrency: null,
|
|
1129
|
+
if: sourceJob.if ?? null,
|
|
1130
|
+
trigger: ["source-impact"],
|
|
1131
|
+
steps: [
|
|
1132
|
+
checkoutStep(),
|
|
1133
|
+
...setupManifestSteps(model),
|
|
1134
|
+
actionManifestStep("validate-source-impact-row", "Validate source impact row", ASYNC_SOURCE_IMPACT_ACTION, {
|
|
1135
|
+
mode: "validate",
|
|
1136
|
+
source: "${{ matrix.source }}"
|
|
1137
|
+
}, "source-impact"),
|
|
1138
|
+
runActionManifestStep("run-source-impact-task", "Run source impact task", `${model.command} github check && ${model.command} run-task ${shellWord(sourceJob.job)}`, sourceJob.env, "run"),
|
|
1139
|
+
...evidenceCollectManifestSteps(model)
|
|
1140
|
+
],
|
|
1141
|
+
network
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
function generatedPagesSelected(pages, event) {
|
|
1145
|
+
if (event.name === "pull_request")
|
|
1146
|
+
return pages.triggers.pullRequest;
|
|
1147
|
+
if (event.name === "push")
|
|
1148
|
+
return Boolean(pages.triggers.main);
|
|
1149
|
+
return event.name === "workflow_dispatch" && event.selectedJob === pages.job;
|
|
1150
|
+
}
|
|
1151
|
+
function bridgeSelected(bridge, event) {
|
|
1152
|
+
if (event.name === "schedule")
|
|
1153
|
+
return Boolean(bridge.schedule && (!event.schedule || event.schedule === bridge.schedule));
|
|
1154
|
+
return event.name === "workflow_dispatch" && event.selectedJob === bridge.job;
|
|
1155
|
+
}
|
|
1156
|
+
function contractSelected(contract, event) {
|
|
1157
|
+
if (event.name === "workflow_dispatch")
|
|
1158
|
+
return event.selectedJob === contract.job;
|
|
1159
|
+
if (contract.mode === "release") {
|
|
1160
|
+
return event.name === "release" && (!event.action || event.action === "published");
|
|
1161
|
+
}
|
|
1162
|
+
return event.name === "pull_request";
|
|
1163
|
+
}
|
|
1164
|
+
function skipReasonForJob(event, trigger) {
|
|
1165
|
+
if (event.name === "workflow_dispatch" && !event.selectedJob && trigger.some((id) => id === "manual"))
|
|
1166
|
+
return "manual_selector_missing";
|
|
1167
|
+
return "event_filter";
|
|
1168
|
+
}
|
|
1169
|
+
function skipReasonForGeneratedJob(event, manualIds) {
|
|
1170
|
+
if (event.name === "workflow_dispatch" && !event.selectedJob && manualIds.length > 0)
|
|
1171
|
+
return "manual_selector_missing";
|
|
1172
|
+
return "event_filter";
|
|
1173
|
+
}
|
|
1174
|
+
function validateLocalStep(manifest, step, env) {
|
|
1175
|
+
const issues = [];
|
|
1176
|
+
for (const [permission, required] of Object.entries(step.permissions ?? {})) {
|
|
1177
|
+
const actual = manifest.job.permissions[permission];
|
|
1178
|
+
if (!permissionAllows(actual, required)) {
|
|
1179
|
+
issues.push(`requires ${permission}: ${required}, but job grants ${actual ?? "none"}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (step.local.networked && manifest.local.network === "deny") {
|
|
1183
|
+
issues.push("networked step is denied by --network deny");
|
|
1184
|
+
}
|
|
1185
|
+
if (step.local.networked && manifest.local.network === "allow") {
|
|
1186
|
+
for (const secret of step.secrets) {
|
|
1187
|
+
if (!env[secret])
|
|
1188
|
+
issues.push(`requires secret env ${secret} for --network allow`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return issues;
|
|
1192
|
+
}
|
|
1193
|
+
function permissionAllows(actual, required) {
|
|
1194
|
+
if (required === "read")
|
|
1195
|
+
return actual === "read" || actual === "write";
|
|
1196
|
+
return actual === "write";
|
|
1197
|
+
}
|
|
1198
|
+
function commandLooksNetworked(command) {
|
|
1199
|
+
return /\b(?:gh|npm\s+publish|npx|curl|wget|git\s+(?:push|fetch|pull|clone))\b/u.test(command);
|
|
1200
|
+
}
|
|
1201
|
+
function commandLooksDangerous(command) {
|
|
1202
|
+
return /\b(?:publish|push|release|comment|merge|pull-request)\b/u.test(command);
|
|
1203
|
+
}
|
|
244
1204
|
function buildRenderModel(pipeline, options) {
|
|
245
1205
|
const usedTriggerIds = new Set(Object.values(pipeline.jobs).flatMap((job) => job.trigger));
|
|
246
1206
|
const usedTriggers = Object.fromEntries([...usedTriggerIds].sort().map((triggerId) => [triggerId, pipeline.triggers[triggerId]]));
|
|
@@ -255,6 +1215,7 @@ function buildRenderModel(pipeline, options) {
|
|
|
255
1215
|
const evidence = resolveGitHubEvidence(pipeline);
|
|
256
1216
|
const pages = resolveGitHubPages(pipeline);
|
|
257
1217
|
const bridge = resolveGitHubBridge(pipeline);
|
|
1218
|
+
const contract = resolveGitHubContract(pipeline, { pages, bridge });
|
|
258
1219
|
if (pages.enabled) {
|
|
259
1220
|
if (pages.triggers.pullRequest) {
|
|
260
1221
|
addGitHubEventTrigger(triggers, "pull_request");
|
|
@@ -266,6 +1227,14 @@ function buildRenderModel(pipeline, options) {
|
|
|
266
1227
|
if (bridge.actionsJob.scheduled && bridge.schedule) {
|
|
267
1228
|
addScheduleTrigger(triggers, bridge.schedule, "async-bridge");
|
|
268
1229
|
}
|
|
1230
|
+
if (contract.enabled) {
|
|
1231
|
+
if (contract.mode === "release") {
|
|
1232
|
+
addReleasePublishedTrigger(triggers);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
addPullRequestTrigger(triggers, "pull_request");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
269
1238
|
const manualDispatchJobs = Object.values(pipeline.jobs)
|
|
270
1239
|
.filter((job) => job.trigger.some((triggerId) => pipeline.triggers[triggerId]?.type === "manual"))
|
|
271
1240
|
.map((job) => job.id)
|
|
@@ -278,6 +1247,10 @@ function buildRenderModel(pipeline, options) {
|
|
|
278
1247
|
manualDispatchJobs.push(bridge.job);
|
|
279
1248
|
manualDispatchJobs.sort((left, right) => left.localeCompare(right));
|
|
280
1249
|
}
|
|
1250
|
+
if (contract.enabled) {
|
|
1251
|
+
manualDispatchJobs.push(contract.job);
|
|
1252
|
+
manualDispatchJobs.sort((left, right) => left.localeCompare(right));
|
|
1253
|
+
}
|
|
281
1254
|
const nodeVersion = pipeline.sync.github.nodeVersion ?? DEFAULT_NODE_VERSION;
|
|
282
1255
|
const runtime = resolveRuntimeSpecs(pipeline.sync.github.runtime, options.projectKind, nodeVersion);
|
|
283
1256
|
const setup = resolveGitHubSetup(pipeline.sync.github.setup, options.packageManager, options.packageManagerVersion);
|
|
@@ -329,6 +1302,7 @@ function buildRenderModel(pipeline, options) {
|
|
|
329
1302
|
evidence,
|
|
330
1303
|
sourceImpact,
|
|
331
1304
|
attest: pipeline.sync.github.attest,
|
|
1305
|
+
contract,
|
|
332
1306
|
bridge,
|
|
333
1307
|
pages,
|
|
334
1308
|
manualDispatchJobs
|
|
@@ -347,6 +1321,30 @@ function resolveGitHubEvidence(pipeline) {
|
|
|
347
1321
|
}
|
|
348
1322
|
return config;
|
|
349
1323
|
}
|
|
1324
|
+
function resolveGitHubContract(pipeline, generated) {
|
|
1325
|
+
const config = pipeline.sync.github.contract;
|
|
1326
|
+
if (!config.enabled)
|
|
1327
|
+
return config;
|
|
1328
|
+
const jobId = config.job.toLowerCase();
|
|
1329
|
+
if (Object.keys(pipeline.jobs).some((id) => id.toLowerCase() === jobId)) {
|
|
1330
|
+
throw pipelineError("ASYNC_PIPELINE_GITHUB_CONTRACT_JOB_CONFLICT", `sync.github.contract.job "${config.job}" conflicts with an existing pipeline job. Remove the explicit job or set sync.github.contract.job to a different id.`);
|
|
1331
|
+
}
|
|
1332
|
+
const generatedJobs = new Set();
|
|
1333
|
+
if (pipeline.sync.github.packagePreviews.enabled)
|
|
1334
|
+
generatedJobs.add("package-preview");
|
|
1335
|
+
if (pipeline.sync.github.dependabotAutoMerge.enabled)
|
|
1336
|
+
generatedJobs.add("dependabot-auto-merge");
|
|
1337
|
+
if (pipeline.sync.github.evidence.enabled)
|
|
1338
|
+
generatedJobs.add(pipeline.sync.github.evidence.job);
|
|
1339
|
+
if (generated.pages.enabled)
|
|
1340
|
+
generatedJobs.add(generated.pages.job);
|
|
1341
|
+
if (generated.bridge.actionsJob.enabled)
|
|
1342
|
+
generatedJobs.add(generated.bridge.job);
|
|
1343
|
+
if ([...generatedJobs].some((id) => id.toLowerCase() === jobId)) {
|
|
1344
|
+
throw pipelineError("ASYNC_PIPELINE_GITHUB_CONTRACT_JOB_CONFLICT", `sync.github.contract.job "${config.job}" conflicts with a generated GitHub job. Set sync.github.contract.job to a different id.`);
|
|
1345
|
+
}
|
|
1346
|
+
return config;
|
|
1347
|
+
}
|
|
350
1348
|
function resolveGitHubSourceImpactJobs(pipeline, cwd, jobs) {
|
|
351
1349
|
const config = pipeline.sync.github.sourceImpact;
|
|
352
1350
|
if (!config.enabled)
|
|
@@ -359,8 +1357,9 @@ function resolveGitHubSourceImpactJobs(pipeline, cwd, jobs) {
|
|
|
359
1357
|
"dependabot-auto-merge",
|
|
360
1358
|
"async-bridge",
|
|
361
1359
|
pipeline.sync.github.evidence.job,
|
|
1360
|
+
pipeline.sync.github.contract.enabled ? pipeline.sync.github.contract.job : "",
|
|
362
1361
|
pipeline.sync.github.pages.job
|
|
363
|
-
].map((id) => id.toLowerCase()));
|
|
1362
|
+
].filter(Boolean).map((id) => id.toLowerCase()));
|
|
364
1363
|
const existingJobIds = new Set(Object.keys(pipeline.jobs).map((id) => id.toLowerCase()));
|
|
365
1364
|
const result = [];
|
|
366
1365
|
for (const jobId of selectedJobIds) {
|
|
@@ -457,6 +1456,16 @@ function addPullRequestTrigger(triggers, event) {
|
|
|
457
1456
|
types: [...new Set([...existingTypes, "opened", "reopened", "synchronize", "ready_for_review"])].sort()
|
|
458
1457
|
});
|
|
459
1458
|
}
|
|
1459
|
+
function addReleasePublishedTrigger(triggers) {
|
|
1460
|
+
const existing = triggers.release && typeof triggers.release === "object" && !Array.isArray(triggers.release)
|
|
1461
|
+
? triggers.release
|
|
1462
|
+
: {};
|
|
1463
|
+
const existingTypes = Array.isArray(existing.types) ? existing.types.filter((value) => typeof value === "string") : [];
|
|
1464
|
+
triggers.release = sortObject({
|
|
1465
|
+
...existing,
|
|
1466
|
+
types: [...new Set([...existingTypes, "published"])].sort()
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
460
1469
|
function addGitHubEventTrigger(triggers, event) {
|
|
461
1470
|
if (triggers[event] === undefined) {
|
|
462
1471
|
triggers[event] = {};
|
|
@@ -581,6 +1590,9 @@ function renderWorkflow(model) {
|
|
|
581
1590
|
if (model.bridge.actionsJob.enabled) {
|
|
582
1591
|
renderBridgeJob(lines, model);
|
|
583
1592
|
}
|
|
1593
|
+
if (model.contract.enabled) {
|
|
1594
|
+
renderContractJob(lines, model);
|
|
1595
|
+
}
|
|
584
1596
|
if (model.evidence.enabled) {
|
|
585
1597
|
renderEvidenceFanInJob(lines, model);
|
|
586
1598
|
}
|
|
@@ -764,7 +1776,8 @@ function renderEvidenceCollectStep(lines, model, options = {}) {
|
|
|
764
1776
|
if (!model.evidence.enabled)
|
|
765
1777
|
return;
|
|
766
1778
|
const suffix = options.matrix ? "${{ github.job }}-${{ strategy.job-index }}" : "${{ github.job }}";
|
|
767
|
-
|
|
1779
|
+
const paths = [...new Set([...model.evidence.paths, ...(options.extraPaths ?? [])])];
|
|
1780
|
+
lines.push("", " - name: Collect evidence manifest", " if: ${{ always() }}", ` uses: ${ASYNC_EVIDENCE_ACTION}`, " with:", " mode: collect", " paths: |", ...paths.map((path) => ` ${path}`), ...(model.evidence.receiptPaths.length > 0
|
|
768
1781
|
? [
|
|
769
1782
|
" receipt-paths: |",
|
|
770
1783
|
...model.evidence.receiptPaths.map((path) => ` ${path}`)
|
|
@@ -1171,6 +2184,32 @@ function renderBridgePullStep(lines, bridge) {
|
|
|
1171
2184
|
].map(shellWord).join(" ");
|
|
1172
2185
|
lines.push("", " - name: Pull and apply Async bridge change sets", ` uses: ${ASYNC_RUN_ACTION}`, " with:", ` command: ${JSON.stringify(command)}`, " check-generated: false", " artifact-name: async-bridge-${{ github.run_id }}", " env:", " CI: true", ` ASYNC_PROJECT_URL: \${{ vars.${bridge.endpointVar} }}`, ` ASYNC_PROJECT_TOKEN: \${{ secrets.${bridge.tokenEnv} }}`, " GITHUB_REPOSITORY: ${{ github.repository }}", " GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}");
|
|
1173
2186
|
}
|
|
2187
|
+
function renderContractJob(lines, model) {
|
|
2188
|
+
const contract = model.contract;
|
|
2189
|
+
lines.push(` ${yamlKey(contract.job)}:`, ` name: ${contract.job}`, ` if: ${renderContractCondition(contract)}`, " runs-on: ubuntu-latest", " permissions:", " contents: read", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
|
|
2190
|
+
if (model.buildCommand) {
|
|
2191
|
+
lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
|
|
2192
|
+
}
|
|
2193
|
+
renderContractActionStep(lines, contract);
|
|
2194
|
+
renderEvidenceCollectStep(lines, model, { extraPaths: [contract.evidenceDir] });
|
|
2195
|
+
lines.push("");
|
|
2196
|
+
}
|
|
2197
|
+
function renderContractActionStep(lines, contract) {
|
|
2198
|
+
lines.push("", " - name: Run contract evidence", ` uses: ${ASYNC_CONTRACT_ACTION}`, " with:", ` mode: ${contract.mode}`, ` checks: ${JSON.stringify(contractChecks(contract).join(","))}`, ` package-path: ${JSON.stringify(contract.packagePath)}`, ...(contract.schema.enabled
|
|
2199
|
+
? [
|
|
2200
|
+
" schema-sources: |",
|
|
2201
|
+
...contract.schema.sources.map((source) => ` ${source}`),
|
|
2202
|
+
` schema-output: ${JSON.stringify(contract.schema.output)}`
|
|
2203
|
+
]
|
|
2204
|
+
: []), ` evidence-dir: ${JSON.stringify(contract.evidenceDir)}`, ` annotations: ${contract.annotations ? "true" : "false"}`, ` fail-on: ${contract.mode === "report" ? "advisory" : "blocking"}`);
|
|
2205
|
+
}
|
|
2206
|
+
function renderContractCondition(contract) {
|
|
2207
|
+
const manual = `github.event_name == 'workflow_dispatch' && github.event.inputs.job == '${escapeExpressionString(contract.job)}'`;
|
|
2208
|
+
if (contract.mode === "release") {
|
|
2209
|
+
return `(github.event_name == 'release' && github.event.action == 'published') || (${manual})`;
|
|
2210
|
+
}
|
|
2211
|
+
return `(github.event_name == 'pull_request' && github.event.pull_request.draft == false) || (${manual})`;
|
|
2212
|
+
}
|
|
1174
2213
|
function renderEvidenceFanInJob(lines, model) {
|
|
1175
2214
|
const needs = evidenceProducerJobIds(model);
|
|
1176
2215
|
if (needs.length === 0)
|
|
@@ -1190,6 +2229,8 @@ function evidenceProducerJobIds(model) {
|
|
|
1190
2229
|
ids.add("package-preview");
|
|
1191
2230
|
if (model.bridge.actionsJob.enabled)
|
|
1192
2231
|
ids.add(model.bridge.job);
|
|
2232
|
+
if (model.contract.enabled)
|
|
2233
|
+
ids.add(model.contract.job);
|
|
1193
2234
|
ids.delete(model.evidence.job);
|
|
1194
2235
|
return [...ids].sort((left, right) => left.localeCompare(right));
|
|
1195
2236
|
}
|