@dbx-tools/projen 0.9.19 → 0.9.28

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.
Files changed (56) hide show
  1. package/README.md +83 -229
  2. package/index.ts +11 -18
  3. package/package.json +9 -5
  4. package/src/_task-command.ts +75 -0
  5. package/src/barrels.ts +1 -74
  6. package/src/module-exports.ts +175 -1
  7. package/src/openapi.ts +5 -72
  8. package/src/packages.ts +45 -0
  9. package/src/project-js.ts +5 -48
  10. package/src/project-predicate.ts +1 -2
  11. package/src/project-py.ts +87 -199
  12. package/src/project.ts +2 -2
  13. package/src/python-node-bindings.ts +138 -0
  14. package/src/python-node-bundle.ts +92 -0
  15. package/src/release-catalog.ts +40 -208
  16. package/src/{release-dispatch.ts → release-context.ts} +0 -11
  17. package/src/release.ts +74 -778
  18. package/src/scaffold.ts +2 -6
  19. package/src/workspace-version.ts +9 -121
  20. package/tasks/bump.ts +17 -34
  21. package/tasks/databricks-cli-assets.ts +129 -0
  22. package/tasks/local-publish.ts +61 -138
  23. package/tasks/publish-npm.ts +71 -30
  24. package/tasks/publish-python.ts +61 -148
  25. package/tasks/publish.ts +32 -182
  26. package/tasks/python-node-bindings-watch.ts +47 -0
  27. package/tasks/python-node-bindings.ts +1017 -0
  28. package/tasks/python-registry.ts +2 -9
  29. package/tasks/python-release.ts +74 -0
  30. package/tasks/release-version.ts +23 -2
  31. package/tasks/release.ts +99 -0
  32. package/tasks/sync.ts +13 -18
  33. package/tasks/version-check.ts +13 -72
  34. package/src/_rust-project.ts +0 -429
  35. package/src/_rust-release-workflow.ts +0 -698
  36. package/src/project-rs.ts +0 -1127
  37. package/src/release-plan.ts +0 -269
  38. package/src/release-please.ts +0 -242
  39. package/src/uniffi.ts +0 -65
  40. package/tasks/publish-uniffi-local.ts +0 -314
  41. package/tasks/release-assets.ts +0 -476
  42. package/tasks/release-bootstrap.ts +0 -59
  43. package/tasks/release-candidate.ts +0 -383
  44. package/tasks/release-manifest.ts +0 -273
  45. package/tasks/release-plan.ts +0 -89
  46. package/tasks/release-please.ts +0 -162
  47. package/tasks/release-pr.ts +0 -563
  48. package/tasks/release-request.ts +0 -166
  49. package/tasks/release-summary-units.ts +0 -75
  50. package/tasks/release-summary.ts +0 -381
  51. package/tasks/rust.ts +0 -178
  52. package/tasks/stamp-python.ts +0 -13
  53. package/tasks/uniffi-python.d.ts +0 -61
  54. package/tasks/uniffi-python.js +0 -191
  55. package/tasks/uniffi-release.mjs +0 -573
  56. package/tasks/uniffi.ts +0 -271
@@ -1,563 +0,0 @@
1
- #!/usr/bin/env -S bun
2
- /**
3
- * Prepare a reviewed release pull request from the current branch.
4
- *
5
- * Pending work is committed and the current branch is pushed first. A dedicated
6
- * release branch then receives the VERSION change, generated files, validation,
7
- * and local registry preflight. Public publication remains owned by the
8
- * main-branch release workflow.
9
- */
10
- import { existsSync } from "node:fs";
11
- import { join } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- import * as exec from "@dbx-tools/core/exec";
14
- import * as projectUtils from "@dbx-tools/core/project-utils";
15
- import { asyncUtils, json, log, object } from "@dbx-tools/shared-core";
16
- import { Command } from "commander";
17
- import { publishLocalRelease } from "./local-publish.ts";
18
- import { hasLocalReleaseTargets, missingLocalReleaseTools } from "./release-assets.ts";
19
- import { generateReleaseSummary } from "./release-summary.ts";
20
- import {
21
- releaseArchitectureOption,
22
- releaseLevelOption,
23
- releaseOperatingSystemOption,
24
- type ReleaseArch,
25
- type ReleaseOs,
26
- type VersionLevel,
27
- } from "../src/_release-platform.ts";
28
- import {
29
- captureTaskCommand,
30
- runTaskCommand,
31
- runTaskCommandAsync,
32
- taskCommandSucceeds,
33
- } from "../src/_task-command.ts";
34
- import {
35
- RELEASE_SUMMARY_PROVIDER_NAMES,
36
- type ReleaseSummaryProviderName,
37
- } from "../src/release-dispatch.ts";
38
- import {
39
- githubAccountSupportsWorkflowChanges,
40
- githubAuthenticatedAccounts,
41
- githubRepositoryApiPath,
42
- githubRepositoryCanManage,
43
- githubRepositoryIdentity,
44
- githubRepositoryPermissions,
45
- githubRepositorySpecifier,
46
- githubTokenArguments,
47
- githubTokenEnvironmentName,
48
- } from "../src/release-github.ts";
49
- import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
50
- import {
51
- readWorkspaceVersion,
52
- resolveBaseVersion,
53
- resolveNextVersion,
54
- } from "../src/workspace-version.ts";
55
-
56
- const logger = log.logger("projen:release");
57
-
58
- function git(
59
- root: string,
60
- args: string[],
61
- { capture = false, check = true }: { capture?: boolean; check?: boolean } = {},
62
- ): string {
63
- if (capture) return captureTaskCommand(root, "git", args, { check });
64
- runTaskCommand(root, "git", args);
65
- return "";
66
- }
67
-
68
- function pushCurrentBranch(root: string, branch: string): void {
69
- git(root, ["push", "--set-upstream", "origin", `HEAD:${branch}`]);
70
- }
71
-
72
- function gitSucceeds(root: string, args: string[]): boolean {
73
- return taskCommandSucceeds(root, "git", args);
74
- }
75
-
76
- /** Wait for required checks and return the merged pull request commit. */
77
- async function waitForPullRequestMerge(
78
- root: string,
79
- releaseBranch: string,
80
- env: NodeJS.ProcessEnv,
81
- timeoutMs: number,
82
- ): Promise<string> {
83
- const checksDeadline = Date.now() + timeoutMs;
84
- let checksReported = false;
85
- while (Date.now() < checksDeadline) {
86
- const count = Number(
87
- captureTaskCommand(
88
- root,
89
- "gh",
90
- [
91
- "pr",
92
- "view",
93
- releaseBranch,
94
- "--json",
95
- "statusCheckRollup",
96
- "--jq",
97
- ".statusCheckRollup | length",
98
- ],
99
- { env },
100
- ),
101
- );
102
- if (Number.isInteger(count) && count > 0) {
103
- checksReported = true;
104
- break;
105
- }
106
- await asyncUtils.sleep(2_000);
107
- }
108
- if (!checksReported) {
109
- throw new Error(`Release pull request reported no checks within ${timeoutMs}ms`);
110
- }
111
- await runTaskCommandAsync(
112
- root,
113
- "gh",
114
- ["pr", "checks", releaseBranch, "--watch", "--fail-fast", "--required", "--interval", "10"],
115
- { env, signal: AbortSignal.timeout(timeoutMs) },
116
- );
117
- const deadline = Date.now() + timeoutMs;
118
- while (Date.now() < deadline) {
119
- const state = captureTaskCommand(
120
- root,
121
- "gh",
122
- [
123
- "pr",
124
- "view",
125
- releaseBranch,
126
- "--json",
127
- "state,mergeCommit",
128
- "--jq",
129
- '[.state, (.mergeCommit.oid // "")] | @tsv',
130
- ],
131
- { env },
132
- );
133
- const [status, mergeSha] = state.split("\t");
134
- if (status === "MERGED" && mergeSha) return mergeSha;
135
- if (status === "CLOSED") throw new Error(`Release pull request closed without merging`);
136
- await asyncUtils.sleep(2_000);
137
- }
138
- throw new Error(`Release pull request did not merge within ${timeoutMs}ms`);
139
- }
140
-
141
- function githubAccount(root: string): {
142
- hostname: string;
143
- owner: string;
144
- repository: string;
145
- login: string;
146
- token: string;
147
- canManage: boolean;
148
- } {
149
- const repository = projectUtils.repositoryUrl(root);
150
- if (!repository) throw new Error("Release preparation requires a GitHub repository");
151
- const identity = githubRepositoryIdentity(repository);
152
- const status = exec
153
- .spawnSync("gh", ["auth", "status", "--hostname", identity.hostname, "--json", "hosts"], {
154
- cwd: root,
155
- stdout: "capture",
156
- stderr: "ignore",
157
- stdin: "ignore",
158
- check: false,
159
- })
160
- .stdout?.trim();
161
- const accounts = githubAuthenticatedAccounts(status ?? "", identity.hostname);
162
- for (const account of accounts) {
163
- if (!githubAccountSupportsWorkflowChanges(account)) continue;
164
- const token = exec
165
- .spawnSync("gh", githubTokenArguments(identity.hostname, account.login), {
166
- cwd: root,
167
- stdout: "capture",
168
- stderr: "ignore",
169
- stdin: "ignore",
170
- check: false,
171
- })
172
- .stdout?.trim();
173
- if (!token) continue;
174
- const tokenEnvironment = {
175
- ...process.env,
176
- GH_HOST: identity.hostname,
177
- GH_REPO: githubRepositorySpecifier(identity),
178
- [githubTokenEnvironmentName(identity.hostname)]: token,
179
- };
180
- const repositoryResult = exec.spawnSync("gh", ["api", githubRepositoryApiPath(identity)], {
181
- cwd: root,
182
- env: tokenEnvironment,
183
- stdout: "capture",
184
- stderr: "ignore",
185
- stdin: "ignore",
186
- check: false,
187
- });
188
- const repositoryData =
189
- repositoryResult.exitCode === 0 ? json.parseRecord(repositoryResult.stdout ?? "") : undefined;
190
- const permissions = githubRepositoryPermissions(repositoryData);
191
- if (!permissions.push && !permissions.maintain && !permissions.admin) {
192
- continue;
193
- }
194
- logger.info("selected GitHub CLI account", {
195
- login: account.login,
196
- repository: `${identity.owner}/${identity.repository}`,
197
- });
198
- return {
199
- ...identity,
200
- login: account.login,
201
- token,
202
- canManage: githubRepositoryCanManage(permissions),
203
- };
204
- }
205
- const detected =
206
- accounts
207
- .map((account) => {
208
- const workflow = githubAccountSupportsWorkflowChanges(account)
209
- ? "workflow-capable"
210
- : "missing workflow scope";
211
- return `${account.login} (${workflow})`;
212
- })
213
- .join(", ") || "none";
214
- throw new Error(
215
- `No authenticated GitHub CLI account can write release workflow changes to ${identity.owner}/${identity.repository} on ${identity.hostname}; detected accounts: ${detected}`,
216
- );
217
- }
218
-
219
- function releaseSummaryProviders(
220
- value: string | undefined,
221
- ): ReleaseSummaryProviderName[] | undefined {
222
- if (value === undefined) return undefined;
223
- const providers = value
224
- .split(",")
225
- .map((provider) => provider.trim())
226
- .filter(Boolean);
227
- for (const provider of providers) {
228
- if (!RELEASE_SUMMARY_PROVIDER_NAMES.includes(provider as ReleaseSummaryProviderName)) {
229
- throw new Error(`Unknown release summary provider: ${provider}`);
230
- }
231
- }
232
- return providers as ReleaseSummaryProviderName[];
233
- }
234
-
235
- const program = new Command();
236
- program
237
- .description("Prepare, validate, locally publish, and open a reviewed release PR")
238
- .addOption(releaseLevelOption())
239
- .option("--prefix <prefix>", "release tag prefix", "v")
240
- .option("--base <branch>", "release pull request base branch", "main")
241
- .option("--message <message>", "commit message for pending source work", "chore: prepare release")
242
- .addOption(releaseOperatingSystemOption())
243
- .addOption(releaseArchitectureOption())
244
- .option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
245
- .option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
246
- .option("--python-root <path>", "Python workspace package root")
247
- .option(
248
- "--validate-task <task>",
249
- "repository task to run before release validation; repeatable",
250
- (task: string, tasks: string[]) => [...tasks, task],
251
- [],
252
- )
253
- .option("--no-release-summary", "skip optional AI release summary generation")
254
- .option(
255
- "--release-summary-providers <providers>",
256
- "comma-separated provider order: cursor,codex,claude",
257
- )
258
- .option("--no-validate", "skip repository validation tasks, Rust tests, and TypeScript compile")
259
- .option("--no-local-publish", "skip local npm, PyPI, and Cargo publication")
260
- .option("--no-local-cargo", "skip local Cargo publication")
261
- .option("--no-approve", "open the release pull request without enabling automatic merge")
262
- .option("--no-wait", "return after enabling automatic merge without watching publication")
263
- .option(
264
- "--wait-timeout-minutes <minutes>",
265
- "maximum time for pull request checks and merge",
266
- "120",
267
- )
268
- .action(
269
- async (opts: {
270
- level: VersionLevel;
271
- prefix: string;
272
- base: string;
273
- message: string;
274
- os: ReleaseOs[];
275
- arch: ReleaseArch[];
276
- localRegistry: string;
277
- localPypi: string;
278
- pythonRoot?: string;
279
- validateTask: string[];
280
- releaseSummary: boolean;
281
- releaseSummaryProviders?: string;
282
- validate: boolean;
283
- localPublish: boolean;
284
- localCargo: boolean;
285
- approve: boolean;
286
- wait: boolean;
287
- waitTimeoutMinutes: string;
288
- }) => {
289
- const root = projectUtils.root() ?? process.cwd();
290
- await withWorkspaceMutationLock(root, async () => {
291
- const waitTimeoutMinutes = object.toNumber(opts.waitTimeoutMinutes, {
292
- separators: false,
293
- percent: false,
294
- });
295
- if (
296
- waitTimeoutMinutes === undefined ||
297
- !Number.isInteger(waitTimeoutMinutes) ||
298
- waitTimeoutMinutes <= 0
299
- ) {
300
- throw new Error("--wait-timeout-minutes must be a positive integer");
301
- }
302
- const waitTimeoutMs = waitTimeoutMinutes * 60_000;
303
- if (opts.approve && !opts.wait) {
304
- throw new Error("automatic merge requires waiting so the draft candidate can be built");
305
- }
306
- const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
307
- if (!currentBranch) throw new Error("Release preparation requires a local branch");
308
- const account = githubAccount(root);
309
- if (!account.canManage) {
310
- throw new Error("Release preparation requires maintain or admin permission");
311
- }
312
- if (hasLocalReleaseTargets(root)) {
313
- const localTools = missingLocalReleaseTools(root);
314
- if (localTools.length > 0) {
315
- throw new Error(`Local release candidate build requires: ${localTools.join(", ")}`);
316
- }
317
- }
318
- git(root, ["fetch", "--tags", "origin", opts.base]);
319
- const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
320
- fetch: false,
321
- includeComponentTags: false,
322
- }).version;
323
- const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
324
- const releaseVersionScript = fileURLToPath(
325
- new URL("./release-version.ts", import.meta.url),
326
- );
327
- runTaskCommand(root, process.execPath, [
328
- releaseVersionScript,
329
- "--version",
330
- next.version,
331
- "--prefix",
332
- opts.prefix,
333
- "--assert-next",
334
- ]);
335
- const releaseTag = `${opts.prefix}${next.version}`;
336
- const releaseBranch = `release/${releaseTag}`;
337
- const releaseRoot = join(root, ".worktrees", releaseTag);
338
- if (currentBranch.startsWith("release/")) {
339
- throw new Error("Release preparation must start from a source branch");
340
- }
341
- if (
342
- git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
343
- capture: true,
344
- check: false,
345
- })
346
- ) {
347
- throw new Error(`Release tag already exists: ${releaseTag}`);
348
- }
349
-
350
- const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
351
- capture: true,
352
- });
353
- if (status) {
354
- git(root, ["add", "-A"]);
355
- git(root, ["commit", "-m", opts.message]);
356
- }
357
- if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
358
- git(root, ["merge", "--no-edit", `origin/${opts.base}`]);
359
- }
360
- pushCurrentBranch(root, currentBranch);
361
-
362
- const worktreeExists = existsSync(join(releaseRoot, ".git"));
363
- if (!worktreeExists) {
364
- git(root, ["worktree", "prune"]);
365
- const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
366
- const remoteBranch = git(
367
- root,
368
- ["ls-remote", "--heads", "origin", `refs/heads/${releaseBranch}`],
369
- { capture: true, check: false },
370
- );
371
- if (localBranch || remoteBranch) {
372
- throw new Error(`Release branch already exists without its worktree: ${releaseBranch}`);
373
- }
374
- git(root, ["worktree", "add", "-b", releaseBranch, releaseRoot, "HEAD"]);
375
- } else {
376
- const releaseStatus = git(
377
- releaseRoot,
378
- ["status", "--porcelain=v1", "--untracked-files=all"],
379
- { capture: true },
380
- );
381
- if (releaseStatus) {
382
- git(releaseRoot, [
383
- "stash",
384
- "push",
385
- "--include-untracked",
386
- "--message",
387
- "release-resume",
388
- ]);
389
- }
390
- if (!gitSucceeds(releaseRoot, ["merge-base", "--is-ancestor", currentBranch, "HEAD"])) {
391
- git(releaseRoot, ["merge", "--no-edit", currentBranch]);
392
- }
393
- if (releaseStatus) {
394
- git(releaseRoot, ["stash", "pop"]);
395
- }
396
- logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
397
- }
398
- runTaskCommand(releaseRoot, process.execPath, ["install"]);
399
-
400
- const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
401
- const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
402
- runTaskCommand(releaseRoot, process.execPath, [
403
- bumpScript,
404
- "--level",
405
- opts.level,
406
- "--prefix",
407
- opts.prefix,
408
- ...opts.os.flatMap((value) => ["--os", value]),
409
- ...opts.arch.flatMap((value) => ["--arch", value]),
410
- ]);
411
- if (readWorkspaceVersion(releaseRoot) !== next.version) {
412
- throw new Error(`Release preparation did not produce ${next.version}`);
413
- }
414
-
415
- runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
416
- if (opts.validate) {
417
- for (const task of opts.validateTask) {
418
- runTaskCommand(releaseRoot, process.execPath, ["run", task]);
419
- }
420
- if (existsSync(join(releaseRoot, "Cargo.toml"))) {
421
- runTaskCommand(releaseRoot, "cargo", [
422
- "test",
423
- "--workspace",
424
- ...(existsSync(join(releaseRoot, "Cargo.lock")) ? ["--locked"] : []),
425
- ]);
426
- }
427
- runTaskCommand(releaseRoot, process.execPath, ["run", "compile"]);
428
- } else {
429
- logger.warn("release validation skipped by --no-validate");
430
- }
431
- if (opts.localPublish) {
432
- await publishLocalRelease({
433
- root: releaseRoot,
434
- version: next.version,
435
- localRegistry: opts.localRegistry,
436
- localPypi: opts.localPypi,
437
- pythonRoot: opts.pythonRoot,
438
- localCargo: opts.localCargo,
439
- reuseValidatedNodeCompile: opts.validate,
440
- });
441
- } else {
442
- logger.info("local publication skipped by --no-local-publish");
443
- }
444
- runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
445
- const releaseSummary = opts.releaseSummary
446
- ? await generateReleaseSummary({
447
- root: releaseRoot,
448
- version: next.version,
449
- fromRef: `${opts.prefix}${comparisonBase}`,
450
- providers: releaseSummaryProviders(opts.releaseSummaryProviders),
451
- })
452
- : undefined;
453
-
454
- git(releaseRoot, ["add", "-A"]);
455
- const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
456
- if (staged) {
457
- git(releaseRoot, ["commit", "-m", `chore(release): ${next.version}`]);
458
- } else if (!worktreeExists) {
459
- throw new Error("Release preparation produced no changes");
460
- }
461
- // The source push and release commit hooks have already scanned every new
462
- // byte. A new remote branch has no upstream comparison point, so the
463
- // managed pre-push hook would rescan the repository's complete history.
464
- git(releaseRoot, ["push", "--no-verify", "--set-upstream", "origin", releaseBranch]);
465
-
466
- const title = `chore(release): ${next.version}`;
467
- const body = [
468
- `Release ${releaseTag}.`,
469
- "",
470
- `Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
471
- "",
472
- "Merging this PR updates VERSION on main and prepares a draft GitHub Release.",
473
- ...(releaseSummary ? ["", releaseSummary] : []),
474
- ].join("\n");
475
- const githubEnvironment = {
476
- ...process.env,
477
- GH_HOST: account.hostname,
478
- GH_REPO: githubRepositorySpecifier(account),
479
- [githubTokenEnvironmentName(account.hostname)]: account.token,
480
- };
481
- const ensurePullRequest = (): void => {
482
- if (
483
- taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
484
- env: githubEnvironment,
485
- })
486
- ) {
487
- return;
488
- }
489
- runTaskCommand(
490
- root,
491
- "gh",
492
- [
493
- "pr",
494
- "create",
495
- "--base",
496
- opts.base,
497
- "--head",
498
- releaseBranch,
499
- "--title",
500
- title,
501
- "--body",
502
- body,
503
- ],
504
- { env: githubEnvironment },
505
- );
506
- };
507
- ensurePullRequest();
508
- if (opts.approve) {
509
- runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
510
- env: githubEnvironment,
511
- });
512
- }
513
- if (opts.approve && opts.wait) {
514
- const mergeSha = await waitForPullRequestMerge(
515
- root,
516
- releaseBranch,
517
- githubEnvironment,
518
- waitTimeoutMs,
519
- );
520
- logger.info("release pull request merged", { releaseBranch, mergeSha });
521
- git(root, ["fetch", "origin", opts.base]);
522
- git(releaseRoot, ["switch", "--detach", mergeSha]);
523
- runTaskCommand(releaseRoot, process.execPath, ["install"]);
524
- const releaseCandidateScript = join(
525
- releaseRoot,
526
- "node_modules/@dbx-tools/projen/tasks/release-candidate.ts",
527
- );
528
- runTaskCommand(
529
- releaseRoot,
530
- process.execPath,
531
- [
532
- releaseCandidateScript,
533
- "--root",
534
- releaseRoot,
535
- "--version",
536
- next.version,
537
- "--tag",
538
- releaseTag,
539
- "--sha",
540
- mergeSha,
541
- "--notes-file",
542
- `docs/releases/v${next.version}.md`,
543
- "--upload",
544
- ],
545
- { env: githubEnvironment },
546
- );
547
- git(root, ["worktree", "remove", "--force", releaseRoot]);
548
- git(root, ["branch", "--delete", "--force", releaseBranch]);
549
- logger.success(`prepared draft ${releaseTag} from ${mergeSha}`);
550
- return;
551
- }
552
- git(root, ["worktree", "remove", "--force", releaseRoot]);
553
- git(root, ["branch", "--delete", "--force", releaseBranch]);
554
- logger.success(
555
- `${
556
- opts.approve ? "enabled automatic merge for" : "opened"
557
- } ${releaseBranch} for ${releaseTag}${opts.approve && !opts.wait ? " without waiting" : ""}`,
558
- );
559
- });
560
- },
561
- );
562
-
563
- if (import.meta.main) await program.parseAsync();