@async/pipeline 0.9.1 → 0.9.3

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.
@@ -5,20 +5,58 @@ import { dirname, join, relative, resolve } from "node:path";
5
5
  import { githubConfigForJob, pipelineError } from "../core/index.js";
6
6
  export const GITHUB_WORKFLOW_PATH = ".github/workflows/async-pipeline.yml";
7
7
  export const GITHUB_LOCK_PATH = ".github/async-pipeline.lock.json";
8
- const GENERATOR_VERSION = 12;
8
+ const GENERATOR_VERSION = 14;
9
9
  const DEFAULT_NODE_VERSION = "24";
10
10
  const DEFAULT_DENO_VERSION = "2";
11
- const ASYNC_SETUP_ACTION = "async/actions/setup@v0";
12
- const ASYNC_RUN_ACTION = "async/actions/run@v0";
13
- const ASYNC_PAGES_ACTION = "async/actions/pages@v0";
14
- const ASYNC_PREVIEW_ACTION = "async/actions/preview@v0";
15
- const ASYNC_PUBLISH_ACTION = "async/actions/publish@v0";
16
- const ASYNC_DEPENDABOT_MERGE_ACTION = "async/actions/dependabot-merge@v0";
17
- const PNPM_SETUP_ACTION = "pnpm/setup@cf03a9b516e09bc5a90f041fc26fc930c9dc631b # v1.0.0";
18
- const DENO_SETUP_ACTION = "denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4";
19
- const SETUP_NODE_ACTION = "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6";
20
11
  const DEFAULT_PNPM_VERSION = "11.1.0";
21
12
  const DEFAULT_DENO_PIPELINE_COMMAND = "deno run -A npm:@async/pipeline/cli";
13
+ function defineActionRef(id, uses, sha, label) {
14
+ return {
15
+ id,
16
+ uses,
17
+ sha,
18
+ label,
19
+ ref: `${uses}@${sha} # ${label}`
20
+ };
21
+ }
22
+ const ASYNC_ACTIONS_SHA = "313494352cd10207bf0331c83e83364eb45c8e02";
23
+ const ASYNC_ACTIONS_LABEL = "v0.1.5";
24
+ const GENERATED_ACTIONS = [
25
+ defineActionRef("async.actions.setup", "async/actions/setup", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
26
+ defineActionRef("async.actions.run", "async/actions/run", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
27
+ defineActionRef("async.actions.pages", "async/actions/pages", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
28
+ defineActionRef("async.actions.preview", "async/actions/preview", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
29
+ defineActionRef("async.actions.publish", "async/actions/publish", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
30
+ defineActionRef("async.actions.dependabot-merge", "async/actions/dependabot-merge", ASYNC_ACTIONS_SHA, ASYNC_ACTIONS_LABEL),
31
+ defineActionRef("actions.checkout", "actions/checkout", "de0fac2e4500dabe0009e67214ff5f5447ce83dd", "v6.0.2"),
32
+ defineActionRef("actions.cache", "actions/cache", "0057852bfaa89a56745cba8c7296529d2fc39830", "v4"),
33
+ defineActionRef("pnpm.setup", "pnpm/setup", "cf03a9b516e09bc5a90f041fc26fc930c9dc631b", "v1.0.0"),
34
+ defineActionRef("deno.setup", "denoland/setup-deno", "667a34cdef165d8d2b2e98dde39547c9daac7282", "v2.0.4"),
35
+ defineActionRef("actions.setup-node", "actions/setup-node", "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e", "v6"),
36
+ defineActionRef("dependabot.fetch-metadata", "dependabot/fetch-metadata", "25dd0e34f4fe68f24cc83900b1fe3fe149efef98", "v3.1.0")
37
+ ];
38
+ const ACTION_LOCKS = GENERATED_ACTIONS.map(({ id, uses, sha, label, ref }) => ({ id, uses, sha, label, ref }))
39
+ .sort((left, right) => left.id.localeCompare(right.id));
40
+ const ACTION_BY_ID = Object.fromEntries(GENERATED_ACTIONS.map((action) => [action.id, action]));
41
+ const ASYNC_SETUP_ACTION = actionRef("async.actions.setup");
42
+ const ASYNC_RUN_ACTION = actionRef("async.actions.run");
43
+ const ASYNC_PAGES_ACTION = actionRef("async.actions.pages");
44
+ const ASYNC_PREVIEW_ACTION = actionRef("async.actions.preview");
45
+ const ASYNC_PUBLISH_ACTION = actionRef("async.actions.publish");
46
+ const ASYNC_DEPENDABOT_MERGE_ACTION = actionRef("async.actions.dependabot-merge");
47
+ const CHECKOUT_ACTION = actionRef("actions.checkout");
48
+ const CACHE_ACTION = actionRef("actions.cache");
49
+ const PNPM_SETUP_ACTION = actionRef("pnpm.setup");
50
+ const DENO_SETUP_ACTION = actionRef("deno.setup");
51
+ const SETUP_NODE_ACTION = actionRef("actions.setup-node");
52
+ const DEPENDABOT_FETCH_METADATA_ACTION = actionRef("dependabot.fetch-metadata");
53
+ function actionRef(id) {
54
+ const action = ACTION_BY_ID[id];
55
+ if (!action) {
56
+ throw new Error(`Missing generated GitHub action manifest entry ${id}.`);
57
+ }
58
+ return action.ref;
59
+ }
22
60
  export async function renderGitHubWorkflow(pipeline, options) {
23
61
  const workflowPath = options.workflowPath ?? pipeline.sync.github.workflow ?? GITHUB_WORKFLOW_PATH;
24
62
  const lockPath = options.lockPath ?? pipeline.sync.github.lock ?? GITHUB_LOCK_PATH;
@@ -47,8 +85,10 @@ export async function renderGitHubWorkflow(pipeline, options) {
47
85
  dependencyCachePath: renderModel.dependencyCachePath,
48
86
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
49
87
  packagePreviews: renderModel.packagePreviews,
88
+ bridge: renderModel.bridge,
50
89
  pages: renderModel.pages,
51
- manualDispatchJobs: renderModel.manualDispatchJobs
90
+ manualDispatchJobs: renderModel.manualDispatchJobs,
91
+ actions: ACTION_LOCKS
52
92
  });
53
93
  const lock = {
54
94
  version: GENERATOR_VERSION,
@@ -57,6 +97,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
57
97
  workflow: renderModel.workflowPath,
58
98
  hash,
59
99
  generatedAt: new Date().toISOString(),
100
+ actions: ACTION_LOCKS,
60
101
  triggers: renderModel.triggers,
61
102
  jobs: renderModel.jobs,
62
103
  packageManager: renderModel.packageManager,
@@ -71,6 +112,7 @@ export async function renderGitHubWorkflow(pipeline, options) {
71
112
  dependencyCachePath: renderModel.dependencyCachePath,
72
113
  dependabotAutoMerge: renderModel.dependabotAutoMerge,
73
114
  packagePreviews: renderModel.packagePreviews,
115
+ bridge: renderModel.bridge,
74
116
  pages: renderModel.pages,
75
117
  manualDispatchJobs: renderModel.manualDispatchJobs
76
118
  };
@@ -93,11 +135,19 @@ export async function checkGitHubWorkflow(result, cwd) {
93
135
  const issues = [];
94
136
  const workflowFile = resolve(cwd, result.workflowPath);
95
137
  const lockFile = resolve(cwd, result.lockPath);
138
+ const renderedMutableRefs = findMutableRemoteActionRefs(result.workflow);
139
+ if (renderedMutableRefs.length > 0) {
140
+ issues.push(`Generated workflow renderer produced mutable action refs: ${renderedMutableRefs.join(", ")}.`);
141
+ }
96
142
  if (!existsSync(workflowFile)) {
97
143
  issues.push(`Missing generated workflow ${result.workflowPath}. Run async-pipeline github generate.`);
98
144
  }
99
145
  else {
100
146
  const existingWorkflow = await readFile(workflowFile, "utf8");
147
+ const existingMutableRefs = findMutableRemoteActionRefs(existingWorkflow);
148
+ if (existingMutableRefs.length > 0) {
149
+ issues.push(`Generated workflow ${result.workflowPath} contains mutable action refs (${existingMutableRefs.join(", ")}). Run async-pipeline github generate.`);
150
+ }
101
151
  if (existingWorkflow !== result.workflow) {
102
152
  issues.push(`Generated workflow ${result.workflowPath} is stale. Run async-pipeline github generate.`);
103
153
  }
@@ -113,6 +163,22 @@ export async function checkGitHubWorkflow(result, cwd) {
113
163
  }
114
164
  return issues;
115
165
  }
166
+ function findMutableRemoteActionRefs(workflow) {
167
+ const refs = new Set();
168
+ for (const line of workflow.split("\n")) {
169
+ const match = /^\s*uses:\s*([^#\s]+)/u.exec(line);
170
+ if (!match)
171
+ continue;
172
+ const value = (match[1] ?? "").replace(/^["']|["']$/gu, "");
173
+ if (value.startsWith("./") || value.startsWith("../") || value.startsWith("docker://"))
174
+ continue;
175
+ const atIndex = value.lastIndexOf("@");
176
+ if (atIndex < 0 || !/^[0-9a-f]{40}$/iu.test(value.slice(atIndex + 1))) {
177
+ refs.add(value);
178
+ }
179
+ }
180
+ return [...refs].sort((left, right) => left.localeCompare(right));
181
+ }
116
182
  export async function readGitHubEventContext(env) {
117
183
  const eventName = env.ASYNC_PIPELINE_GITHUB_EVENT_NAME ?? env.GITHUB_EVENT_NAME ?? "workflow_dispatch";
118
184
  const eventPath = env.GITHUB_EVENT_PATH;
@@ -166,6 +232,7 @@ function buildRenderModel(pipeline, options) {
166
232
  addPullRequestTrigger(triggers, "pull_request");
167
233
  }
168
234
  const pages = resolveGitHubPages(pipeline);
235
+ const bridge = resolveGitHubBridge(pipeline);
169
236
  if (pages.enabled) {
170
237
  if (pages.triggers.pullRequest) {
171
238
  addGitHubEventTrigger(triggers, "pull_request");
@@ -174,6 +241,9 @@ function buildRenderModel(pipeline, options) {
174
241
  addPushBranchTrigger(triggers, pages.triggers.main.branch);
175
242
  }
176
243
  }
244
+ if (bridge.actionsJob.scheduled && bridge.schedule) {
245
+ addScheduleTrigger(triggers, bridge.schedule, "async-bridge");
246
+ }
177
247
  const manualDispatchJobs = Object.values(pipeline.jobs)
178
248
  .filter((job) => job.trigger.some((triggerId) => pipeline.triggers[triggerId]?.type === "manual"))
179
249
  .map((job) => job.id)
@@ -182,6 +252,10 @@ function buildRenderModel(pipeline, options) {
182
252
  manualDispatchJobs.push(pages.job);
183
253
  manualDispatchJobs.sort((left, right) => left.localeCompare(right));
184
254
  }
255
+ if (bridge.actionsJob.manual) {
256
+ manualDispatchJobs.push(bridge.job);
257
+ manualDispatchJobs.sort((left, right) => left.localeCompare(right));
258
+ }
185
259
  const nodeVersion = pipeline.sync.github.nodeVersion ?? DEFAULT_NODE_VERSION;
186
260
  const runtime = resolveRuntimeSpecs(pipeline.sync.github.runtime, options.projectKind, nodeVersion);
187
261
  const setup = resolveGitHubSetup(pipeline.sync.github.setup, options.packageManager, options.packageManagerVersion);
@@ -217,10 +291,31 @@ function buildRenderModel(pipeline, options) {
217
291
  dependencyCachePath: pipeline.sync.github.dependencyCache === false ? undefined : options.dependencyCachePath,
218
292
  dependabotAutoMerge: pipeline.sync.github.dependabotAutoMerge,
219
293
  packagePreviews,
294
+ bridge,
220
295
  pages,
221
296
  manualDispatchJobs
222
297
  };
223
298
  }
299
+ function resolveGitHubBridge(pipeline) {
300
+ const config = pipeline.sync.github.bridge;
301
+ const actionsJobEnabled = gitHubBridgeActionsEnabled(config);
302
+ const scheduled = actionsJobEnabled && config.schedule !== false;
303
+ const manual = actionsJobEnabled;
304
+ return {
305
+ ...config,
306
+ job: "async-bridge",
307
+ actionsJob: {
308
+ enabled: actionsJobEnabled,
309
+ scheduled,
310
+ manual
311
+ }
312
+ };
313
+ }
314
+ function gitHubBridgeActionsEnabled(bridge) {
315
+ if (!bridge.enabled)
316
+ return false;
317
+ return bridge.mode === "actions";
318
+ }
224
319
  function resolveGitHubPages(pipeline) {
225
320
  const config = pipeline.sync.github.pages;
226
321
  if (!config.enabled)
@@ -269,6 +364,17 @@ function addPushBranchTrigger(triggers, branch) {
269
364
  branches: [...new Set([...existingBranches, branch])].sort()
270
365
  });
271
366
  }
367
+ function addScheduleTrigger(triggers, cron, id) {
368
+ const existing = Array.isArray(triggers.schedule)
369
+ ? triggers.schedule.filter((value) => {
370
+ return Boolean(value) && typeof value === "object" && typeof value.cron === "string";
371
+ })
372
+ : [];
373
+ if (!existing.some((schedule) => schedule.cron === cron)) {
374
+ existing.push({ cron, id });
375
+ }
376
+ triggers.schedule = existing.sort((left, right) => left.cron.localeCompare(right.cron));
377
+ }
272
378
  function resolvePackagePreviews(pipeline, packageInfo) {
273
379
  const config = pipeline.sync.github.packagePreviews;
274
380
  if (!config.enabled)
@@ -360,6 +466,9 @@ function renderWorkflow(model) {
360
466
  if (model.packagePreviews.enabled) {
361
467
  renderPackagePreviewJob(lines, model);
362
468
  }
469
+ if (model.bridge.actionsJob.enabled) {
470
+ renderBridgeJob(lines, model);
471
+ }
363
472
  return `${lines.join("\n").replace(/\n+$/u, "")}\n`;
364
473
  }
365
474
  function renderJob(lines, model, job) {
@@ -407,10 +516,10 @@ function renderJob(lines, model, job) {
407
516
  if (pullRequests)
408
517
  lines.push(` pull-requests: ${pullRequests}`);
409
518
  }
410
- lines.push(" steps:", " - name: Checkout", " uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2", "", ...(model.taskCache
519
+ lines.push(" steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
411
520
  ? [
412
521
  " - name: Restore task cache",
413
- " uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4",
522
+ ` uses: ${CACHE_ACTION}`,
414
523
  " with:",
415
524
  " path: .async/cache",
416
525
  " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
@@ -445,10 +554,10 @@ function renderGeneratedPagesJob(lines, model) {
445
554
  const pages = model.pages;
446
555
  if (!pages.target)
447
556
  return;
448
- lines.push(` ${yamlKey(pages.job)}:`, ` name: ${pages.job}`, ` if: ${renderGeneratedPagesCondition(pages)}`, " runs-on: ubuntu-latest", " steps:", " - name: Checkout", " uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2", "", ...(model.taskCache
557
+ lines.push(` ${yamlKey(pages.job)}:`, ` name: ${pages.job}`, ` if: ${renderGeneratedPagesCondition(pages)}`, " runs-on: ubuntu-latest", " steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
449
558
  ? [
450
559
  " - name: Restore task cache",
451
- " uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4",
560
+ ` uses: ${CACHE_ACTION}`,
452
561
  " with:",
453
562
  " path: .async/cache",
454
563
  " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
@@ -511,6 +620,9 @@ function appendLifecycleTaskPlan(tasks, taskId, plan, visited) {
511
620
  const task = tasks[taskId];
512
621
  if (!task)
513
622
  return false;
623
+ if (task.retry.attempts !== 1 || task.retry.delayMs || task.timeoutMs !== undefined) {
624
+ return false;
625
+ }
514
626
  const lifecycleSteps = task.steps.map((step) => {
515
627
  if (typeof step === "object" && step && "kind" in step && step.kind === "shell") {
516
628
  return parseLifecycleCommand(step.command);
@@ -532,24 +644,32 @@ function appendLifecycleTaskPlan(tasks, taskId, plan, visited) {
532
644
  return true;
533
645
  }
534
646
  function parseLifecycleCommand(command) {
647
+ if (containsUnsupportedShellSyntax(command))
648
+ return undefined;
535
649
  const argv = splitShellWords(command);
536
650
  const cliIndex = argv.findIndex(isPipelineCliToken);
537
651
  if (cliIndex < 0)
538
652
  return undefined;
653
+ if (!isAllowedCliPrefix(argv.slice(0, cliIndex)))
654
+ return undefined;
539
655
  const args = argv.slice(cliIndex + 1);
540
656
  const packagePath = flagValue(args, "--package") ?? ".";
541
657
  if (args[0] === "publish" && args[1] === "github" && (args[2] === "main" || args[2] === "pr")) {
658
+ if (!hasOnlyAllowedOptions(args, 3, new Set(["--package", "--registry", "--namespace", "--token-env-name"]), new Set(["--no-comment"])))
659
+ return undefined;
542
660
  return {
543
661
  kind: "preview",
544
662
  mode: args[2],
545
663
  packagePath,
546
664
  registry: flagValue(args, "--registry") ?? "https://npm.pkg.github.com",
547
665
  namespace: flagValue(args, "--namespace"),
548
- comment: args[2] === "pr",
666
+ comment: args[2] === "pr" && !args.includes("--no-comment"),
549
667
  tokenEnv: flagValue(args, "--token-env-name") ?? "GITHUB_TOKEN"
550
668
  };
551
669
  }
552
670
  if (args[0] === "publish" && args[1] === "github" && args[2] === "release") {
671
+ if (!hasOnlyAllowedOptions(args, 3, new Set(["--package", "--registry", "--tag", "--dist-tag"]), new Set()))
672
+ return undefined;
553
673
  return {
554
674
  kind: "publish",
555
675
  mode: "github-packages",
@@ -559,6 +679,8 @@ function parseLifecycleCommand(command) {
559
679
  };
560
680
  }
561
681
  if (args[0] === "publish" && args[1] === "npm") {
682
+ if (!hasOnlyAllowedOptions(args, 2, new Set(["--package", "--registry", "--tag", "--dist-tag"]), new Set()))
683
+ return undefined;
562
684
  return {
563
685
  kind: "publish",
564
686
  mode: "npm",
@@ -568,6 +690,8 @@ function parseLifecycleCommand(command) {
568
690
  };
569
691
  }
570
692
  if (args[0] === "release" && args[1] === "ensure") {
693
+ if (!hasOnlyAllowedOptions(args, 2, new Set(["--package"]), new Set()))
694
+ return undefined;
571
695
  return {
572
696
  kind: "publish",
573
697
  mode: "github-release",
@@ -577,6 +701,8 @@ function parseLifecycleCommand(command) {
577
701
  };
578
702
  }
579
703
  if (args[0] === "release" && args[1] === "doctor") {
704
+ if (!hasOnlyAllowedOptions(args, 2, new Set(["--package"]), new Set()))
705
+ return undefined;
580
706
  return {
581
707
  kind: "publish",
582
708
  mode: "doctor",
@@ -590,7 +716,7 @@ function parseLifecycleCommand(command) {
590
716
  function renderLifecycleJobPlan(lines, model, job, plan) {
591
717
  for (const item of plan) {
592
718
  if (item.kind === "run-task") {
593
- renderRunActionStep(lines, `Run pipeline task ${item.taskId}`, `${model.command} github check && ${model.command} run-task ${shellWord(item.taskId)}`, job.env, { artifactName: `async-pipeline-\${{ github.job }}-${safeArtifactPart(item.taskId)}-runs` });
719
+ renderRunActionStep(lines, `Run pipeline task ${item.taskId}`, `${model.command} github check && ${model.command} run-task ${shellWord(item.taskId)}`, scopeTaskRunEnv(job.env, model.tasks[item.taskId]), { artifactName: `async-pipeline-\${{ github.job }}-${safeArtifactPart(item.taskId)}-runs` });
594
720
  continue;
595
721
  }
596
722
  if (item.kind === "preview") {
@@ -602,7 +728,7 @@ function renderLifecycleJobPlan(lines, model, job, plan) {
602
728
  }
603
729
  function renderPreviewActionStep(lines, preview, env) {
604
730
  lines.push("", ` - name: Publish ${preview.mode === "main" ? "main" : "PR"} package preview`, ` uses: ${ASYNC_PREVIEW_ACTION}`, " with:", ` package-path: ${JSON.stringify(preview.packagePath)}`, ` target-registry: ${JSON.stringify(preview.registry)}`, ...(preview.namespace ? [` namespace: ${JSON.stringify(preview.namespace)}`] : []), ` mode: ${preview.mode}`, ` comment: ${preview.comment ? "true" : "false"}`, ` token-env-name: ${JSON.stringify(preview.tokenEnv)}`);
605
- renderActionEnv(lines, env);
731
+ renderActionEnv(lines, scopeActionEnv(env, new Set([preview.tokenEnv])));
606
732
  }
607
733
  function renderPublishActionStep(lines, publish, env, provenance) {
608
734
  const label = publish.mode === "github-release"
@@ -612,8 +738,25 @@ function renderPublishActionStep(lines, publish, env, provenance) {
612
738
  : publish.mode === "doctor"
613
739
  ? "Run release doctor"
614
740
  : "Publish npm package";
615
- lines.push("", ` - name: ${label}`, ` uses: ${ASYNC_PUBLISH_ACTION}`, " with:", ` package-path: ${JSON.stringify(publish.packagePath)}`, ` mode: ${publish.mode}`, ` registry: ${JSON.stringify(publish.registry)}`, ` dist-tag: ${JSON.stringify(publish.distTag)}`, ...(publish.mode === "npm" ? [` provenance: ${provenance ? "true" : "false"}`] : []));
616
- renderActionEnv(lines, env);
741
+ lines.push("", ` - name: ${label}`, ` uses: ${ASYNC_PUBLISH_ACTION}`, " with:", ` package-path: ${JSON.stringify(publish.packagePath)}`, ` mode: ${publish.mode}`, ` registry: ${JSON.stringify(publish.registry)}`, ` dist-tag: ${JSON.stringify(publish.distTag)}`, ...(publish.mode === "npm" ? [" token-env-name: NODE_AUTH_TOKEN"] : []), ...(publish.mode === "github-packages" ? [" token-env-name: GITHUB_TOKEN"] : []), ...(publish.mode === "npm" ? [` provenance: ${provenance ? "true" : "false"}`] : []));
742
+ renderActionEnv(lines, scopeActionEnv(env, publish.mode === "npm" ? new Set(["NODE_AUTH_TOKEN"]) : new Set(["GITHUB_TOKEN"])));
743
+ }
744
+ function scopeActionEnv(env, allowedSecretNames) {
745
+ return Object.fromEntries(Object.entries(env).filter(([name, value]) => !isSecretEnvValue(value) || isAllowedSecretEnv(name, value, allowedSecretNames)));
746
+ }
747
+ function isSecretEnvValue(value) {
748
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "async-pipeline.env.secret";
749
+ }
750
+ function isAllowedSecretEnv(name, value, allowedSecretNames) {
751
+ if (allowedSecretNames.has(name))
752
+ return true;
753
+ if (typeof value === "object" && value !== null && "name" in value && typeof value.name === "string") {
754
+ return allowedSecretNames.has(value.name);
755
+ }
756
+ return false;
757
+ }
758
+ function scopeTaskRunEnv(env, task) {
759
+ return scopeActionEnv(env, new Set(task?.requires?.secrets ?? []));
617
760
  }
618
761
  function renderActionEnv(lines, env) {
619
762
  lines.push(" env:", " CI: true");
@@ -627,6 +770,37 @@ function renderActionEnv(lines, env) {
627
770
  function isPipelineCliToken(token) {
628
771
  return token === "async-pipeline" || token.includes("@async/pipeline/cli");
629
772
  }
773
+ function containsUnsupportedShellSyntax(command) {
774
+ return /(?:^|\s)(?:&&|\|\||;|\||&|>|<)(?:\s|$)|[`]|\$\(/u.test(command);
775
+ }
776
+ function isAllowedCliPrefix(prefix) {
777
+ const rendered = prefix.join(" ");
778
+ return [
779
+ "",
780
+ "pnpm",
781
+ "pnpm exec",
782
+ "npx",
783
+ "npm exec",
784
+ "npm exec --",
785
+ "deno run -A"
786
+ ].includes(rendered);
787
+ }
788
+ function hasOnlyAllowedOptions(args, startIndex, valueFlags, booleanFlags) {
789
+ for (let index = startIndex; index < args.length; index += 1) {
790
+ const arg = args[index];
791
+ if (!arg)
792
+ return false;
793
+ if (booleanFlags.has(arg))
794
+ continue;
795
+ if (!valueFlags.has(arg))
796
+ return false;
797
+ const value = args[index + 1];
798
+ if (!value || value.startsWith("--"))
799
+ return false;
800
+ index += 1;
801
+ }
802
+ return true;
803
+ }
630
804
  function flagValue(args, name) {
631
805
  const index = args.indexOf(name);
632
806
  if (index < 0)
@@ -682,10 +856,10 @@ function renderPackagePreviewJob(lines, model) {
682
856
  const preview = model.packagePreviews;
683
857
  if (!preview.package || !preview.target)
684
858
  return;
685
- 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: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2", " with:", " persist-credentials: false", "", ...(model.taskCache
859
+ 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
686
860
  ? [
687
861
  " - name: Restore task cache",
688
- " uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4",
862
+ ` uses: ${CACHE_ACTION}`,
689
863
  " with:",
690
864
  " path: .async/cache",
691
865
  " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
@@ -701,8 +875,54 @@ function renderPackagePreviewJob(lines, model) {
701
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} }}`, "");
702
876
  lines.push("");
703
877
  }
878
+ function renderBridgeJob(lines, model) {
879
+ const bridge = model.bridge;
880
+ 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
881
+ ? [
882
+ " - name: Restore task cache",
883
+ ` uses: ${CACHE_ACTION}`,
884
+ " with:",
885
+ " path: .async/cache",
886
+ " key: async-pipeline-${{ runner.os }}-${{ github.sha }}",
887
+ " restore-keys: |",
888
+ " async-pipeline-${{ runner.os }}-",
889
+ ""
890
+ ]
891
+ : []), ...renderSetupSteps(model), ...renderDependencyInstallSteps(model));
892
+ if (model.buildCommand) {
893
+ lines.push("", " - name: Build pipeline CLI", ` run: ${model.buildCommand}`);
894
+ }
895
+ renderRunActionStep(lines, "Check generated workflow", `${model.command} github check`, {});
896
+ renderBridgePullStep(lines, bridge);
897
+ lines.push("");
898
+ }
899
+ function renderBridgeCondition(bridge) {
900
+ const conditions = [];
901
+ if (bridge.actionsJob.scheduled && bridge.schedule) {
902
+ conditions.push(`github.event_name == 'schedule' && github.event.schedule == '${escapeExpressionString(bridge.schedule)}'`);
903
+ }
904
+ if (bridge.actionsJob.manual) {
905
+ conditions.push(`github.event_name == 'workflow_dispatch' && github.event.inputs.job == '${bridge.job}'`);
906
+ }
907
+ return conditions.length > 0 ? conditions.join(" || ") : "false";
908
+ }
909
+ function renderBridgePullStep(lines, bridge) {
910
+ const command = [
911
+ "npx",
912
+ "--yes",
913
+ `@async/github-app@${bridge.packageVersion}`,
914
+ "actions",
915
+ "pull",
916
+ "--branch-prefix",
917
+ bridge.branchPrefix,
918
+ "--pull-request",
919
+ String(bridge.pullRequest),
920
+ ...bridge.allowedPaths.flatMap((path) => ["--allowed-path", path])
921
+ ].map(shellWord).join(" ");
922
+ 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
+ }
704
924
  function renderDependabotAutoMergeJob(lines, ecosystems) {
705
- 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@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0", " 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 }}", "");
925
+ 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 }}", "");
706
926
  }
707
927
  function renderSetupSteps(model) {
708
928
  const pnpmVersion = pnpmSetupVersion(model.packageManager, model.packageManagerVersion);