@dbx-tools/projen 0.9.19 → 0.9.21

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 (104) hide show
  1. package/._LICENSE +0 -0
  2. package/._README.md +0 -0
  3. package/._index.ts +0 -0
  4. package/._package.json +0 -0
  5. package/README.md +67 -83
  6. package/index.ts +2 -7
  7. package/package.json +9 -5
  8. package/src/.__lazy-require.ts +0 -0
  9. package/src/.__release-platform.ts +0 -0
  10. package/src/.__rust-project.ts +0 -0
  11. package/src/.__rust-release-workflow.ts +0 -0
  12. package/src/.__task-command.ts +0 -0
  13. package/src/._barrels.ts +0 -0
  14. package/src/._bun-app.ts +0 -0
  15. package/src/._bun-workflow.ts +0 -0
  16. package/src/._clean.ts +0 -0
  17. package/src/._codegen.ts +0 -0
  18. package/src/._dbx-tools-config.ts +0 -0
  19. package/src/._engine-root.ts +0 -0
  20. package/src/._generated.ts +0 -0
  21. package/src/._mixin.ts +0 -0
  22. package/src/._module-exports.ts +0 -0
  23. package/src/._openapi.ts +0 -0
  24. package/src/._packages.ts +0 -0
  25. package/src/._pnpm-workspace.ts +0 -0
  26. package/src/._project-js.ts +0 -0
  27. package/src/._project-predicate.ts +0 -0
  28. package/src/._project-py.ts +0 -0
  29. package/src/._project-rs.ts +0 -0
  30. package/src/._project.ts +0 -0
  31. package/src/._projen-version.ts +0 -0
  32. package/src/._publish.ts +0 -0
  33. package/src/._release-catalog.ts +0 -0
  34. package/src/._release-dispatch.ts +0 -0
  35. package/src/._release-github.ts +0 -0
  36. package/src/._release.ts +0 -0
  37. package/src/._scaffold.ts +0 -0
  38. package/src/._tags.ts +0 -0
  39. package/src/._tsconfig.ts +0 -0
  40. package/src/._uniffi.ts +0 -0
  41. package/src/._vscode.ts +0 -0
  42. package/src/._watch.ts +0 -0
  43. package/src/._workspace-lock.ts +0 -0
  44. package/src/._workspace-version.ts +0 -0
  45. package/src/_rust-release-workflow.ts +4 -391
  46. package/src/_task-command.ts +75 -0
  47. package/src/project-js.ts +2 -42
  48. package/src/project-py.ts +5 -96
  49. package/src/project-rs.ts +3 -76
  50. package/src/release-catalog.ts +37 -203
  51. package/src/release.ts +12 -634
  52. package/src/scaffold.ts +2 -6
  53. package/src/workspace-version.ts +9 -121
  54. package/tasks/._barrels.ts +0 -0
  55. package/tasks/._bump.ts +0 -0
  56. package/tasks/._clean.ts +0 -0
  57. package/tasks/._compile-workspace.ts +0 -0
  58. package/tasks/._local-publish.ts +0 -0
  59. package/tasks/._openapi.ts +0 -0
  60. package/tasks/._projenrc.ts +0 -0
  61. package/tasks/._publish-npm.ts +0 -0
  62. package/tasks/._publish-python.ts +0 -0
  63. package/tasks/._publish-uniffi-local.ts +0 -0
  64. package/tasks/._publish.ts +0 -0
  65. package/tasks/._python-registry.ts +0 -0
  66. package/tasks/._release-assets.ts +0 -0
  67. package/tasks/._release-candidate.ts +0 -0
  68. package/tasks/._release-manifest.ts +0 -0
  69. package/tasks/._release-pr.ts +0 -0
  70. package/tasks/._release-summary.ts +0 -0
  71. package/tasks/._release-version.ts +0 -0
  72. package/tasks/._rust.ts +0 -0
  73. package/tasks/._sync.ts +0 -0
  74. package/tasks/._uniffi-python.d.ts +0 -0
  75. package/tasks/._uniffi-python.js +0 -0
  76. package/tasks/._uniffi-release.mjs +0 -0
  77. package/tasks/._uniffi.ts +0 -0
  78. package/tasks/._version-check.ts +0 -0
  79. package/tasks/bump.ts +17 -34
  80. package/tasks/local-publish.ts +124 -117
  81. package/tasks/publish-npm.ts +62 -30
  82. package/tasks/publish-python.ts +38 -140
  83. package/tasks/publish-uniffi-local.ts +129 -84
  84. package/tasks/publish.ts +25 -139
  85. package/tasks/python-registry.ts +2 -9
  86. package/tasks/release-assets.ts +56 -65
  87. package/tasks/release-candidate.ts +123 -90
  88. package/tasks/release-pr.ts +266 -240
  89. package/tasks/release-summary.ts +8 -10
  90. package/tasks/release-version.ts +23 -2
  91. package/tasks/rust.ts +8 -13
  92. package/tasks/uniffi-python.d.ts +2 -3
  93. package/tasks/uniffi-python.js +7 -20
  94. package/tasks/uniffi-release.mjs +19 -13
  95. package/tasks/uniffi.ts +15 -24
  96. package/tasks/version-check.ts +24 -56
  97. package/src/release-plan.ts +0 -269
  98. package/src/release-please.ts +0 -242
  99. package/tasks/release-bootstrap.ts +0 -59
  100. package/tasks/release-plan.ts +0 -89
  101. package/tasks/release-please.ts +0 -162
  102. package/tasks/release-request.ts +0 -166
  103. package/tasks/release-summary-units.ts +0 -75
  104. package/tasks/stamp-python.ts +0 -13
@@ -10,11 +10,9 @@
10
10
  import { existsSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
- import * as exec from "@dbx-tools/core/exec";
14
13
  import * as projectUtils from "@dbx-tools/core/project-utils";
15
14
  import { asyncUtils, json, log, object } from "@dbx-tools/shared-core";
16
15
  import { Command } from "commander";
17
- import { publishLocalRelease } from "./local-publish.ts";
18
16
  import { hasLocalReleaseTargets, missingLocalReleaseTools } from "./release-assets.ts";
19
17
  import { generateReleaseSummary } from "./release-summary.ts";
20
18
  import {
@@ -26,7 +24,10 @@ import {
26
24
  type VersionLevel,
27
25
  } from "../src/_release-platform.ts";
28
26
  import {
27
+ captureGitTaskCommand,
29
28
  captureTaskCommand,
29
+ gitTaskCommandSucceeds,
30
+ runGitTaskCommand,
30
31
  runTaskCommand,
31
32
  runTaskCommandAsync,
32
33
  taskCommandSucceeds,
@@ -55,22 +56,8 @@ import {
55
56
 
56
57
  const logger = log.logger("projen:release");
57
58
 
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
59
  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);
60
+ runGitTaskCommand(root, ["push", "--set-upstream", "origin", `HEAD:${branch}`]);
74
61
  }
75
62
 
76
63
  /** Wait for required checks and return the merged pull request commit. */
@@ -149,27 +136,22 @@ function githubAccount(root: string): {
149
136
  const repository = projectUtils.repositoryUrl(root);
150
137
  if (!repository) throw new Error("Release preparation requires a GitHub repository");
151
138
  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);
139
+ const status = captureTaskCommand(root, "gh", [
140
+ "auth",
141
+ "status",
142
+ "--hostname",
143
+ identity.hostname,
144
+ "--json",
145
+ "hosts",
146
+ ]);
147
+ const accounts = githubAuthenticatedAccounts(status, identity.hostname);
162
148
  for (const account of accounts) {
163
149
  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();
150
+ const token = captureTaskCommand(
151
+ root,
152
+ "gh",
153
+ githubTokenArguments(identity.hostname, account.login),
154
+ );
173
155
  if (!token) continue;
174
156
  const tokenEnvironment = {
175
157
  ...process.env,
@@ -177,16 +159,11 @@ function githubAccount(root: string): {
177
159
  GH_REPO: githubRepositorySpecifier(identity),
178
160
  [githubTokenEnvironmentName(identity.hostname)]: token,
179
161
  };
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;
162
+ const repositoryData = json.parseRecord(
163
+ captureTaskCommand(root, "gh", ["api", githubRepositoryApiPath(identity)], {
164
+ env: tokenEnvironment,
165
+ }),
166
+ );
190
167
  const permissions = githubRepositoryPermissions(repositoryData);
191
168
  if (!permissions.push && !permissions.maintain && !permissions.admin) {
192
169
  continue;
@@ -238,12 +215,12 @@ program
238
215
  .addOption(releaseLevelOption())
239
216
  .option("--prefix <prefix>", "release tag prefix", "v")
240
217
  .option("--base <branch>", "release pull request base branch", "main")
218
+ .option("--python-root <path>", "Python package root", "packages/py")
241
219
  .option("--message <message>", "commit message for pending source work", "chore: prepare release")
242
220
  .addOption(releaseOperatingSystemOption())
243
221
  .addOption(releaseArchitectureOption())
244
222
  .option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
245
223
  .option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
246
- .option("--python-root <path>", "Python workspace package root")
247
224
  .option(
248
225
  "--validate-task <task>",
249
226
  "repository task to run before release validation; repeatable",
@@ -270,12 +247,12 @@ program
270
247
  level: VersionLevel;
271
248
  prefix: string;
272
249
  base: string;
250
+ pythonRoot: string;
273
251
  message: string;
274
252
  os: ReleaseOs[];
275
253
  arch: ReleaseArch[];
276
254
  localRegistry: string;
277
255
  localPypi: string;
278
- pythonRoot?: string;
279
256
  validateTask: string[];
280
257
  releaseSummary: boolean;
281
258
  releaseSummaryProviders?: string;
@@ -303,7 +280,9 @@ program
303
280
  if (opts.approve && !opts.wait) {
304
281
  throw new Error("automatic merge requires waiting so the draft candidate can be built");
305
282
  }
306
- const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
283
+ const currentBranch = captureGitTaskCommand(root, ["branch", "--show-current"], {
284
+ check: true,
285
+ });
307
286
  if (!currentBranch) throw new Error("Release preparation requires a local branch");
308
287
  const account = githubAccount(root);
309
288
  if (!account.canManage) {
@@ -315,12 +294,9 @@ program
315
294
  throw new Error(`Local release candidate build requires: ${localTools.join(", ")}`);
316
295
  }
317
296
  }
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 });
297
+ runGitTaskCommand(root, ["fetch", "--tags", "origin", opts.base]);
298
+ const comparisonBase = resolveBaseVersion(root);
299
+ const next = resolveNextVersion(root, opts.level);
324
300
  const releaseVersionScript = fileURLToPath(
325
301
  new URL("./release-version.ts", import.meta.url),
326
302
  );
@@ -334,228 +310,278 @@ program
334
310
  ]);
335
311
  const releaseTag = `${opts.prefix}${next.version}`;
336
312
  const releaseBranch = `release/${releaseTag}`;
337
- const releaseRoot = join(root, ".worktrees", releaseTag);
338
313
  if (currentBranch.startsWith("release/")) {
339
314
  throw new Error("Release preparation must start from a source branch");
340
315
  }
341
316
  if (
342
- git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
343
- capture: true,
344
- check: false,
345
- })
317
+ captureGitTaskCommand(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`])
346
318
  ) {
347
319
  throw new Error(`Release tag already exists: ${releaseTag}`);
348
320
  }
349
321
 
350
- const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
351
- capture: true,
352
- });
322
+ const status = captureGitTaskCommand(
323
+ root,
324
+ ["status", "--porcelain=v1", "--untracked-files=all"],
325
+ { check: true },
326
+ );
353
327
  if (status) {
354
- git(root, ["add", "-A"]);
355
- git(root, ["commit", "-m", opts.message]);
328
+ runGitTaskCommand(root, ["add", "-A"]);
329
+ runGitTaskCommand(root, ["commit", "-m", opts.message]);
356
330
  }
357
- if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
358
- git(root, ["merge", "--no-edit", `origin/${opts.base}`]);
331
+ if (
332
+ !gitTaskCommandSucceeds(root, [
333
+ "merge-base",
334
+ "--is-ancestor",
335
+ `origin/${opts.base}`,
336
+ "HEAD",
337
+ ])
338
+ ) {
339
+ runGitTaskCommand(root, ["merge", "--no-edit", `origin/${opts.base}`]);
359
340
  }
360
341
  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",
342
+ const localBranch = Boolean(
343
+ captureGitTaskCommand(root, ["branch", "--list", releaseBranch]),
344
+ );
345
+ const remoteBranch = Boolean(
346
+ captureGitTaskCommand(root, [
347
+ "ls-remote",
348
+ "--heads",
349
+ "origin",
350
+ `refs/heads/${releaseBranch}`,
351
+ ]),
352
+ );
353
+ let releaseCompleted = false;
354
+ try {
355
+ if (localBranch) {
356
+ runGitTaskCommand(root, ["switch", releaseBranch]);
357
+ } else if (remoteBranch) {
358
+ runGitTaskCommand(root, [
359
+ "switch",
360
+ "--track",
361
+ "-c",
362
+ releaseBranch,
363
+ `origin/${releaseBranch}`,
388
364
  ]);
365
+ } else {
366
+ runGitTaskCommand(root, ["switch", "-c", releaseBranch]);
389
367
  }
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"]);
368
+ const resumableStash = captureGitTaskCommand(root, [
369
+ "stash",
370
+ "list",
371
+ "--format=%gd%x09%s",
372
+ ])
373
+ .split("\n")
374
+ .find((line) => line.includes(`release-resume:${releaseBranch}`))
375
+ ?.split("\t")[0];
376
+ if (resumableStash) runGitTaskCommand(root, ["stash", "pop", resumableStash]);
377
+ if (
378
+ !gitTaskCommandSucceeds(root, ["merge-base", "--is-ancestor", currentBranch, "HEAD"])
379
+ ) {
380
+ runGitTaskCommand(root, ["merge", "--no-edit", currentBranch]);
395
381
  }
396
- logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
397
- }
398
- runTaskCommand(releaseRoot, process.execPath, ["install"]);
382
+ runTaskCommand(root, process.execPath, ["install"]);
399
383
 
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"] : []),
384
+ const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
385
+ const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
386
+ const preparedVersion = readWorkspaceVersion(root);
387
+ if (preparedVersion !== next.version) {
388
+ if (preparedVersion !== comparisonBase) {
389
+ throw new Error(
390
+ `${releaseBranch} carries ${preparedVersion}; expected ${comparisonBase} or ${next.version}`,
391
+ );
392
+ }
393
+ runTaskCommand(root, process.execPath, [
394
+ bumpScript,
395
+ "--level",
396
+ opts.level,
397
+ ...opts.os.flatMap((value) => ["--os", value]),
398
+ ...opts.arch.flatMap((value) => ["--arch", value]),
425
399
  ]);
400
+ } else {
401
+ logger.info(`resuming prepared ${releaseTag}`);
402
+ }
403
+ if (readWorkspaceVersion(root) !== next.version) {
404
+ throw new Error(`Release preparation did not produce ${next.version}`);
426
405
  }
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
406
 
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]);
407
+ runTaskCommand(root, process.execPath, [versionCheckScript]);
408
+ if (opts.validate) {
409
+ for (const task of opts.validateTask) {
410
+ runTaskCommand(root, process.execPath, ["run", task]);
411
+ }
412
+ if (existsSync(join(root, "Cargo.toml"))) {
413
+ runTaskCommand(root, "cargo", [
414
+ "test",
415
+ "--workspace",
416
+ ...(existsSync(join(root, "Cargo.lock")) ? ["--locked"] : []),
417
+ ]);
418
+ }
419
+ runTaskCommand(root, process.execPath, ["run", "compile"]);
420
+ } else {
421
+ logger.warn("release validation skipped by --no-validate");
422
+ }
423
+ runTaskCommand(root, process.execPath, [versionCheckScript]);
424
+ const releaseSummary = opts.releaseSummary
425
+ ? await generateReleaseSummary({
426
+ root,
427
+ version: next.version,
428
+ fromRef: `${opts.prefix}${comparisonBase}`,
429
+ providers: releaseSummaryProviders(opts.releaseSummaryProviders),
430
+ })
431
+ : undefined;
432
+
433
+ runGitTaskCommand(root, ["add", "-A"]);
434
+ const staged = captureGitTaskCommand(root, ["diff", "--cached", "--name-only"], {
435
+ check: true,
436
+ });
437
+ if (staged) {
438
+ runGitTaskCommand(root, ["commit", "-m", `chore(release): ${next.version}`]);
439
+ } else if (!localBranch && !remoteBranch) {
440
+ throw new Error("Release preparation produced no changes");
441
+ }
442
+ runGitTaskCommand(root, ["push", "--set-upstream", "origin", releaseBranch]);
465
443
 
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 => {
444
+ const title = `chore(release): ${next.version}`;
445
+ const body = [
446
+ `Release ${releaseTag}.`,
447
+ "",
448
+ `Source commit: ${captureGitTaskCommand(root, ["rev-parse", `${releaseBranch}^`], { check: true })}`,
449
+ "",
450
+ "Merging this PR updates VERSION on main and prepares a draft GitHub Release.",
451
+ ...(releaseSummary ? ["", releaseSummary] : []),
452
+ ].join("\n");
453
+ const githubEnvironment = {
454
+ ...process.env,
455
+ GH_HOST: account.hostname,
456
+ GH_REPO: githubRepositorySpecifier(account),
457
+ [githubTokenEnvironmentName(account.hostname)]: account.token,
458
+ };
482
459
  if (
483
- taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
460
+ !taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
484
461
  env: githubEnvironment,
485
462
  })
486
463
  ) {
487
- return;
464
+ runTaskCommand(
465
+ root,
466
+ "gh",
467
+ [
468
+ "pr",
469
+ "create",
470
+ "--base",
471
+ opts.base,
472
+ "--head",
473
+ releaseBranch,
474
+ "--title",
475
+ title,
476
+ "--body",
477
+ body,
478
+ ],
479
+ { env: githubEnvironment },
480
+ );
481
+ }
482
+ if (opts.approve) {
483
+ runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
484
+ env: githubEnvironment,
485
+ });
488
486
  }
489
- runTaskCommand(
490
- root,
491
- "gh",
492
- [
493
- "pr",
494
- "create",
495
- "--base",
496
- opts.base,
497
- "--head",
487
+ if (opts.approve && opts.wait) {
488
+ const mergeSha = await waitForPullRequestMerge(
489
+ root,
498
490
  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
- [
491
+ githubEnvironment,
492
+ waitTimeoutMs,
493
+ );
494
+ logger.info("release pull request merged", { releaseBranch, mergeSha });
495
+ runGitTaskCommand(root, ["fetch", "origin", opts.base]);
496
+ runGitTaskCommand(root, ["switch", "--detach", mergeSha]);
497
+ runTaskCommand(root, process.execPath, ["install"]);
498
+ const releaseCandidateScript = join(
499
+ root,
500
+ "node_modules/@dbx-tools/projen/tasks/release-candidate.ts",
501
+ );
502
+ const candidateArguments = [
532
503
  releaseCandidateScript,
533
504
  "--root",
534
- releaseRoot,
505
+ root,
535
506
  "--version",
536
507
  next.version,
537
508
  "--tag",
538
509
  releaseTag,
539
510
  "--sha",
540
511
  mergeSha,
512
+ "--python-root",
513
+ opts.pythonRoot,
541
514
  "--notes-file",
542
515
  `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;
516
+ ];
517
+ runTaskCommand(root, process.execPath, candidateArguments, {
518
+ env: githubEnvironment,
519
+ });
520
+ if (opts.localPublish) {
521
+ const localPublishScript = join(
522
+ root,
523
+ "node_modules/@dbx-tools/projen/tasks/local-publish.ts",
524
+ );
525
+ runTaskCommand(
526
+ root,
527
+ process.execPath,
528
+ [
529
+ localPublishScript,
530
+ "--root",
531
+ root,
532
+ "--candidate-directory",
533
+ "dist/release/upload",
534
+ "--version",
535
+ next.version,
536
+ "--tag",
537
+ releaseTag,
538
+ "--sha",
539
+ mergeSha,
540
+ "--local-registry",
541
+ opts.localRegistry,
542
+ "--local-pypi",
543
+ opts.localPypi,
544
+ ...(opts.localCargo ? ["--local-cargo"] : []),
545
+ ],
546
+ { env: githubEnvironment },
547
+ );
548
+ } else {
549
+ logger.info("local publication skipped by --no-local-publish");
550
+ }
551
+ runTaskCommand(root, process.execPath, [...candidateArguments, "--upload-existing"], {
552
+ env: githubEnvironment,
553
+ });
554
+ logger.success(`prepared draft ${releaseTag} from ${mergeSha}`);
555
+ } else {
556
+ logger.success(`opened ${releaseBranch} for ${releaseTag}`);
557
+ }
558
+ releaseCompleted = true;
559
+ } finally {
560
+ const activeBranch = captureGitTaskCommand(root, ["branch", "--show-current"]);
561
+ if (activeBranch !== currentBranch) {
562
+ const releaseStatus = captureGitTaskCommand(
563
+ root,
564
+ ["status", "--porcelain=v1", "--untracked-files=all"],
565
+ { check: true },
566
+ );
567
+ if (releaseStatus) {
568
+ runGitTaskCommand(root, [
569
+ "stash",
570
+ "push",
571
+ "--include-untracked",
572
+ "--message",
573
+ `release-resume:${releaseBranch}`,
574
+ ]);
575
+ }
576
+ runGitTaskCommand(root, ["switch", currentBranch]);
577
+ }
578
+ if (
579
+ releaseCompleted &&
580
+ captureGitTaskCommand(root, ["branch", "--list", releaseBranch])
581
+ ) {
582
+ runGitTaskCommand(root, ["branch", "--delete", "--force", releaseBranch]);
583
+ }
551
584
  }
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
585
  });
560
586
  },
561
587
  );