@async/pipeline 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,10 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { githubConfigForJob, pipelineError } from "../core/index.js";
6
+ import { sourceImpactPlanForJob } from "./sources.js";
6
7
  export const GITHUB_WORKFLOW_PATH = ".github/workflows/async-pipeline.yml";
7
8
  export const GITHUB_LOCK_PATH = ".github/async-pipeline.lock.json";
8
- const GENERATOR_VERSION = 14;
9
+ const GENERATOR_VERSION = 16;
9
10
  const DEFAULT_NODE_VERSION = "24";
10
11
  const DEFAULT_DENO_VERSION = "2";
11
12
  const DEFAULT_PNPM_VERSION = "11.1.0";
@@ -19,8 +20,8 @@ function defineActionRef(id, uses, sha, label) {
19
20
  ref: `${uses}@${sha} # ${label}`
20
21
  };
21
22
  }
22
- const ASYNC_ACTIONS_SHA = "313494352cd10207bf0331c83e83364eb45c8e02";
23
- const ASYNC_ACTIONS_LABEL = "v0.1.5";
23
+ const ASYNC_ACTIONS_SHA = "cef0f1a3b7dd1300a16004e6d69b472261a3272f";
24
+ const ASYNC_ACTIONS_LABEL = "v0.1.7";
24
25
  const GENERATED_ACTIONS = [
25
26
  defineActionRef("async.actions.setup", "async/actions/setup", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
26
27
  defineActionRef("async.actions.run", "async/actions/run", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
@@ -28,6 +29,8 @@ const GENERATED_ACTIONS = [
28
29
  defineActionRef("async.actions.preview", "async/actions/preview", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
29
30
  defineActionRef("async.actions.publish", "async/actions/publish", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
30
31
  defineActionRef("async.actions.dependabot-merge", "async/actions/dependabot-merge", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
32
+ defineActionRef("async.actions.evidence", "async/actions/evidence", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
33
+ defineActionRef("async.actions.source-impact", "async/actions/source-impact", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
31
34
  defineActionRef("actions.checkout", "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd", "v6.0.2"),
32
35
  defineActionRef("actions.cache", "actions/cache", "0057852bfaa89a56745cba8c7296529d2fc39830", "v4"),
33
36
  defineActionRef("pnpm.setup", "pnpm/setup", "cf03a9b516e09bc5a90f041fc26fc930c9dc631b", "v1.0.0"),
@@ -44,6 +47,8 @@ const ASYNC_PAGES_ACTION = actionRef("async.actions.pages");
44
47
  const ASYNC_PREVIEW_ACTION = actionRef("async.actions.preview");
45
48
  const ASYNC_PUBLISH_ACTION = actionRef("async.actions.publish");
46
49
  const ASYNC_DEPENDABOT_MERGE_ACTION = actionRef("async.actions.dependabot-merge");
50
+ const ASYNC_EVIDENCE_ACTION = actionRef("async.actions.evidence");
51
+ const ASYNC_SOURCE_IMPACT_ACTION = actionRef("async.actions.source-impact");
47
52
  const CHECKOUT_ACTION = actionRef("actions.checkout");
48
53
  const CACHE_ACTION = actionRef("actions.cache");
49
54
  const PNPM_SETUP_ACTION = actionRef("pnpm.setup");
@@ -63,6 +68,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
63
68
  const packageInfo = await readPackageInfo(options.cwd);
64
69
  const renderModel = buildRenderModel(pipeline, {
65
70
  ...packageInfo,
71
+ cwd: options.cwd,
66
72
  configPath: relativePath(options.cwd, options.configPath),
67
73
  workflowPath
68
74
  });
@@ -85,6 +91,8 @@ export async function renderGitHubWorkflow(pipeline, options) {
85
91
  dependencyCachePath: renderModel.dependencyCachePath,
86
92
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
87
93
  packagePreviews: renderModel.packagePreviews,
94
+ evidence: renderModel.evidence,
95
+ sourceImpact: renderModel.sourceImpact,
88
96
  bridge: renderModel.bridge,
89
97
  pages: renderModel.pages,
90
98
  manualDispatchJobs: renderModel.manualDispatchJobs,
@@ -112,6 +120,8 @@ export async function renderGitHubWorkflow(pipeline, options) {
112
120
  dependencyCachePath: renderModel.dependencyCachePath,
113
121
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
114
122
  packagePreviews: renderModel.packagePreviews,
123
+ evidence: renderModel.evidence,
124
+ sourceImpact: renderModel.sourceImpact,
115
125
  bridge: renderModel.bridge,
116
126
  pages: renderModel.pages,
117
127
  manualDispatchJobs: renderModel.manualDispatchJobs
@@ -231,6 +241,7 @@ function buildRenderModel(pipeline, options) {
231
241
  if (packagePreviews.enabled) {
232
242
  addPullRequestTrigger(triggers, "pull_request");
233
243
  }
244
+ const evidence = resolveGitHubEvidence(pipeline);
234
245
  const pages = resolveGitHubPages(pipeline);
235
246
  const bridge = resolveGitHubBridge(pipeline);
236
247
  if (pages.enabled) {
@@ -259,25 +270,38 @@ function buildRenderModel(pipeline, options) {
259
270
  const nodeVersion = pipeline.sync.github.nodeVersion ?? DEFAULT_NODE_VERSION;
260
271
  const runtime = resolveRuntimeSpecs(pipeline.sync.github.runtime, options.projectKind, nodeVersion);
261
272
  const setup = resolveGitHubSetup(pipeline.sync.github.setup, options.packageManager, options.packageManagerVersion);
273
+ const jobs = Object.values(pipeline.jobs)
274
+ .map((job) => ({
275
+ id: job.id,
276
+ target: [...job.target],
277
+ trigger: [...job.trigger],
278
+ env: { ...pipeline.env, ...(job.env ?? {}) },
279
+ environment: job.environment,
280
+ requires: job.requires,
281
+ execution: job.execution,
282
+ github: githubConfigForJob(pipeline, job),
283
+ if: renderGitHubJobCondition(job, pipeline.triggers)
284
+ }))
285
+ .sort((left, right) => left.id.localeCompare(right.id));
286
+ const sourceImpactJobs = resolveGitHubSourceImpactJobs(pipeline, options.cwd, jobs);
287
+ const sourceImpact = {
288
+ ...pipeline.sync.github.sourceImpact,
289
+ generatedJobs: sourceImpactJobs.map((job) => ({
290
+ job: job.job,
291
+ planJob: job.planJob,
292
+ matrixJob: job.matrixJob,
293
+ matrixRows: job.plan.matrix.include.length,
294
+ sources: Object.keys(job.plan.sources).sort((left, right) => left.localeCompare(right))
295
+ }))
296
+ };
262
297
  return {
263
298
  name: "Async Pipeline",
264
299
  configPath: options.configPath,
265
300
  workflowPath: options.workflowPath,
266
301
  projectKind: options.projectKind,
267
302
  triggers,
268
- jobs: Object.values(pipeline.jobs)
269
- .map((job) => ({
270
- id: job.id,
271
- target: [...job.target],
272
- trigger: [...job.trigger],
273
- env: { ...pipeline.env, ...(job.env ?? {}) },
274
- environment: job.environment,
275
- requires: job.requires,
276
- execution: job.execution,
277
- github: githubConfigForJob(pipeline, job),
278
- if: renderGitHubJobCondition(job, pipeline.triggers)
279
- }))
280
- .sort((left, right) => left.id.localeCompare(right.id)),
303
+ jobs,
304
+ sourceImpactJobs,
281
305
  tasks: pipeline.tasks,
282
306
  packageManager: options.packageManager,
283
307
  packageManagerVersion: options.packageManagerVersion,
@@ -291,11 +315,83 @@ function buildRenderModel(pipeline, options) {
291
315
  dependencyCachePath: pipeline.sync.github.dependencyCache === false ? undefined : options.dependencyCachePath,
292
316
  dependabotAutoMerge: pipeline.sync.github.dependabotAutoMerge,
293
317
  packagePreviews,
318
+ evidence,
319
+ sourceImpact,
294
320
  bridge,
295
321
  pages,
296
322
  manualDispatchJobs
297
323
  };
298
324
  }
325
+ function resolveGitHubEvidence(pipeline) {
326
+ const config = pipeline.sync.github.evidence;
327
+ if (!config.enabled)
328
+ return config;
329
+ if (pipeline.jobs[config.job]) {
330
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_EVIDENCE_JOB_CONFLICT", `sync.github.evidence.job "${config.job}" conflicts with an existing pipeline job. Remove the explicit job or set sync.github.evidence.job to a different id.`);
331
+ }
332
+ const generatedJobs = new Set(["package-preview", "dependabot-auto-merge", pipeline.sync.github.pages.job, "async-bridge"]);
333
+ if (generatedJobs.has(config.job)) {
334
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_EVIDENCE_JOB_CONFLICT", `sync.github.evidence.job "${config.job}" conflicts with a generated GitHub job. Set sync.github.evidence.job to a different id.`);
335
+ }
336
+ return config;
337
+ }
338
+ function resolveGitHubSourceImpactJobs(pipeline, cwd, jobs) {
339
+ const config = pipeline.sync.github.sourceImpact;
340
+ if (!config.enabled)
341
+ return [];
342
+ const jobsById = new Map(jobs.map((job) => [job.id, job]));
343
+ const explicitJobs = new Set(config.jobs);
344
+ const selectedJobIds = config.jobs.length > 0 ? config.jobs : jobs.map((job) => job.id);
345
+ const generatedIds = new Set([
346
+ "package-preview",
347
+ "dependabot-auto-merge",
348
+ "async-bridge",
349
+ pipeline.sync.github.evidence.job,
350
+ pipeline.sync.github.pages.job
351
+ ].map((id) => id.toLowerCase()));
352
+ const existingJobIds = new Set(Object.keys(pipeline.jobs).map((id) => id.toLowerCase()));
353
+ const result = [];
354
+ for (const jobId of selectedJobIds) {
355
+ const job = jobsById.get(jobId);
356
+ if (!job) {
357
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_SOURCE_IMPACT_INVALID", `sync.github.sourceImpact references missing job "${jobId}".`);
358
+ }
359
+ if (job.github?.runsOnMatrix) {
360
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_SOURCE_IMPACT_INVALID", `sync.github.sourceImpact cannot target job "${jobId}" because that job already uses github.runsOnMatrix.`);
361
+ }
362
+ const plan = sourceImpactPlanForJob(pipeline, cwd, jobId);
363
+ if (plan.matrix.include.length === 0) {
364
+ if (explicitJobs.has(jobId)) {
365
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_SOURCE_IMPACT_INVALID", `sync.github.sourceImpact job "${jobId}" has no source task refs.`);
366
+ }
367
+ continue;
368
+ }
369
+ const generatedJobPrefix = safeGeneratedJobId(jobId);
370
+ const planJob = `${generatedJobPrefix}-source-plan`;
371
+ const matrixJob = `${generatedJobPrefix}-sources`;
372
+ for (const generatedJob of [planJob, matrixJob]) {
373
+ const generatedJobKey = generatedJob.toLowerCase();
374
+ if (existingJobIds.has(generatedJobKey)) {
375
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_SOURCE_IMPACT_JOB_CONFLICT", `Generated source-impact job "${generatedJob}" conflicts with an existing pipeline job.`);
376
+ }
377
+ if (generatedIds.has(generatedJobKey)) {
378
+ throw pipelineError("ASYNC_PIPELINE_GITHUB_SOURCE_IMPACT_JOB_CONFLICT", `Generated source-impact job "${generatedJob}" conflicts with another generated GitHub job.`);
379
+ }
380
+ generatedIds.add(generatedJobKey);
381
+ }
382
+ result.push({
383
+ job: jobId,
384
+ planJob,
385
+ matrixJob,
386
+ planPath: `.async/actions/source-impact/${safeArtifactPart(jobId)}-source-plan.json`,
387
+ plan,
388
+ if: job.if,
389
+ github: job.github,
390
+ env: job.env
391
+ });
392
+ }
393
+ return result.sort((left, right) => left.job.localeCompare(right.job));
394
+ }
299
395
  function resolveGitHubBridge(pipeline) {
300
396
  const config = pipeline.sync.github.bridge;
301
397
  const actionsJobEnabled = gitHubBridgeActionsEnabled(config);
@@ -447,6 +543,10 @@ function renderWorkflow(model) {
447
543
  renderPagesDeployJob(lines, job);
448
544
  }
449
545
  }
546
+ for (const sourceJob of model.sourceImpactJobs) {
547
+ renderSourceImpactPlanJob(lines, model, sourceJob);
548
+ renderSourceImpactMatrixJob(lines, model, sourceJob);
549
+ }
450
550
  if (model.pages.enabled) {
451
551
  renderGeneratedPagesJob(lines, model);
452
552
  renderPagesDeployJob(lines, {
@@ -469,6 +569,9 @@ function renderWorkflow(model) {
469
569
  if (model.bridge.actionsJob.enabled) {
470
570
  renderBridgeJob(lines, model);
471
571
  }
572
+ if (model.evidence.enabled) {
573
+ renderEvidenceFanInJob(lines, model);
574
+ }
472
575
  return `${lines.join("\n").replace(/\n+$/u, "")}\n`;
473
576
  }
474
577
  function renderJob(lines, model, job) {
@@ -548,8 +651,48 @@ function renderJob(lines, model, job) {
548
651
  lines.push("");
549
652
  renderPagesBuildSteps(lines, job.github.pages);
550
653
  }
654
+ renderEvidenceCollectStep(lines, model, { matrix: Boolean(runnerMatrix && runnerMatrix.length > 0) });
551
655
  lines.push("");
552
656
  }
657
+ function renderSourceImpactPlanJob(lines, model, sourceJob) {
658
+ const runsOn = sourceJob.github?.runsOn ?? "ubuntu-latest";
659
+ lines.push(` ${yamlKey(sourceJob.planJob)}:`, ` name: ${sourceJob.planJob}`);
660
+ if (sourceJob.if) {
661
+ lines.push(` if: ${sourceJob.if}`);
662
+ }
663
+ lines.push(` runs-on: ${Array.isArray(runsOn) ? JSON.stringify(runsOn) : runsOn}`, " permissions:", " contents: read", " outputs:", " matrix: ${{ steps.source-plan.outputs.matrix }}", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...renderSetupSteps(model));
664
+ renderWriteSourceImpactPlanStep(lines, sourceJob);
665
+ lines.push("", " - name: Plan source impact matrix", " id: source-plan", ` uses: ${ASYNC_SOURCE_IMPACT_ACTION}`, " with:", " mode: plan", ` source-plan: ${sourceJob.planPath}`, " output-matrix: true");
666
+ renderEvidenceCollectStep(lines, model);
667
+ lines.push("");
668
+ }
669
+ function renderSourceImpactMatrixJob(lines, model, sourceJob) {
670
+ const runsOn = sourceJob.github?.runsOn ?? "ubuntu-latest";
671
+ lines.push(` ${yamlKey(sourceJob.matrixJob)}:`, ` name: ${sourceJob.job} source (\${{ matrix.source }}:\${{ matrix.taskId }})`, ` needs: ${JSON.stringify(sourceJob.planJob)}`, ` if: \${{ always() && needs['${sourceJob.planJob}'].result == 'success' }}`, " strategy:", " fail-fast: false", ` matrix: \${{ fromJSON(needs['${sourceJob.planJob}'].outputs.matrix || '{"include":[]}') }}`, ` runs-on: ${Array.isArray(runsOn) ? JSON.stringify(runsOn) : runsOn}`, " permissions:", " contents: read", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
672
+ ? [
673
+ " - name: Restore task cache",
674
+ ` uses: ${CACHE_ACTION}`,
675
+ " with:",
676
+ " path: .async/cache",
677
+ " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
678
+ " restore-keys: |",
679
+ " async-pipeline-${{ runner.os }}-",
680
+ ""
681
+ ]
682
+ : []), ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
683
+ if (model.buildCommand) {
684
+ lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
685
+ }
686
+ renderWriteSourceImpactPlanStep(lines, sourceJob);
687
+ lines.push("", " - name: Validate source checkout", ` uses: ${ASYNC_SOURCE_IMPACT_ACTION}`, " with:", " mode: checkout", ` source-plan: ${sourceJob.planPath}`, " source-id: ${{ matrix.source }}", " ref: ${{ matrix.ref }}", " path: ${{ matrix.path }}", "", " - name: Prepare source checkout", ` uses: ${ASYNC_SOURCE_IMPACT_ACTION}`, " with:", " mode: prepare", ` source-plan: ${sourceJob.planPath}`, " source-id: ${{ matrix.source }}", " path: ${{ matrix.path }}");
688
+ renderRunActionStep(lines, "Run source task", `${model.command} github check && ${model.command} run-task "\${{ matrix.task }}"`, scopeActionEnv(sourceJob.env, new Set()), { artifactName: "async-pipeline-${{ github.job }}-${{ matrix.source }}-${{ matrix.taskId }}-runs" });
689
+ renderEvidenceCollectStep(lines, model, { matrix: true });
690
+ lines.push("");
691
+ }
692
+ function renderWriteSourceImpactPlanStep(lines, sourceJob) {
693
+ const planJson = JSON.stringify(sourceJob.plan, null, 2);
694
+ lines.push("", " - name: Write generated source plan", " run: |", ` mkdir -p ${shellWord(dirname(sourceJob.planPath))}`, ` cat > ${shellWord(sourceJob.planPath)} <<'ASYNC_SOURCE_PLAN'`, ...planJson.split("\n").map((line) => ` ${line}`), " ASYNC_SOURCE_PLAN");
695
+ }
553
696
  function renderGeneratedPagesJob(lines, model) {
554
697
  const pages = model.pages;
555
698
  if (!pages.target)
@@ -572,6 +715,7 @@ function renderGeneratedPagesJob(lines, model) {
572
715
  renderRunActionStep(lines, "Run Pages target", `${model.command} github check && ${model.command} run-task ${shellWord(pages.target)}`, {});
573
716
  lines.push("");
574
717
  renderPagesBuildSteps(lines, pages);
718
+ renderEvidenceCollectStep(lines, model);
575
719
  lines.push("");
576
720
  }
577
721
  function renderGeneratedPagesCondition(pages) {
@@ -605,6 +749,17 @@ function renderRunActionStep(lines, name, command, env, options = {}) {
605
749
  lines.push("", ` - name: ${name}`, ` uses: ${ASYNC_RUN_ACTION}`, " with:", ` command: ${JSON.stringify(command)}`, " check-generated: false", ` artifact-name: ${options.artifactName ?? "async-pipeline-${{ github.job }}-runs"}`);
606
750
  renderActionEnv(lines, env);
607
751
  }
752
+ function renderEvidenceCollectStep(lines, model, options = {}) {
753
+ if (!model.evidence.enabled)
754
+ return;
755
+ const suffix = options.matrix ? "${{ github.job }}-${{ strategy.job-index }}" : "${{ github.job }}";
756
+ lines.push("", " - name: Collect evidence manifest", " if: ${{ always() }}", ` uses: ${ASYNC_EVIDENCE_ACTION}`, " with:", " mode: collect", " paths: |", ...model.evidence.paths.map((path) => ` ${path}`), ...(model.evidence.receiptPaths.length > 0
757
+ ? [
758
+ " receipt-paths: |",
759
+ ...model.evidence.receiptPaths.map((path) => ` ${path}`)
760
+ ]
761
+ : []), ` manifest-path: ".async/evidence/${suffix}/manifest.json"`, ` summary-path: ".async/evidence/${suffix}/summary.md"`, ` artifact-name: ${model.evidence.artifactNamePrefix}-${suffix}`, ` retention-days: ${model.evidence.retentionDays}`, ` if-no-files-found: ${model.evidence.ifNoFilesFound}`, ` include-summary: ${model.evidence.includeSummary ? "true" : "false"}`);
762
+ }
608
763
  function resolveLifecycleJobPlan(model, job) {
609
764
  if (job.execution || job.target.length !== 1)
610
765
  return undefined;
@@ -852,6 +1007,10 @@ function splitShellWords(command) {
852
1007
  function safeArtifactPart(value) {
853
1008
  return value.replace(/[^A-Za-z0-9_.-]+/gu, "-");
854
1009
  }
1010
+ function safeGeneratedJobId(value) {
1011
+ const normalized = value.toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "");
1012
+ return normalized || "job";
1013
+ }
855
1014
  function renderPackagePreviewJob(lines, model) {
856
1015
  const preview = model.packagePreviews;
857
1016
  if (!preview.package || !preview.target)
@@ -872,7 +1031,8 @@ function renderPackagePreviewJob(lines, model) {
872
1031
  lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
873
1032
  }
874
1033
  renderRunActionStep(lines, "Run package preview target", `${model.command} github check && ${model.command} run-task ${shellWord(preview.target)}`, {});
875
- lines.push("", " - name: Publish package preview", ` uses: ${ASYNC_PREVIEW_ACTION}`, " with:", ` package-path: ${JSON.stringify(preview.package)}`, ` target-registry: ${JSON.stringify(preview.registry)}`, ...(preview.namespace ? [` namespace: ${JSON.stringify(preview.namespace)}`] : []), " mode: pr", ` comment: ${preview.comment ? "true" : "false"}`, ` token-env-name: ${JSON.stringify(preview.tokenEnv)}`, " env:", " CI: true", ` ${preview.tokenEnv}: \${{ secrets.${preview.tokenEnv} }}`, "");
1034
+ lines.push("", " - name: Publish package preview", ` uses: ${ASYNC_PREVIEW_ACTION}`, " with:", ` package-path: ${JSON.stringify(preview.package)}`, ` target-registry: ${JSON.stringify(preview.registry)}`, ...(preview.namespace ? [` namespace: ${JSON.stringify(preview.namespace)}`] : []), " mode: pr", ` comment: ${preview.comment ? "true" : "false"}`, ` token-env-name: ${JSON.stringify(preview.tokenEnv)}`, " env:", " CI: true", ` ${preview.tokenEnv}: \${{ secrets.${preview.tokenEnv} }}`);
1035
+ renderEvidenceCollectStep(lines, model);
876
1036
  lines.push("");
877
1037
  }
878
1038
  function renderBridgeJob(lines, model) {
@@ -894,6 +1054,7 @@ function renderBridgeJob(lines, model) {
894
1054
  }
895
1055
  renderRunActionStep(lines, "Check generated workflow", `${model.command} github check`, {});
896
1056
  renderBridgePullStep(lines, bridge);
1057
+ renderEvidenceCollectStep(lines, model);
897
1058
  lines.push("");
898
1059
  }
899
1060
  function renderBridgeCondition(bridge) {
@@ -921,6 +1082,28 @@ function renderBridgePullStep(lines, bridge) {
921
1082
  ].map(shellWord).join(" ");
922
1083
  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 }}");
923
1084
  }
1085
+ function renderEvidenceFanInJob(lines, model) {
1086
+ const needs = evidenceProducerJobIds(model);
1087
+ if (needs.length === 0)
1088
+ return;
1089
+ const evidence = model.evidence;
1090
+ lines.push(` ${yamlKey(evidence.job)}:`, ` name: ${evidence.job}`, ` needs: ${JSON.stringify(needs)}`, " if: always()", " runs-on: ubuntu-latest", " permissions:", " contents: read", " steps:", " - name: Merge evidence manifests", ` uses: ${ASYNC_EVIDENCE_ACTION}`, " with:", " mode: merge", ` artifact-pattern: ${evidence.artifactNamePrefix}-*`, " manifest-path: .async/evidence/index.json", " summary-path: .async/evidence/index.md", ` artifact-name: ${evidence.artifactNamePrefix}-index`, ` retention-days: ${evidence.retentionDays}`, ` if-no-files-found: ${evidence.ifNoFilesFound}`, ` include-summary: ${evidence.includeSummary ? "true" : "false"}`, "");
1091
+ }
1092
+ function evidenceProducerJobIds(model) {
1093
+ const ids = new Set(model.jobs.map((job) => job.id));
1094
+ for (const sourceJob of model.sourceImpactJobs) {
1095
+ ids.add(sourceJob.planJob);
1096
+ ids.add(sourceJob.matrixJob);
1097
+ }
1098
+ if (model.pages.enabled)
1099
+ ids.add(model.pages.job);
1100
+ if (model.packagePreviews.enabled)
1101
+ ids.add("package-preview");
1102
+ if (model.bridge.actionsJob.enabled)
1103
+ ids.add(model.bridge.job);
1104
+ ids.delete(model.evidence.job);
1105
+ return [...ids].sort((left, right) => left.localeCompare(right));
1106
+ }
924
1107
  function renderDependabotAutoMergeJob(lines, ecosystems) {
925
1108
  lines.push(" dependabot-auto-merge:", " name: dependabot-auto-merge", " if: github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.draft == false", " runs-on: ubuntu-latest", " permissions:", " contents: write", " pull-requests: write", " steps:", " - name: Fetch Dependabot metadata", " id: dependabot-metadata", ` uses: ${DEPENDABOT_FETCH_METADATA_ACTION}`, " with:", " github-token: ${{ secrets.GITHUB_TOKEN }}", "", " - name: Merge validated Dependabot PR", ` uses: ${ASYNC_DEPENDABOT_MERGE_ACTION}`, " with:", " pull-request-number: ${{ github.event.pull_request.number }}", " actor: ${{ github.event.pull_request.user.login }}", " dependency-ecosystem: ${{ steps.dependabot-metadata.outputs.package-ecosystem }}", " allowed-ecosystems: |", ...ecosystems.map((ecosystem) => ` ${ecosystem}`), " env:", " GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}", "");
926
1109
  }