@async/pipeline 0.9.4 → 0.9.6

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 = 15;
9
+ const GENERATOR_VERSION = 17;
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 = "e91fb515670a66c0694936c079de4061f6306d43";
23
- const ASYNC_ACTIONS_LABEL = "v0.1.6";
23
+ const ASYNC_ACTIONS_SHA = "1b1a167072f242ed200f8f5ec7cdf10c8a9ae241";
24
+ const ASYNC_ACTIONS_LABEL = "v0.1.8";
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),
@@ -29,8 +30,9 @@ const GENERATED_ACTIONS = [
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),
31
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),
34
+ defineActionRef("async.actions.cache", "async/actions/cache", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
32
35
  defineActionRef("actions.checkout", "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd", "v6.0.2"),
33
- defineActionRef("actions.cache", "actions/cache", "0057852bfaa89a56745cba8c7296529d2fc39830", "v4"),
34
36
  defineActionRef("pnpm.setup", "pnpm/setup", "cf03a9b516e09bc5a90f041fc26fc930c9dc631b", "v1.0.0"),
35
37
  defineActionRef("deno.setup", "denoland/setup-deno", "667a34cdef165d8d2b2e98dde39547c9daac7282", "v2.0.4"),
36
38
  defineActionRef("actions.setup-node", "actions/setup-node", "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e", "v6"),
@@ -46,8 +48,9 @@ const ASYNC_PREVIEW_ACTION = actionRef("async.actions.preview");
46
48
  const ASYNC_PUBLISH_ACTION = actionRef("async.actions.publish");
47
49
  const ASYNC_DEPENDABOT_MERGE_ACTION = actionRef("async.actions.dependabot-merge");
48
50
  const ASYNC_EVIDENCE_ACTION = actionRef("async.actions.evidence");
51
+ const ASYNC_SOURCE_IMPACT_ACTION = actionRef("async.actions.source-impact");
52
+ const ASYNC_CACHE_ACTION = actionRef("async.actions.cache");
49
53
  const CHECKOUT_ACTION = actionRef("actions.checkout");
50
- const CACHE_ACTION = actionRef("actions.cache");
51
54
  const PNPM_SETUP_ACTION = actionRef("pnpm.setup");
52
55
  const DENO_SETUP_ACTION = actionRef("deno.setup");
53
56
  const SETUP_NODE_ACTION = actionRef("actions.setup-node");
@@ -65,6 +68,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
65
68
  const packageInfo = await readPackageInfo(options.cwd);
66
69
  const renderModel = buildRenderModel(pipeline, {
67
70
  ...packageInfo,
71
+ cwd: options.cwd,
68
72
  configPath: relativePath(options.cwd, options.configPath),
69
73
  workflowPath
70
74
  });
@@ -88,6 +92,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
88
92
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
89
93
  packagePreviews: renderModel.packagePreviews,
90
94
  evidence: renderModel.evidence,
95
+ sourceImpact: renderModel.sourceImpact,
91
96
  bridge: renderModel.bridge,
92
97
  pages: renderModel.pages,
93
98
  manualDispatchJobs: renderModel.manualDispatchJobs,
@@ -116,6 +121,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
116
121
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
117
122
  packagePreviews: renderModel.packagePreviews,
118
123
  evidence: renderModel.evidence,
124
+ sourceImpact: renderModel.sourceImpact,
119
125
  bridge: renderModel.bridge,
120
126
  pages: renderModel.pages,
121
127
  manualDispatchJobs: renderModel.manualDispatchJobs
@@ -264,25 +270,38 @@ function buildRenderModel(pipeline, options) {
264
270
  const nodeVersion = pipeline.sync.github.nodeVersion ?? DEFAULT_NODE_VERSION;
265
271
  const runtime = resolveRuntimeSpecs(pipeline.sync.github.runtime, options.projectKind, nodeVersion);
266
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
+ };
267
297
  return {
268
298
  name: "Async Pipeline",
269
299
  configPath: options.configPath,
270
300
  workflowPath: options.workflowPath,
271
301
  projectKind: options.projectKind,
272
302
  triggers,
273
- 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)),
303
+ jobs,
304
+ sourceImpactJobs,
286
305
  tasks: pipeline.tasks,
287
306
  packageManager: options.packageManager,
288
307
  packageManagerVersion: options.packageManagerVersion,
@@ -297,6 +316,7 @@ function buildRenderModel(pipeline, options) {
297
316
  dependabotAutoMerge: pipeline.sync.github.dependabotAutoMerge,
298
317
  packagePreviews,
299
318
  evidence,
319
+ sourceImpact,
300
320
  bridge,
301
321
  pages,
302
322
  manualDispatchJobs
@@ -315,6 +335,63 @@ function resolveGitHubEvidence(pipeline) {
315
335
  }
316
336
  return config;
317
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
+ }
318
395
  function resolveGitHubBridge(pipeline) {
319
396
  const config = pipeline.sync.github.bridge;
320
397
  const actionsJobEnabled = gitHubBridgeActionsEnabled(config);
@@ -466,6 +543,10 @@ function renderWorkflow(model) {
466
543
  renderPagesDeployJob(lines, job);
467
544
  }
468
545
  }
546
+ for (const sourceJob of model.sourceImpactJobs) {
547
+ renderSourceImpactPlanJob(lines, model, sourceJob);
548
+ renderSourceImpactMatrixJob(lines, model, sourceJob);
549
+ }
469
550
  if (model.pages.enabled) {
470
551
  renderGeneratedPagesJob(lines, model);
471
552
  renderPagesDeployJob(lines, {
@@ -538,18 +619,7 @@ function renderJob(lines, model, job) {
538
619
  if (pullRequests)
539
620
  lines.push(` pull-requests: ${pullRequests}`);
540
621
  }
541
- lines.push(" steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
542
- ? [
543
- " - name: Restore task cache",
544
- ` uses: ${CACHE_ACTION}`,
545
- " with:",
546
- " path: .async/cache",
547
- " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
548
- " restore-keys: |",
549
- " async-pipeline-${{ runner.os }}-",
550
- ""
551
- ]
552
- : []), ...renderSetupSteps(model), ...(idToken === "write"
622
+ lines.push(" steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...renderSetupSteps(model), ...(idToken === "write"
553
623
  ? [
554
624
  " - name: Use current npm",
555
625
  " run: npm install -g npm@11.16.0",
@@ -559,6 +629,7 @@ function renderJob(lines, model, job) {
559
629
  if (model.buildCommand) {
560
630
  lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
561
631
  }
632
+ renderTaskCacheRestoreSteps(lines, model, { kind: "job", id: job.id });
562
633
  const lifecyclePlan = resolveLifecycleJobPlan(model, job);
563
634
  if (lifecyclePlan) {
564
635
  renderLifecycleJobPlan(lines, model, job, lifecyclePlan);
@@ -566,6 +637,7 @@ function renderJob(lines, model, job) {
566
637
  else {
567
638
  renderRunActionStep(lines, "Run pipeline job", `${model.command} github check && ${model.command} run ${shellWord(job.id)}${job.execution ? ` --execution ${shellWord(job.execution)}` : ""}`, job.env);
568
639
  }
640
+ renderTaskCacheSaveSteps(lines, model, { kind: "job", id: job.id });
569
641
  if (job.github?.pages) {
570
642
  lines.push("");
571
643
  renderPagesBuildSteps(lines, job.github.pages);
@@ -573,26 +645,71 @@ function renderJob(lines, model, job) {
573
645
  renderEvidenceCollectStep(lines, model, { matrix: Boolean(runnerMatrix && runnerMatrix.length > 0) });
574
646
  lines.push("");
575
647
  }
648
+ function renderSourceImpactPlanJob(lines, model, sourceJob) {
649
+ const runsOn = sourceJob.github?.runsOn ?? "ubuntu-latest";
650
+ lines.push(` ${yamlKey(sourceJob.planJob)}:`, ` name: ${sourceJob.planJob}`);
651
+ if (sourceJob.if) {
652
+ lines.push(` if: ${sourceJob.if}`);
653
+ }
654
+ 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));
655
+ renderWriteSourceImpactPlanStep(lines, sourceJob);
656
+ 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");
657
+ renderEvidenceCollectStep(lines, model);
658
+ lines.push("");
659
+ }
660
+ function renderSourceImpactMatrixJob(lines, model, sourceJob) {
661
+ const runsOn = sourceJob.github?.runsOn ?? "ubuntu-latest";
662
+ 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}`, "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
663
+ if (model.buildCommand) {
664
+ lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
665
+ }
666
+ renderWriteSourceImpactPlanStep(lines, sourceJob);
667
+ 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 }}");
668
+ renderTaskCacheRestoreSteps(lines, model, {
669
+ kind: "task",
670
+ id: "\"${{ matrix.task }}\"",
671
+ manifestPath: ".async/actions/cache/${{ matrix.source }}-${{ matrix.taskId }}-cache-manifest.json"
672
+ });
673
+ 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" });
674
+ renderTaskCacheSaveSteps(lines, model, {
675
+ kind: "task",
676
+ id: "\"${{ matrix.task }}\"",
677
+ manifestPath: ".async/actions/cache/${{ matrix.source }}-${{ matrix.taskId }}-cache-manifest.json"
678
+ });
679
+ renderEvidenceCollectStep(lines, model, { matrix: true });
680
+ lines.push("");
681
+ }
682
+ function renderTaskCacheRestoreSteps(lines, model, target) {
683
+ if (!model.taskCache)
684
+ return;
685
+ const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
686
+ lines.push("", " - name: Write task cache manifest", ` run: ${renderCacheManifestCommand(model, target, manifestPath, "read-only")}`, "", " - name: Restore Async task cache", " id: async-cache-restore", ` uses: ${ASYNC_CACHE_ACTION}`, " with:", " mode: restore", ` manifest: ${manifestPath}`, " trust: read-only");
687
+ }
688
+ function renderTaskCacheSaveSteps(lines, model, target) {
689
+ if (!model.taskCache)
690
+ return;
691
+ const manifestPath = target.manifestPath ?? `.async/actions/cache/${safeArtifactPart(target.id)}-cache-manifest.json`;
692
+ lines.push("", " - name: Save Async task cache", " if: ${{ success() && github.event_name != 'pull_request' && steps.async-cache-restore.outputs.cache-hit != 'true' }}", ` uses: ${ASYNC_CACHE_ACTION}`, " with:", " mode: save", ` manifest: ${manifestPath}`, " trust: read-write");
693
+ }
694
+ function renderCacheManifestCommand(model, target, manifestPath, trust) {
695
+ const targetFlag = target.kind === "job" ? "--job" : "--task";
696
+ return `${model.command} cache manifest ${targetFlag} ${target.id.startsWith("\"") ? target.id : shellWord(target.id)} --output ${shellWord(manifestPath)} --trust ${trust}`;
697
+ }
698
+ function renderWriteSourceImpactPlanStep(lines, sourceJob) {
699
+ const planJson = JSON.stringify(sourceJob.plan, null, 2);
700
+ 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");
701
+ }
576
702
  function renderGeneratedPagesJob(lines, model) {
577
703
  const pages = model.pages;
578
704
  if (!pages.target)
579
705
  return;
580
- lines.push(` ${yamlKey(pages.job)}:`, ` name: ${pages.job}`, ` if: ${renderGeneratedPagesCondition(pages)}`, " runs-on: ubuntu-latest", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
581
- ? [
582
- " - name: Restore task cache",
583
- ` uses: ${CACHE_ACTION}`,
584
- " with:",
585
- " path: .async/cache",
586
- " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
587
- " restore-keys: |",
588
- " async-pipeline-${{ runner.os }}-",
589
- ""
590
- ]
591
- : []), ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
706
+ lines.push(` ${yamlKey(pages.job)}:`, ` name: ${pages.job}`, ` if: ${renderGeneratedPagesCondition(pages)}`, " runs-on: ubuntu-latest", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
592
707
  if (model.buildCommand) {
593
708
  lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
594
709
  }
710
+ renderTaskCacheRestoreSteps(lines, model, { kind: "task", id: pages.target });
595
711
  renderRunActionStep(lines, "Run Pages target", `${model.command} github check && ${model.command} run-task ${shellWord(pages.target)}`, {});
712
+ renderTaskCacheSaveSteps(lines, model, { kind: "task", id: pages.target });
596
713
  lines.push("");
597
714
  renderPagesBuildSteps(lines, pages);
598
715
  renderEvidenceCollectStep(lines, model);
@@ -887,44 +1004,28 @@ function splitShellWords(command) {
887
1004
  function safeArtifactPart(value) {
888
1005
  return value.replace(/[^A-Za-z0-9_.-]+/gu, "-");
889
1006
  }
1007
+ function safeGeneratedJobId(value) {
1008
+ const normalized = value.toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "");
1009
+ return normalized || "job";
1010
+ }
890
1011
  function renderPackagePreviewJob(lines, model) {
891
1012
  const preview = model.packagePreviews;
892
1013
  if (!preview.package || !preview.target)
893
1014
  return;
894
- lines.push(" package-preview:", " name: package-preview", " if: github.event_name == 'pull_request' && github.event.pull_request.draft == false", " runs-on: ubuntu-latest", " permissions:", " contents: read", " issues: write", " packages: write", " pull-requests: write", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, " with:", " persist-credentials: false", "", ...(model.taskCache
895
- ? [
896
- " - name: Restore task cache",
897
- ` uses: ${CACHE_ACTION}`,
898
- " with:",
899
- " path: .async/cache",
900
- " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
901
- " restore-keys: |",
902
- " async-pipeline-${{ runner.os }}-",
903
- ""
904
- ]
905
- : []), ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
1015
+ lines.push(" package-preview:", " name: package-preview", " if: github.event_name == 'pull_request' && github.event.pull_request.draft == false", " runs-on: ubuntu-latest", " permissions:", " contents: read", " issues: write", " packages: write", " pull-requests: write", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, " with:", " persist-credentials: false", "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
906
1016
  if (model.buildCommand) {
907
1017
  lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
908
1018
  }
1019
+ renderTaskCacheRestoreSteps(lines, model, { kind: "task", id: preview.target, manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" });
909
1020
  renderRunActionStep(lines, "Run package preview target", `${model.command} github check && ${model.command} run-task ${shellWord(preview.target)}`, {});
1021
+ renderTaskCacheSaveSteps(lines, model, { kind: "task", id: preview.target, manifestPath: ".async/actions/cache/package-preview-cache-manifest.json" });
910
1022
  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} }}`);
911
1023
  renderEvidenceCollectStep(lines, model);
912
1024
  lines.push("");
913
1025
  }
914
1026
  function renderBridgeJob(lines, model) {
915
1027
  const bridge = model.bridge;
916
- lines.push(` ${bridge.job}:`, ` name: ${bridge.job}`, ` if: ${renderBridgeCondition(bridge)}`, " runs-on: ubuntu-latest", " permissions:", " contents: write", " pull-requests: write", " concurrency:", " group: async-bridge-${{ github.repository }}", " cancel-in-progress: false", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, " with:", " persist-credentials: false", "", ...(model.taskCache
917
- ? [
918
- " - name: Restore task cache",
919
- ` uses: ${CACHE_ACTION}`,
920
- " with:",
921
- " path: .async/cache",
922
- " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
923
- " restore-keys: |",
924
- " async-pipeline-${{ runner.os }}-",
925
- ""
926
- ]
927
- : []), ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
1028
+ lines.push(` ${bridge.job}:`, ` name: ${bridge.job}`, ` if: ${renderBridgeCondition(bridge)}`, " runs-on: ubuntu-latest", " permissions:", " contents: write", " pull-requests: write", " concurrency:", " group: async-bridge-${{ github.repository }}", " cancel-in-progress: false", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, " with:", " persist-credentials: false", "", ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
928
1029
  if (model.buildCommand) {
929
1030
  lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
930
1031
  }
@@ -967,6 +1068,10 @@ function renderEvidenceFanInJob(lines, model) {
967
1068
  }
968
1069
  function evidenceProducerJobIds(model) {
969
1070
  const ids = new Set(model.jobs.map((job) => job.id));
1071
+ for (const sourceJob of model.sourceImpactJobs) {
1072
+ ids.add(sourceJob.planJob);
1073
+ ids.add(sourceJob.matrixJob);
1074
+ }
970
1075
  if (model.pages.enabled)
971
1076
  ids.add(model.pages.job);
972
1077
  if (model.packagePreviews.enabled)