@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.
- package/API_SURFACE.md +2 -1
- package/api-contract.json +10 -0
- package/dist/cli.js +0 -0
- package/dist/internal/core/index.d.ts +25 -0
- package/dist/internal/core/index.d.ts.map +1 -1
- package/dist/internal/core/index.js +111 -1
- package/dist/internal/core/index.js.map +1 -1
- package/dist/internal/node/cli.js +0 -0
- package/dist/internal/node/github.d.ts +16 -1
- package/dist/internal/node/github.d.ts.map +1 -1
- package/dist/internal/node/github.js +243 -23
- package/dist/internal/node/github.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 =
|
|
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",
|
|
519
|
+
lines.push(" steps:", " - name: Checkout", ` uses: ${CHECKOUT_ACTION}`, "", ...(model.taskCache
|
|
411
520
|
? [
|
|
412
521
|
" - name: Restore task cache",
|
|
413
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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",
|
|
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);
|