@async/pipeline 0.9.10 → 0.9.12
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/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 +144 -0
- package/dist/internal/node/github.d.ts.map +1 -1
- package/dist/internal/node/github.js +887 -0
- package/dist/internal/node/github.js.map +1 -1
- package/package.json +1 -1
|
@@ -241,6 +241,893 @@ export function jobsForGitHubEvent(pipeline, context) {
|
|
|
241
241
|
}
|
|
242
242
|
return matches.sort((left, right) => left.id.localeCompare(right.id));
|
|
243
243
|
}
|
|
244
|
+
export async function planGitHubJobs(pipeline, options) {
|
|
245
|
+
const rendered = await renderGitHubWorkflow(pipeline, options);
|
|
246
|
+
const packageInfo = await readPackageInfo(options.cwd);
|
|
247
|
+
const model = buildRenderModel(pipeline, {
|
|
248
|
+
...packageInfo,
|
|
249
|
+
cwd: options.cwd,
|
|
250
|
+
configPath: relativePath(options.cwd, options.configPath),
|
|
251
|
+
workflowPath: rendered.workflowPath
|
|
252
|
+
});
|
|
253
|
+
const event = manifestEventFromOptions(options);
|
|
254
|
+
const network = options.network ?? "mock";
|
|
255
|
+
const candidates = buildManifestCandidates(pipeline, model, rendered, event, network);
|
|
256
|
+
const selected = options.job
|
|
257
|
+
? candidates.filter((candidate) => candidate.manifest.job.id === options.job)
|
|
258
|
+
: candidates.filter((candidate) => candidate.selected);
|
|
259
|
+
if (options.job && selected.length === 0) {
|
|
260
|
+
throw pipelineError("ASYNC_PIPELINE_GITHUB_PLAN_UNKNOWN_JOB", `Unknown generated GitHub job "${options.job}".`);
|
|
261
|
+
}
|
|
262
|
+
const selectedIds = new Set(selected.map((candidate) => candidate.manifest.job.id));
|
|
263
|
+
return {
|
|
264
|
+
version: 1,
|
|
265
|
+
generatedBy: "@async/pipeline",
|
|
266
|
+
workflow: rendered.workflowPath,
|
|
267
|
+
lock: rendered.lockPath,
|
|
268
|
+
event,
|
|
269
|
+
manifests: selected.map((candidate) => candidate.manifest),
|
|
270
|
+
skippedJobs: candidates
|
|
271
|
+
.filter((candidate) => !selectedIds.has(candidate.manifest.job.id))
|
|
272
|
+
.map((candidate) => ({
|
|
273
|
+
id: candidate.manifest.job.id,
|
|
274
|
+
reason: candidate.skipReason || (options.job ? "job_filter" : "event_filter"),
|
|
275
|
+
trigger: candidate.manifest.job.trigger
|
|
276
|
+
}))
|
|
277
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export async function runGitHubLocalPlan(pipeline, options) {
|
|
281
|
+
const plan = await planGitHubJobs(pipeline, options);
|
|
282
|
+
if (plan.manifests.length === 0) {
|
|
283
|
+
return { status: "skipped", plan, receipts: [] };
|
|
284
|
+
}
|
|
285
|
+
const receipts = [];
|
|
286
|
+
for (const manifest of plan.manifests) {
|
|
287
|
+
receipts.push(await runGitHubLocalManifest(manifest, options.cwd, {
|
|
288
|
+
env: options.env ?? process.env,
|
|
289
|
+
dryRun: options.dryRun ?? false
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
const status = options.dryRun
|
|
293
|
+
? "planned"
|
|
294
|
+
: receipts.some((receipt) => receipt.status === "failed")
|
|
295
|
+
? "failed"
|
|
296
|
+
: "passed";
|
|
297
|
+
return { status, plan, receipts };
|
|
298
|
+
}
|
|
299
|
+
export async function runGitHubLocalManifest(manifest, cwd, options = {}) {
|
|
300
|
+
const env = options.env ?? process.env;
|
|
301
|
+
const dryRun = options.dryRun ?? false;
|
|
302
|
+
const stepReceipts = [];
|
|
303
|
+
const issues = [];
|
|
304
|
+
for (const step of manifest.steps) {
|
|
305
|
+
const stepIssues = validateLocalStep(manifest, step, env);
|
|
306
|
+
issues.push(...stepIssues.map((issue) => `${step.id}: ${issue}`));
|
|
307
|
+
const blockedByNetwork = step.local.networked && manifest.local.network === "deny";
|
|
308
|
+
const status = dryRun ? "planned" : stepIssues.length > 0 ? "failed" : "passed";
|
|
309
|
+
stepReceipts.push({
|
|
310
|
+
id: step.id,
|
|
311
|
+
name: step.name,
|
|
312
|
+
contract: step.local.contract,
|
|
313
|
+
status,
|
|
314
|
+
decision: dryRun
|
|
315
|
+
? "planned"
|
|
316
|
+
: blockedByNetwork
|
|
317
|
+
? "denied"
|
|
318
|
+
: manifest.local.network === "allow" && step.local.networked
|
|
319
|
+
? "allowed"
|
|
320
|
+
: step.local.mode === "shell"
|
|
321
|
+
? "simulated"
|
|
322
|
+
: "mocked",
|
|
323
|
+
issues: stepIssues
|
|
324
|
+
});
|
|
325
|
+
if (stepIssues.length > 0 && !dryRun)
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const receipt = {
|
|
329
|
+
job: manifest.job.id,
|
|
330
|
+
status: dryRun ? "planned" : issues.length > 0 ? "failed" : "passed",
|
|
331
|
+
dryRun,
|
|
332
|
+
network: manifest.local.network,
|
|
333
|
+
artifacts: manifest.artifacts,
|
|
334
|
+
stepReceipts,
|
|
335
|
+
issues
|
|
336
|
+
};
|
|
337
|
+
if (!dryRun) {
|
|
338
|
+
const jobDir = resolve(cwd, manifest.local.stateDirectory);
|
|
339
|
+
await mkdir(join(jobDir, "steps"), { recursive: true });
|
|
340
|
+
await mkdir(join(jobDir, "outputs"), { recursive: true });
|
|
341
|
+
await mkdir(join(jobDir, "artifacts"), { recursive: true });
|
|
342
|
+
await writeFile(join(jobDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
343
|
+
for (const [index, step] of manifest.steps.entries()) {
|
|
344
|
+
await writeFile(join(jobDir, "steps", `${String(index + 1).padStart(2, "0")}-${step.id}.json`), `${JSON.stringify(step, null, 2)}\n`, "utf8");
|
|
345
|
+
}
|
|
346
|
+
for (const artifact of manifest.artifacts) {
|
|
347
|
+
await mkdir(join(jobDir, "artifacts", safeArtifactPart(artifact.name)), { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
receipt.manifestPath = relativePath(cwd, join(jobDir, "manifest.json"));
|
|
350
|
+
await writeFile(join(jobDir, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`, "utf8");
|
|
351
|
+
}
|
|
352
|
+
return receipt;
|
|
353
|
+
}
|
|
354
|
+
function manifestEventFromOptions(options) {
|
|
355
|
+
const name = options.eventName ?? "workflow_dispatch";
|
|
356
|
+
const selectedJob = options.selectedJob ?? (name === "workflow_dispatch" ? options.job : undefined);
|
|
357
|
+
return {
|
|
358
|
+
name,
|
|
359
|
+
...(options.eventAction ? { action: options.eventAction } : {}),
|
|
360
|
+
ref: options.ref ?? (name === "push" ? "refs/heads/main" : undefined),
|
|
361
|
+
sha: options.sha,
|
|
362
|
+
actor: options.actor,
|
|
363
|
+
schedule: options.schedule,
|
|
364
|
+
selectedJob,
|
|
365
|
+
pullRequest: name === "pull_request" || name === "pull_request_target"
|
|
366
|
+
? {
|
|
367
|
+
number: options.prNumber,
|
|
368
|
+
headRepo: options.headRepo,
|
|
369
|
+
headSha: options.headSha,
|
|
370
|
+
baseRef: options.baseRef
|
|
371
|
+
}
|
|
372
|
+
: undefined
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function eventContextFromManifestEvent(event) {
|
|
376
|
+
return {
|
|
377
|
+
eventName: event.name,
|
|
378
|
+
action: event.action,
|
|
379
|
+
ref: event.ref,
|
|
380
|
+
baseRef: event.pullRequest?.baseRef,
|
|
381
|
+
schedule: event.schedule,
|
|
382
|
+
selectedJob: event.selectedJob
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function buildManifestCandidates(pipeline, model, rendered, event, network) {
|
|
386
|
+
const selectedPipelineJobs = new Set(jobsForGitHubEvent(pipeline, eventContextFromManifestEvent(event)).map((job) => job.id));
|
|
387
|
+
const candidates = [];
|
|
388
|
+
for (const job of model.jobs) {
|
|
389
|
+
const selected = selectedPipelineJobs.has(job.id);
|
|
390
|
+
candidates.push({
|
|
391
|
+
manifest: buildPipelineJobManifest(model, rendered, event, job, network),
|
|
392
|
+
selected,
|
|
393
|
+
skipReason: selected ? "" : skipReasonForJob(event, job.trigger)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
for (const generated of buildGeneratedJobManifests(model, rendered, event, network, selectedPipelineJobs)) {
|
|
397
|
+
candidates.push(generated);
|
|
398
|
+
}
|
|
399
|
+
return candidates.sort((left, right) => left.manifest.job.id.localeCompare(right.manifest.job.id));
|
|
400
|
+
}
|
|
401
|
+
function buildPipelineJobManifest(model, rendered, event, job, network) {
|
|
402
|
+
const lifecyclePlan = resolveLifecycleJobPlan(model, job);
|
|
403
|
+
const permissions = manifestJobPermissions(model, job, lifecyclePlan);
|
|
404
|
+
const runnerMatrix = job.github?.runsOnMatrix;
|
|
405
|
+
const steps = [
|
|
406
|
+
checkoutStep(),
|
|
407
|
+
...setupManifestSteps(model),
|
|
408
|
+
...dependencyInstallManifestSteps(model),
|
|
409
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
410
|
+
...taskCacheManifestSteps(model, { kind: "job", id: job.id }),
|
|
411
|
+
...(lifecyclePlan ? lifecycleManifestSteps(model, job, lifecyclePlan) : [
|
|
412
|
+
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")
|
|
413
|
+
]),
|
|
414
|
+
...attestManifestSteps(model, lifecyclePlan, job.requires?.provenance === true),
|
|
415
|
+
...agentEvidenceManifestSteps(model, job),
|
|
416
|
+
...taskCacheSaveManifestSteps(model, { kind: "job", id: job.id }),
|
|
417
|
+
...(job.github?.pages ? [pagesActionManifestStep("upload-pages-artifact", "Upload Pages artifact", job.github.pages)] : []),
|
|
418
|
+
...evidenceCollectManifestSteps(model)
|
|
419
|
+
];
|
|
420
|
+
return makeJobManifest(model, rendered, event, {
|
|
421
|
+
id: job.id,
|
|
422
|
+
kind: "pipeline",
|
|
423
|
+
target: job.target,
|
|
424
|
+
runsOn: runnerMatrix && runnerMatrix.length > 0 ? "${{ matrix.runner }}" : job.github?.runsOn ?? "ubuntu-latest",
|
|
425
|
+
matrix: runnerMatrix ? runnerMatrix.map((runner, index) => ({ runner: Array.isArray(runner) ? runner : [runner], index })) : undefined,
|
|
426
|
+
permissions,
|
|
427
|
+
environment: job.environment ?? job.github?.environment ?? null,
|
|
428
|
+
concurrency: null,
|
|
429
|
+
if: job.if ?? null,
|
|
430
|
+
trigger: job.trigger,
|
|
431
|
+
steps,
|
|
432
|
+
network
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function buildGeneratedJobManifests(model, rendered, event, network, selectedPipelineJobs) {
|
|
436
|
+
const candidates = [];
|
|
437
|
+
if (model.pages.enabled && model.pages.target) {
|
|
438
|
+
const selected = generatedPagesSelected(model.pages, event);
|
|
439
|
+
candidates.push({
|
|
440
|
+
manifest: buildGeneratedPagesManifest(model, rendered, event, network),
|
|
441
|
+
selected,
|
|
442
|
+
skipReason: selected ? "" : skipReasonForGeneratedJob(event, [model.pages.job])
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (model.packagePreviews.enabled && model.packagePreviews.target) {
|
|
446
|
+
const selected = event.name === "pull_request";
|
|
447
|
+
candidates.push({
|
|
448
|
+
manifest: buildPackagePreviewManifest(model, rendered, event, network),
|
|
449
|
+
selected,
|
|
450
|
+
skipReason: selected ? "" : "event_filter"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
if (model.bridge.actionsJob.enabled) {
|
|
454
|
+
const selected = bridgeSelected(model.bridge, event);
|
|
455
|
+
candidates.push({
|
|
456
|
+
manifest: buildBridgeManifest(model, rendered, event, network),
|
|
457
|
+
selected,
|
|
458
|
+
skipReason: selected ? "" : "event_filter"
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (model.dependabotAutoMerge.enabled) {
|
|
462
|
+
const selected = event.name === "pull_request_target";
|
|
463
|
+
candidates.push({
|
|
464
|
+
manifest: buildDependabotManifest(model, rendered, event, network),
|
|
465
|
+
selected,
|
|
466
|
+
skipReason: selected ? "" : "event_filter"
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
for (const sourceJob of model.sourceImpactJobs) {
|
|
470
|
+
const selected = selectedPipelineJobs.has(sourceJob.job);
|
|
471
|
+
candidates.push({
|
|
472
|
+
manifest: buildSourceImpactPlanManifest(model, rendered, event, sourceJob, network),
|
|
473
|
+
selected,
|
|
474
|
+
skipReason: selected ? "" : "upstream_job_skipped"
|
|
475
|
+
});
|
|
476
|
+
candidates.push({
|
|
477
|
+
manifest: buildSourceImpactMatrixManifest(model, rendered, event, sourceJob, network),
|
|
478
|
+
selected,
|
|
479
|
+
skipReason: selected ? "" : "upstream_job_skipped"
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (model.evidence.enabled) {
|
|
483
|
+
const producerIds = new Set(evidenceProducerJobIds(model));
|
|
484
|
+
const selected = candidates.some((candidate) => candidate.selected && producerIds.has(candidate.manifest.job.id))
|
|
485
|
+
|| [...selectedPipelineJobs].some((jobId) => producerIds.has(jobId));
|
|
486
|
+
candidates.push({
|
|
487
|
+
manifest: buildEvidenceFanInManifest(model, rendered, event, network),
|
|
488
|
+
selected,
|
|
489
|
+
skipReason: selected ? "" : "no_evidence_producers"
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return candidates;
|
|
493
|
+
}
|
|
494
|
+
function makeJobManifest(model, rendered, event, options) {
|
|
495
|
+
const stateDirectory = `.async/github-local/jobs/${safeArtifactPart(options.id)}`;
|
|
496
|
+
return {
|
|
497
|
+
version: 1,
|
|
498
|
+
generatedBy: "@async/pipeline",
|
|
499
|
+
repo: "${{ github.repository }}",
|
|
500
|
+
workflow: rendered.workflowPath,
|
|
501
|
+
lock: rendered.lockPath,
|
|
502
|
+
event,
|
|
503
|
+
job: {
|
|
504
|
+
id: options.id,
|
|
505
|
+
name: options.id,
|
|
506
|
+
kind: options.kind,
|
|
507
|
+
target: options.target,
|
|
508
|
+
runsOn: options.runsOn,
|
|
509
|
+
...(options.matrix ? { matrix: options.matrix } : {}),
|
|
510
|
+
permissions: options.permissions,
|
|
511
|
+
environment: options.environment,
|
|
512
|
+
concurrency: options.concurrency,
|
|
513
|
+
if: options.if,
|
|
514
|
+
trigger: options.trigger
|
|
515
|
+
},
|
|
516
|
+
steps: options.steps,
|
|
517
|
+
trust: {
|
|
518
|
+
actionRefsPinned: ACTION_LOCKS.every((action) => /^[0-9a-f]{40}$/iu.test(action.sha)),
|
|
519
|
+
workflow: rendered.workflowPath,
|
|
520
|
+
lock: rendered.lockPath,
|
|
521
|
+
lifecycleFallbackReason: lifecycleFallbackReason(options.steps)
|
|
522
|
+
},
|
|
523
|
+
artifacts: artifactsForSteps(options.id, options.steps, model.evidence.retentionDays, model.evidence.ifNoFilesFound),
|
|
524
|
+
local: {
|
|
525
|
+
workspace: ".",
|
|
526
|
+
stateDirectory,
|
|
527
|
+
network: options.network,
|
|
528
|
+
permissionsMode: "enforced",
|
|
529
|
+
mocks: [
|
|
530
|
+
"setup",
|
|
531
|
+
"run",
|
|
532
|
+
"pages",
|
|
533
|
+
"preview",
|
|
534
|
+
"publish",
|
|
535
|
+
"storage-bridge",
|
|
536
|
+
"release",
|
|
537
|
+
"contract",
|
|
538
|
+
"hygiene",
|
|
539
|
+
"comment",
|
|
540
|
+
"evidence",
|
|
541
|
+
"agent-evidence",
|
|
542
|
+
"source-impact",
|
|
543
|
+
"cache",
|
|
544
|
+
"attest"
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function lifecycleFallbackReason(steps) {
|
|
550
|
+
return steps.find((step) => step.local.fallbackReason)?.local.fallbackReason ?? null;
|
|
551
|
+
}
|
|
552
|
+
function manifestJobPermissions(model, job, lifecyclePlan) {
|
|
553
|
+
const grants = job.github?.permissions;
|
|
554
|
+
const idToken = grants?.idToken ?? (job.requires?.provenance || attestRequiresOidc(model, lifecyclePlan) ? "write" : undefined);
|
|
555
|
+
const issues = grants?.issues;
|
|
556
|
+
const packages = grants?.packages;
|
|
557
|
+
const pullRequests = grants?.pullRequests;
|
|
558
|
+
const contents = grants?.contents ?? ((idToken || issues || packages || pullRequests) ? "read" : "read");
|
|
559
|
+
return cleanPermissions({
|
|
560
|
+
contents,
|
|
561
|
+
...(idToken ? { "id-token": idToken } : {}),
|
|
562
|
+
...(issues ? { issues } : {}),
|
|
563
|
+
...(packages ? { packages } : {}),
|
|
564
|
+
...(pullRequests ? { "pull-requests": pullRequests } : {})
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
function cleanPermissions(permissions) {
|
|
568
|
+
return Object.fromEntries(Object.entries(permissions).filter((entry) => entry[1] === "read" || entry[1] === "write"));
|
|
569
|
+
}
|
|
570
|
+
function checkoutStep() {
|
|
571
|
+
return actionManifestStep("checkout", "Checkout", CHECKOUT_ACTION, {}, "checkout");
|
|
572
|
+
}
|
|
573
|
+
function setupManifestSteps(model) {
|
|
574
|
+
const runtime = model.runtime.map((entry) => entry.spec);
|
|
575
|
+
if (model.setup === "async") {
|
|
576
|
+
return [
|
|
577
|
+
actionManifestStep("setup-async-runtimes", "Setup Async runtimes", ASYNC_SETUP_ACTION, {
|
|
578
|
+
"node-version": model.nodeVersion,
|
|
579
|
+
"pnpm-version": pnpmSetupVersion(model.packageManager, model.packageManagerVersion),
|
|
580
|
+
"npm-version": "11.16.0",
|
|
581
|
+
runtime: runtime.length > 1 ? runtime.join("\n") : runtime[0] ?? `node@${model.nodeVersion}`,
|
|
582
|
+
"package-manager": model.packageManager,
|
|
583
|
+
install: model.projectKind === "package",
|
|
584
|
+
"frozen-lockfile": true,
|
|
585
|
+
cache: model.dependencyCache,
|
|
586
|
+
...(model.dependencyCachePath ? { "dependency-cache-path": model.dependencyCachePath } : {})
|
|
587
|
+
}, "setup")
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
const steps = [actionManifestStep("setup-node", "Setup Node", SETUP_NODE_ACTION, { "node-version": model.nodeVersion }, "setup")];
|
|
591
|
+
if (model.projectKind === "deno") {
|
|
592
|
+
steps.push(actionManifestStep("setup-deno", "Setup Deno", DENO_SETUP_ACTION, { "deno-version": `v${DEFAULT_DENO_VERSION}.x` }, "setup"));
|
|
593
|
+
}
|
|
594
|
+
return steps;
|
|
595
|
+
}
|
|
596
|
+
function dependencyInstallManifestSteps(model) {
|
|
597
|
+
if (model.setup === "async")
|
|
598
|
+
return [];
|
|
599
|
+
return renderDependencyInstallSteps(model).map((_, index) => {
|
|
600
|
+
const command = model.projectKind === "deno"
|
|
601
|
+
? `deno install --frozen=${model.dependencyCachePath ? "true" : "false"}`
|
|
602
|
+
: `${model.packageManager} install --frozen-lockfile`;
|
|
603
|
+
return shellManifestStep(`install-dependencies-${index}`, "Install dependencies", command, "setup");
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function taskCacheManifestSteps(model, target) {
|
|
607
|
+
if (!model.taskCache)
|
|
608
|
+
return [];
|
|
609
|
+
const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
|
|
610
|
+
return [
|
|
611
|
+
shellManifestStep("write-task-cache-manifest", "Write task cache manifest", renderCacheManifestCommand(model, target, manifestPath, "read-only"), "cache"),
|
|
612
|
+
actionManifestStep("restore-async-task-cache", "Restore Async task cache", ASYNC_CACHE_ACTION, {
|
|
613
|
+
mode: "restore",
|
|
614
|
+
manifest: manifestPath,
|
|
615
|
+
trust: "read-only"
|
|
616
|
+
}, "cache")
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
function taskCacheSaveManifestSteps(model, target) {
|
|
620
|
+
if (!model.taskCache)
|
|
621
|
+
return [];
|
|
622
|
+
const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
|
|
623
|
+
return [
|
|
624
|
+
actionManifestStep("save-async-task-cache", "Save Async task cache", ASYNC_CACHE_ACTION, {
|
|
625
|
+
mode: "save",
|
|
626
|
+
manifest: manifestPath,
|
|
627
|
+
trust: "read-write"
|
|
628
|
+
}, "cache", { if: "${{ success() && github.event_name != 'pull_request' && steps.async-cache-restore.outputs.cache-hit != 'true' }}" })
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
function lifecycleManifestSteps(model, job, plan) {
|
|
632
|
+
const steps = [];
|
|
633
|
+
if (hasReleaseLifecycle(plan)) {
|
|
634
|
+
const packagePath = lifecyclePackagePath(plan) ?? ".";
|
|
635
|
+
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));
|
|
636
|
+
}
|
|
637
|
+
for (const item of plan) {
|
|
638
|
+
if (item.kind === "run-task") {
|
|
639
|
+
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` }));
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (item.kind === "preview") {
|
|
643
|
+
steps.push(previewManifestStep(item));
|
|
644
|
+
if (item.mode === "pr" && item.comment)
|
|
645
|
+
steps.push(commentManifestStep("comment-package-preview", "Comment package preview"));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (item.kind === "release") {
|
|
649
|
+
steps.push(releaseDoctorManifestStep("run-release-doctor", "Run release doctor", "doctor", item.packagePath, true));
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
steps.push(publishManifestStep(item, job.requires?.provenance === true));
|
|
653
|
+
}
|
|
654
|
+
return steps;
|
|
655
|
+
}
|
|
656
|
+
function attestManifestSteps(model, lifecyclePlan, provenance) {
|
|
657
|
+
if (!model.attest.enabled || !hasReleaseLifecycle(lifecyclePlan))
|
|
658
|
+
return [];
|
|
659
|
+
const packagePath = model.attest.packagePath ?? lifecyclePackagePath(lifecyclePlan) ?? ".";
|
|
660
|
+
const steps = [
|
|
661
|
+
actionManifestStep("create-attestation-subject-manifest", "Create attestation subject manifest", ASYNC_ATTEST_ACTION, {
|
|
662
|
+
mode: "digest",
|
|
663
|
+
"package-path": packagePath,
|
|
664
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
665
|
+
"sbom-path": model.attest.sbomPath,
|
|
666
|
+
"require-npm-provenance": model.attest.requireNpmProvenance,
|
|
667
|
+
"tarball-scan": model.attest.tarballScan
|
|
668
|
+
}, "attest"),
|
|
669
|
+
actionManifestStep("write-attestation-sbom-evidence", "Write attestation SBOM evidence", ASYNC_ATTEST_ACTION, {
|
|
670
|
+
mode: "sbom",
|
|
671
|
+
"package-path": packagePath,
|
|
672
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
673
|
+
"sbom-path": model.attest.sbomPath
|
|
674
|
+
}, "attest")
|
|
675
|
+
];
|
|
676
|
+
if (model.attest.githubAttestation || provenance) {
|
|
677
|
+
steps.push(actionManifestStep("record-github-attestation-intent", "Record GitHub attestation intent", ASYNC_ATTEST_ACTION, {
|
|
678
|
+
mode: "attest",
|
|
679
|
+
"package-path": packagePath,
|
|
680
|
+
"subject-manifest": model.attest.subjectManifest,
|
|
681
|
+
"github-attestation": true
|
|
682
|
+
}, "attest", { permissions: { "id-token": "write" }, networked: true, dangerous: true }));
|
|
683
|
+
}
|
|
684
|
+
return steps;
|
|
685
|
+
}
|
|
686
|
+
function agentEvidenceManifestSteps(model, job) {
|
|
687
|
+
const evidence = agentEvidenceForTargets(model.tasks, job.target);
|
|
688
|
+
if (!evidence.hasAgentStep)
|
|
689
|
+
return [];
|
|
690
|
+
const canComment = job.github?.permissions?.issues === "write" || job.github?.permissions?.pullRequests === "write";
|
|
691
|
+
const steps = [
|
|
692
|
+
actionManifestStep("bundle-agent-evidence", "Bundle agent evidence", ASYNC_AGENT_EVIDENCE_ACTION, {
|
|
693
|
+
mode: canComment ? "comment" : "bundle",
|
|
694
|
+
"run-directory": ".async/runs",
|
|
695
|
+
outputs: evidence.outputs,
|
|
696
|
+
"evidence-path": ".async/actions/agent-evidence/${{ github.job }}/manifest.json",
|
|
697
|
+
"bundle-path": ".async/actions/agent-evidence/${{ github.job }}/bundle.json",
|
|
698
|
+
"receipt-path": ".async/actions/receipts/${{ github.job }}-agent-evidence.json",
|
|
699
|
+
comment: canComment,
|
|
700
|
+
"comment-marker": "async-agent-evidence-${{ github.job }}"
|
|
701
|
+
}, "agent-evidence")
|
|
702
|
+
];
|
|
703
|
+
if (canComment) {
|
|
704
|
+
steps.push(commentManifestStep("comment-agent-evidence", "Comment agent evidence"));
|
|
705
|
+
}
|
|
706
|
+
return steps;
|
|
707
|
+
}
|
|
708
|
+
function evidenceCollectManifestSteps(model) {
|
|
709
|
+
if (!model.evidence.enabled)
|
|
710
|
+
return [];
|
|
711
|
+
return [
|
|
712
|
+
actionManifestStep("collect-evidence-manifest", "Collect evidence manifest", ASYNC_EVIDENCE_ACTION, {
|
|
713
|
+
mode: "collect",
|
|
714
|
+
paths: model.evidence.paths,
|
|
715
|
+
"receipt-paths": model.evidence.receiptPaths,
|
|
716
|
+
"manifest-path": ".async/evidence/${{ github.job }}/manifest.json",
|
|
717
|
+
"summary-path": ".async/evidence/${{ github.job }}/summary.md",
|
|
718
|
+
"artifact-name": `${model.evidence.artifactNamePrefix}-\${{ github.job }}`,
|
|
719
|
+
"retention-days": model.evidence.retentionDays,
|
|
720
|
+
"if-no-files-found": model.evidence.ifNoFilesFound,
|
|
721
|
+
"include-summary": model.evidence.includeSummary
|
|
722
|
+
}, "evidence", { if: "${{ always() }}" })
|
|
723
|
+
];
|
|
724
|
+
}
|
|
725
|
+
function runActionManifestStep(id, name, command, env, contract, extraWith = {}) {
|
|
726
|
+
const networked = commandLooksNetworked(command);
|
|
727
|
+
return actionManifestStep(id, name, ASYNC_RUN_ACTION, {
|
|
728
|
+
command,
|
|
729
|
+
"check-generated": false,
|
|
730
|
+
"artifact-name": "async-pipeline-${{ github.job }}-runs",
|
|
731
|
+
...extraWith
|
|
732
|
+
}, contract, { env: manifestEnv(env), secrets: secretNamesFromEnv(env), networked, dangerous: networked });
|
|
733
|
+
}
|
|
734
|
+
function releaseDoctorManifestStep(id, name, mode, packagePath, live = false) {
|
|
735
|
+
return actionManifestStep(id, name, ASYNC_DOCTOR_ACTION, {
|
|
736
|
+
mode,
|
|
737
|
+
"package-path": packagePath,
|
|
738
|
+
"evidence-dir": ".async/release",
|
|
739
|
+
"release-command": ASYNC_RELEASE_COMMAND,
|
|
740
|
+
...(mode === "doctor" ? { network: live ? "live" : "mock" } : {})
|
|
741
|
+
}, "release", { networked: live, dangerous: live, secrets: live ? ["GITHUB_TOKEN"] : [] });
|
|
742
|
+
}
|
|
743
|
+
function previewManifestStep(preview) {
|
|
744
|
+
return actionManifestStep(`publish-${preview.mode}-package-preview`, `Publish ${preview.mode === "main" ? "main" : "PR"} package preview`, ASYNC_PREVIEW_ACTION, {
|
|
745
|
+
"package-path": preview.packagePath,
|
|
746
|
+
"target-registry": preview.registry,
|
|
747
|
+
...(preview.namespace ? { namespace: preview.namespace } : {}),
|
|
748
|
+
mode: preview.mode,
|
|
749
|
+
comment: preview.comment,
|
|
750
|
+
"token-env-name": preview.tokenEnv
|
|
751
|
+
}, "preview", { permissions: { packages: "write" }, secrets: [preview.tokenEnv], networked: true, dangerous: true });
|
|
752
|
+
}
|
|
753
|
+
function publishManifestStep(publish, provenance) {
|
|
754
|
+
const name = publish.mode === "github-release"
|
|
755
|
+
? "Create or update GitHub Release"
|
|
756
|
+
: publish.mode === "github-packages"
|
|
757
|
+
? "Publish GitHub Packages mirror"
|
|
758
|
+
: "Publish npm package";
|
|
759
|
+
return actionManifestStep(`publish-${publish.mode}`, name, ASYNC_PUBLISH_ACTION, {
|
|
760
|
+
"package-path": publish.packagePath,
|
|
761
|
+
mode: publish.mode,
|
|
762
|
+
registry: publish.registry,
|
|
763
|
+
"dist-tag": publish.distTag,
|
|
764
|
+
...(publish.mode === "npm" ? { "token-env-name": "NODE_AUTH_TOKEN", provenance } : {}),
|
|
765
|
+
...(publish.mode === "github-packages" ? { "token-env-name": "GITHUB_TOKEN" } : {}),
|
|
766
|
+
...(publish.mode === "github-release" ? { "notes-file": ".async/release/release-notes.md" } : {})
|
|
767
|
+
}, "publish", {
|
|
768
|
+
permissions: {
|
|
769
|
+
...(publish.mode === "github-packages" || publish.mode === "github-release" ? { packages: "write", contents: "write" } : {}),
|
|
770
|
+
...(provenance ? { "id-token": "write" } : {})
|
|
771
|
+
},
|
|
772
|
+
secrets: publish.mode === "npm" ? ["NODE_AUTH_TOKEN"] : ["GITHUB_TOKEN"],
|
|
773
|
+
networked: true,
|
|
774
|
+
dangerous: true
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function commentManifestStep(id, name) {
|
|
778
|
+
return actionManifestStep(id, name, ASYNC_COMMENT_ACTION, {
|
|
779
|
+
mode: "pr-comment",
|
|
780
|
+
repository: "${{ github.repository }}",
|
|
781
|
+
number: "${{ github.event.pull_request.number }}",
|
|
782
|
+
marker: "${{ steps.source.outputs.comment-marker }}",
|
|
783
|
+
body: "${{ steps.source.outputs.comment-body }}"
|
|
784
|
+
}, "comment", { permissions: { issues: "write" }, secrets: ["GITHUB_TOKEN"], networked: true });
|
|
785
|
+
}
|
|
786
|
+
function pagesActionManifestStep(id, name, pages) {
|
|
787
|
+
return actionManifestStep(id, name, ASYNC_PAGES_ACTION, {
|
|
788
|
+
mode: pages.build.kind,
|
|
789
|
+
...(pages.build.kind === "jekyll" ? { source: pages.build.source, destination: pages.build.destination ?? "./_site" } : {}),
|
|
790
|
+
...(pages.build.kind === "static" ? { path: pages.build.path } : {}),
|
|
791
|
+
...(pages.build.kind === "prerender" ? { path: pages.build.path, "validate-index": pages.build.validateIndex ?? true, "spa-fallback": pages.build.spaFallback ?? false } : {})
|
|
792
|
+
}, "pages", { permissions: { pages: "write", "id-token": "write" } });
|
|
793
|
+
}
|
|
794
|
+
function actionManifestStep(id, name, ref, input, contract, options = {}) {
|
|
795
|
+
const action = actionFromRef(ref);
|
|
796
|
+
return {
|
|
797
|
+
id: safeGeneratedJobId(id),
|
|
798
|
+
name,
|
|
799
|
+
uses: `${action.uses}@${action.sha}`,
|
|
800
|
+
label: action.label,
|
|
801
|
+
...(options.if ? { if: options.if } : {}),
|
|
802
|
+
with: sortObject(input),
|
|
803
|
+
...(options.env && Object.keys(options.env).length > 0 ? { env: options.env } : {}),
|
|
804
|
+
permissions: options.permissions ?? {},
|
|
805
|
+
secrets: [...new Set(options.secrets ?? [])].sort(),
|
|
806
|
+
local: {
|
|
807
|
+
contract,
|
|
808
|
+
mode: "action",
|
|
809
|
+
network: options.networked ? "mock" : "mock",
|
|
810
|
+
networked: options.networked ?? false,
|
|
811
|
+
dangerous: options.dangerous ?? false,
|
|
812
|
+
...(options.networked ? { mockReason: "networked action is mocked unless --network allow is selected" } : {}),
|
|
813
|
+
...(options.fallbackReason ? { fallbackReason: options.fallbackReason } : {})
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function shellManifestStep(id, name, command, contract) {
|
|
818
|
+
return {
|
|
819
|
+
id: safeGeneratedJobId(id),
|
|
820
|
+
name,
|
|
821
|
+
run: command,
|
|
822
|
+
secrets: [],
|
|
823
|
+
permissions: {},
|
|
824
|
+
local: {
|
|
825
|
+
contract,
|
|
826
|
+
mode: "shell",
|
|
827
|
+
network: "mock",
|
|
828
|
+
networked: commandLooksNetworked(command),
|
|
829
|
+
dangerous: commandLooksDangerous(command),
|
|
830
|
+
...(commandLooksNetworked(command) ? { mockReason: "networked shell command is simulated unless --network allow is selected" } : {})
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function actionFromRef(ref) {
|
|
835
|
+
const found = GENERATED_ACTIONS.find((action) => action.ref === ref);
|
|
836
|
+
if (found)
|
|
837
|
+
return found;
|
|
838
|
+
const [usesPart, rest] = ref.split("@");
|
|
839
|
+
const sha = rest?.split(/\s+/u)[0] ?? "";
|
|
840
|
+
return { id: usesPart ?? ref, uses: usesPart ?? ref, sha, label: "", ref };
|
|
841
|
+
}
|
|
842
|
+
function manifestEnv(env) {
|
|
843
|
+
return Object.fromEntries(Object.entries(env)
|
|
844
|
+
.map(([name, value]) => [name, renderGitHubEnvValue(value)])
|
|
845
|
+
.filter((entry) => typeof entry[1] === "string")
|
|
846
|
+
.sort(([left], [right]) => left.localeCompare(right)));
|
|
847
|
+
}
|
|
848
|
+
function secretNamesFromEnv(env) {
|
|
849
|
+
const names = new Set();
|
|
850
|
+
for (const [envName, value] of Object.entries(env)) {
|
|
851
|
+
if (isSecretEnvValue(value)) {
|
|
852
|
+
names.add(typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ? value.name : envName);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return [...names].sort();
|
|
856
|
+
}
|
|
857
|
+
function artifactsForSteps(jobId, steps, retentionDays, ifNoFilesFound) {
|
|
858
|
+
const artifacts = [];
|
|
859
|
+
for (const step of steps) {
|
|
860
|
+
const artifactName = typeof step.with?.["artifact-name"] === "string" ? step.with["artifact-name"] : undefined;
|
|
861
|
+
if (artifactName) {
|
|
862
|
+
artifacts.push({
|
|
863
|
+
name: artifactName,
|
|
864
|
+
path: artifactPathForContract(step.local.contract),
|
|
865
|
+
mode: step.local.contract === "evidence" ? "upload" : "local",
|
|
866
|
+
producerJob: jobId,
|
|
867
|
+
retentionDays,
|
|
868
|
+
ifNoFilesFound
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return artifacts.sort((left, right) => left.name.localeCompare(right.name));
|
|
873
|
+
}
|
|
874
|
+
function artifactPathForContract(contract) {
|
|
875
|
+
if (contract === "evidence")
|
|
876
|
+
return ".async/evidence";
|
|
877
|
+
if (contract === "agent-evidence")
|
|
878
|
+
return ".async/actions/agent-evidence";
|
|
879
|
+
if (contract === "pages")
|
|
880
|
+
return ".async/pages";
|
|
881
|
+
return ".async/runs";
|
|
882
|
+
}
|
|
883
|
+
function buildGeneratedPagesManifest(model, rendered, event, network) {
|
|
884
|
+
const pages = model.pages;
|
|
885
|
+
const steps = [
|
|
886
|
+
checkoutStep(),
|
|
887
|
+
...setupManifestSteps(model),
|
|
888
|
+
...dependencyInstallManifestSteps(model),
|
|
889
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
890
|
+
...taskCacheManifestSteps(model, { kind: "task", id: pages.target ?? "docs.site" }),
|
|
891
|
+
runActionManifestStep("run-pages-target", "Run Pages target", `${model.command} github check && ${model.command} run-task ${shellWord(pages.target ?? "docs.site")}`, {}, "run"),
|
|
892
|
+
...taskCacheSaveManifestSteps(model, { kind: "task", id: pages.target ?? "docs.site" }),
|
|
893
|
+
pagesActionManifestStep("upload-pages-artifact", "Upload Pages artifact", pages),
|
|
894
|
+
...evidenceCollectManifestSteps(model)
|
|
895
|
+
];
|
|
896
|
+
return makeJobManifest(model, rendered, event, {
|
|
897
|
+
id: pages.job,
|
|
898
|
+
kind: "generated",
|
|
899
|
+
target: pages.target ? [pages.target] : [],
|
|
900
|
+
runsOn: "ubuntu-latest",
|
|
901
|
+
permissions: { contents: "read", pages: "write", "id-token": "write" },
|
|
902
|
+
environment: pages.environment ?? null,
|
|
903
|
+
concurrency: null,
|
|
904
|
+
if: renderGeneratedPagesCondition(pages),
|
|
905
|
+
trigger: ["pull_request", "push", "workflow_dispatch"],
|
|
906
|
+
steps,
|
|
907
|
+
network
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
function buildPackagePreviewManifest(model, rendered, event, network) {
|
|
911
|
+
const preview = model.packagePreviews;
|
|
912
|
+
const steps = [
|
|
913
|
+
checkoutStep(),
|
|
914
|
+
...setupManifestSteps(model),
|
|
915
|
+
...dependencyInstallManifestSteps(model),
|
|
916
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
917
|
+
...taskCacheManifestSteps(model, { kind: "task", id: preview.target ?? "pack", manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" }),
|
|
918
|
+
runActionManifestStep("run-package-preview-target", "Run package preview target", `${model.command} github check && ${model.command} run-task ${shellWord(preview.target ?? "pack")}`, {}, "run"),
|
|
919
|
+
...taskCacheSaveManifestSteps(model, { kind: "task", id: preview.target ?? "pack", manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" }),
|
|
920
|
+
actionManifestStep("publish-package-preview", "Publish package preview", ASYNC_PREVIEW_ACTION, {
|
|
921
|
+
"package-path": preview.package ?? ".",
|
|
922
|
+
"target-registry": preview.registry,
|
|
923
|
+
...(preview.namespace ? { namespace: preview.namespace } : {}),
|
|
924
|
+
mode: "pr",
|
|
925
|
+
comment: preview.comment,
|
|
926
|
+
"token-env-name": preview.tokenEnv
|
|
927
|
+
}, "preview", { permissions: { packages: "write" }, secrets: [preview.tokenEnv], networked: true, dangerous: true }),
|
|
928
|
+
...(preview.comment ? [commentManifestStep("comment-package-preview", "Comment package preview")] : []),
|
|
929
|
+
...evidenceCollectManifestSteps(model)
|
|
930
|
+
];
|
|
931
|
+
return makeJobManifest(model, rendered, event, {
|
|
932
|
+
id: "package-preview",
|
|
933
|
+
kind: "generated",
|
|
934
|
+
target: preview.target ? [preview.target] : [],
|
|
935
|
+
runsOn: "ubuntu-latest",
|
|
936
|
+
permissions: { contents: "read", issues: "write", packages: "write", "pull-requests": "write" },
|
|
937
|
+
environment: null,
|
|
938
|
+
concurrency: null,
|
|
939
|
+
if: "github.event_name == 'pull_request' && github.event.pull_request.draft == false",
|
|
940
|
+
trigger: ["pull_request"],
|
|
941
|
+
steps,
|
|
942
|
+
network
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
function buildBridgeManifest(model, rendered, event, network) {
|
|
946
|
+
const bridge = model.bridge;
|
|
947
|
+
const command = [
|
|
948
|
+
"npx",
|
|
949
|
+
"--yes",
|
|
950
|
+
`@async/github-app@${bridge.packageVersion}`,
|
|
951
|
+
"actions",
|
|
952
|
+
"pull",
|
|
953
|
+
"--branch-prefix",
|
|
954
|
+
bridge.branchPrefix,
|
|
955
|
+
"--pull-request",
|
|
956
|
+
String(bridge.pullRequest),
|
|
957
|
+
...bridge.allowedPaths.flatMap((path) => ["--allowed-path", path])
|
|
958
|
+
].map(shellWord).join(" ");
|
|
959
|
+
const steps = [
|
|
960
|
+
checkoutStep(),
|
|
961
|
+
...setupManifestSteps(model),
|
|
962
|
+
...dependencyInstallManifestSteps(model),
|
|
963
|
+
...(model.buildCommand ? [shellManifestStep("build-pipeline-cli", "Build pipeline CLI", model.buildCommand, "build")] : []),
|
|
964
|
+
runActionManifestStep("check-generated-workflow", "Check generated workflow", `${model.command} github check`, {}, "run"),
|
|
965
|
+
runActionManifestStep("pull-and-apply-async-bridge-change-sets", "Pull and apply Async bridge change sets", command, {
|
|
966
|
+
ASYNC_PROJECT_URL: { kind: "async-pipeline.env.var", name: bridge.endpointVar },
|
|
967
|
+
ASYNC_PROJECT_TOKEN: { kind: "async-pipeline.env.secret", name: bridge.tokenEnv },
|
|
968
|
+
GITHUB_TOKEN: { kind: "async-pipeline.env.secret", name: "GITHUB_TOKEN" }
|
|
969
|
+
}, "storage-bridge"),
|
|
970
|
+
...evidenceCollectManifestSteps(model)
|
|
971
|
+
];
|
|
972
|
+
return makeJobManifest(model, rendered, event, {
|
|
973
|
+
id: bridge.job,
|
|
974
|
+
kind: "generated",
|
|
975
|
+
target: [],
|
|
976
|
+
runsOn: "ubuntu-latest",
|
|
977
|
+
permissions: { contents: "write", "pull-requests": "write" },
|
|
978
|
+
environment: null,
|
|
979
|
+
concurrency: "async-bridge-${{ github.repository }}",
|
|
980
|
+
if: renderBridgeCondition(bridge),
|
|
981
|
+
trigger: ["schedule", "workflow_dispatch"],
|
|
982
|
+
steps,
|
|
983
|
+
network
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
function buildEvidenceFanInManifest(model, rendered, event, network) {
|
|
987
|
+
return makeJobManifest(model, rendered, event, {
|
|
988
|
+
id: model.evidence.job,
|
|
989
|
+
kind: "generated",
|
|
990
|
+
target: [],
|
|
991
|
+
runsOn: "ubuntu-latest",
|
|
992
|
+
permissions: { contents: "read" },
|
|
993
|
+
environment: null,
|
|
994
|
+
concurrency: null,
|
|
995
|
+
if: "always()",
|
|
996
|
+
trigger: ["fan-in"],
|
|
997
|
+
steps: [
|
|
998
|
+
actionManifestStep("merge-evidence-manifests", "Merge evidence manifests", ASYNC_EVIDENCE_ACTION, {
|
|
999
|
+
mode: "merge",
|
|
1000
|
+
"artifact-pattern": `${model.evidence.artifactNamePrefix}-*`,
|
|
1001
|
+
"manifest-path": ".async/evidence/index.json",
|
|
1002
|
+
"summary-path": ".async/evidence/index.md",
|
|
1003
|
+
"artifact-name": `${model.evidence.artifactNamePrefix}-index`,
|
|
1004
|
+
"retention-days": model.evidence.retentionDays,
|
|
1005
|
+
"if-no-files-found": model.evidence.ifNoFilesFound,
|
|
1006
|
+
"include-summary": model.evidence.includeSummary
|
|
1007
|
+
}, "evidence")
|
|
1008
|
+
],
|
|
1009
|
+
network
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function buildDependabotManifest(model, rendered, event, network) {
|
|
1013
|
+
return makeJobManifest(model, rendered, event, {
|
|
1014
|
+
id: "dependabot-auto-merge",
|
|
1015
|
+
kind: "generated",
|
|
1016
|
+
target: [],
|
|
1017
|
+
runsOn: "ubuntu-latest",
|
|
1018
|
+
permissions: { contents: "write", "pull-requests": "write" },
|
|
1019
|
+
environment: null,
|
|
1020
|
+
concurrency: null,
|
|
1021
|
+
if: "github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.draft == false",
|
|
1022
|
+
trigger: ["pull_request_target"],
|
|
1023
|
+
steps: [
|
|
1024
|
+
actionManifestStep("fetch-dependabot-metadata", "Fetch Dependabot metadata", DEPENDABOT_FETCH_METADATA_ACTION, { "github-token": "${{ secrets.GITHUB_TOKEN }}" }, "dependabot", { secrets: ["GITHUB_TOKEN"], networked: true }),
|
|
1025
|
+
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 })
|
|
1026
|
+
],
|
|
1027
|
+
network
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
function buildSourceImpactPlanManifest(model, rendered, event, sourceJob, network) {
|
|
1031
|
+
return makeJobManifest(model, rendered, event, {
|
|
1032
|
+
id: sourceJob.planJob,
|
|
1033
|
+
kind: "generated",
|
|
1034
|
+
target: [],
|
|
1035
|
+
runsOn: sourceJob.github?.runsOn ?? "ubuntu-latest",
|
|
1036
|
+
permissions: { contents: "read" },
|
|
1037
|
+
environment: null,
|
|
1038
|
+
concurrency: null,
|
|
1039
|
+
if: sourceJob.if ?? null,
|
|
1040
|
+
trigger: ["source-impact"],
|
|
1041
|
+
steps: [
|
|
1042
|
+
checkoutStep(),
|
|
1043
|
+
...setupManifestSteps(model),
|
|
1044
|
+
shellManifestStep("write-generated-source-plan", "Write generated source plan", `write ${sourceJob.planPath}`, "source-impact"),
|
|
1045
|
+
actionManifestStep("plan-source-impact-matrix", "Plan source impact matrix", ASYNC_SOURCE_IMPACT_ACTION, {
|
|
1046
|
+
mode: "matrix",
|
|
1047
|
+
"plan-path": sourceJob.planPath,
|
|
1048
|
+
"matrix-path": `.async/actions/source-impact/${safeArtifactPart(sourceJob.job)}-matrix.json`
|
|
1049
|
+
}, "source-impact")
|
|
1050
|
+
],
|
|
1051
|
+
network
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function buildSourceImpactMatrixManifest(model, rendered, event, sourceJob, network) {
|
|
1055
|
+
return makeJobManifest(model, rendered, event, {
|
|
1056
|
+
id: sourceJob.matrixJob,
|
|
1057
|
+
kind: "generated",
|
|
1058
|
+
target: [sourceJob.job],
|
|
1059
|
+
runsOn: sourceJob.github?.runsOn ?? "ubuntu-latest",
|
|
1060
|
+
matrix: sourceJob.plan.matrix.include.map((entry, index) => ({ runner: [String(entry.source)], index })),
|
|
1061
|
+
permissions: { contents: "read" },
|
|
1062
|
+
environment: null,
|
|
1063
|
+
concurrency: null,
|
|
1064
|
+
if: sourceJob.if ?? null,
|
|
1065
|
+
trigger: ["source-impact"],
|
|
1066
|
+
steps: [
|
|
1067
|
+
checkoutStep(),
|
|
1068
|
+
...setupManifestSteps(model),
|
|
1069
|
+
actionManifestStep("validate-source-impact-row", "Validate source impact row", ASYNC_SOURCE_IMPACT_ACTION, {
|
|
1070
|
+
mode: "validate",
|
|
1071
|
+
source: "${{ matrix.source }}"
|
|
1072
|
+
}, "source-impact"),
|
|
1073
|
+
runActionManifestStep("run-source-impact-task", "Run source impact task", `${model.command} github check && ${model.command} run-task ${shellWord(sourceJob.job)}`, sourceJob.env, "run"),
|
|
1074
|
+
...evidenceCollectManifestSteps(model)
|
|
1075
|
+
],
|
|
1076
|
+
network
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
function generatedPagesSelected(pages, event) {
|
|
1080
|
+
if (event.name === "pull_request")
|
|
1081
|
+
return pages.triggers.pullRequest;
|
|
1082
|
+
if (event.name === "push")
|
|
1083
|
+
return Boolean(pages.triggers.main);
|
|
1084
|
+
return event.name === "workflow_dispatch" && event.selectedJob === pages.job;
|
|
1085
|
+
}
|
|
1086
|
+
function bridgeSelected(bridge, event) {
|
|
1087
|
+
if (event.name === "schedule")
|
|
1088
|
+
return Boolean(bridge.schedule && (!event.schedule || event.schedule === bridge.schedule));
|
|
1089
|
+
return event.name === "workflow_dispatch" && event.selectedJob === bridge.job;
|
|
1090
|
+
}
|
|
1091
|
+
function skipReasonForJob(event, trigger) {
|
|
1092
|
+
if (event.name === "workflow_dispatch" && !event.selectedJob && trigger.some((id) => id === "manual"))
|
|
1093
|
+
return "manual_selector_missing";
|
|
1094
|
+
return "event_filter";
|
|
1095
|
+
}
|
|
1096
|
+
function skipReasonForGeneratedJob(event, manualIds) {
|
|
1097
|
+
if (event.name === "workflow_dispatch" && !event.selectedJob && manualIds.length > 0)
|
|
1098
|
+
return "manual_selector_missing";
|
|
1099
|
+
return "event_filter";
|
|
1100
|
+
}
|
|
1101
|
+
function validateLocalStep(manifest, step, env) {
|
|
1102
|
+
const issues = [];
|
|
1103
|
+
for (const [permission, required] of Object.entries(step.permissions ?? {})) {
|
|
1104
|
+
const actual = manifest.job.permissions[permission];
|
|
1105
|
+
if (!permissionAllows(actual, required)) {
|
|
1106
|
+
issues.push(`requires ${permission}: ${required}, but job grants ${actual ?? "none"}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (step.local.networked && manifest.local.network === "deny") {
|
|
1110
|
+
issues.push("networked step is denied by --network deny");
|
|
1111
|
+
}
|
|
1112
|
+
if (step.local.networked && manifest.local.network === "allow") {
|
|
1113
|
+
for (const secret of step.secrets) {
|
|
1114
|
+
if (!env[secret])
|
|
1115
|
+
issues.push(`requires secret env ${secret} for --network allow`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return issues;
|
|
1119
|
+
}
|
|
1120
|
+
function permissionAllows(actual, required) {
|
|
1121
|
+
if (required === "read")
|
|
1122
|
+
return actual === "read" || actual === "write";
|
|
1123
|
+
return actual === "write";
|
|
1124
|
+
}
|
|
1125
|
+
function commandLooksNetworked(command) {
|
|
1126
|
+
return /\b(?:gh|npm\s+publish|npx|curl|wget|git\s+(?:push|fetch|pull|clone))\b/u.test(command);
|
|
1127
|
+
}
|
|
1128
|
+
function commandLooksDangerous(command) {
|
|
1129
|
+
return /\b(?:publish|push|release|comment|merge|pull-request)\b/u.test(command);
|
|
1130
|
+
}
|
|
244
1131
|
function buildRenderModel(pipeline, options) {
|
|
245
1132
|
const usedTriggerIds = new Set(Object.values(pipeline.jobs).flatMap((job) => job.trigger));
|
|
246
1133
|
const usedTriggers = Object.fromEntries([...usedTriggerIds].sort().map((triggerId) => [triggerId, pipeline.triggers[triggerId]]));
|