@dbx-tools/projen 0.9.18 → 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 (105) 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 +72 -88
  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 +18 -582
  46. package/src/_task-command.ts +75 -0
  47. package/src/project-js.ts +2 -42
  48. package/src/project-py.ts +60 -162
  49. package/src/project-rs.ts +4 -100
  50. package/src/release-catalog.ts +37 -203
  51. package/src/release.ts +96 -775
  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 +125 -41
  82. package/tasks/publish-python.ts +79 -106
  83. package/tasks/publish-uniffi-local.ts +129 -84
  84. package/tasks/publish.ts +44 -139
  85. package/tasks/python-registry.ts +2 -9
  86. package/tasks/release-assets.ts +56 -65
  87. package/tasks/release-candidate.ts +416 -0
  88. package/tasks/release-manifest.ts +273 -0
  89. package/tasks/release-pr.ts +312 -369
  90. package/tasks/release-summary.ts +8 -10
  91. package/tasks/release-version.ts +23 -2
  92. package/tasks/rust.ts +8 -13
  93. package/tasks/uniffi-python.d.ts +2 -3
  94. package/tasks/uniffi-python.js +7 -20
  95. package/tasks/uniffi-release.mjs +19 -13
  96. package/tasks/uniffi.ts +15 -24
  97. package/tasks/version-check.ts +24 -56
  98. package/src/release-plan.ts +0 -269
  99. package/src/release-please.ts +0 -242
  100. package/tasks/release-bootstrap.ts +0 -59
  101. package/tasks/release-plan.ts +0 -89
  102. package/tasks/release-please.ts +0 -162
  103. package/tasks/release-request.ts +0 -166
  104. package/tasks/release-summary-units.ts +0 -75
  105. 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,57 +56,8 @@ import {
55
56
 
56
57
  const logger = log.logger("projen:release");
57
58
 
58
- export type ReleaseBuildMode = "auto" | "local" | "remote";
59
-
60
- /** Resolve an explicit or environment-sensitive native release build mode. */
61
- export function resolveReleaseBuildMode(options: {
62
- readonly requested: ReleaseBuildMode;
63
- readonly canManage: boolean;
64
- readonly localTools: readonly string[];
65
- readonly hasLocalTargets: boolean;
66
- readonly approve: boolean;
67
- readonly wait: boolean;
68
- readonly hostPlatform?: NodeJS.Platform;
69
- }): Exclude<ReleaseBuildMode, "auto"> {
70
- const local =
71
- options.canManage &&
72
- options.hasLocalTargets &&
73
- (options.hostPlatform ?? process.platform) === "darwin" &&
74
- options.localTools.length === 0 &&
75
- options.approve &&
76
- options.wait;
77
- if (options.requested === "auto") return local ? "local" : "remote";
78
- if (options.requested === "remote") return "remote";
79
- if (!options.canManage) throw new Error("--build local requires maintain or admin permission");
80
- if (!options.hasLocalTargets) throw new Error("--build local requires Rust release targets");
81
- if ((options.hostPlatform ?? process.platform) !== "darwin") {
82
- throw new Error("--build local requires macOS");
83
- }
84
- if (options.localTools.length > 0) {
85
- throw new Error(`--build local requires: ${options.localTools.join(", ")}`);
86
- }
87
- if (!options.approve || !options.wait) {
88
- throw new Error("--build local requires automatic merge and waiting");
89
- }
90
- return "local";
91
- }
92
-
93
- function git(
94
- root: string,
95
- args: string[],
96
- { capture = false, check = true }: { capture?: boolean; check?: boolean } = {},
97
- ): string {
98
- if (capture) return captureTaskCommand(root, "git", args, { check });
99
- runTaskCommand(root, "git", args);
100
- return "";
101
- }
102
-
103
59
  function pushCurrentBranch(root: string, branch: string): void {
104
- git(root, ["push", "--set-upstream", "origin", `HEAD:${branch}`]);
105
- }
106
-
107
- function gitSucceeds(root: string, args: string[]): boolean {
108
- return taskCommandSucceeds(root, "git", args);
60
+ runGitTaskCommand(root, ["push", "--set-upstream", "origin", `HEAD:${branch}`]);
109
61
  }
110
62
 
111
63
  /** Wait for required checks and return the merged pull request commit. */
@@ -115,6 +67,34 @@ async function waitForPullRequestMerge(
115
67
  env: NodeJS.ProcessEnv,
116
68
  timeoutMs: number,
117
69
  ): Promise<string> {
70
+ const checksDeadline = Date.now() + timeoutMs;
71
+ let checksReported = false;
72
+ while (Date.now() < checksDeadline) {
73
+ const count = Number(
74
+ captureTaskCommand(
75
+ root,
76
+ "gh",
77
+ [
78
+ "pr",
79
+ "view",
80
+ releaseBranch,
81
+ "--json",
82
+ "statusCheckRollup",
83
+ "--jq",
84
+ ".statusCheckRollup | length",
85
+ ],
86
+ { env },
87
+ ),
88
+ );
89
+ if (Number.isInteger(count) && count > 0) {
90
+ checksReported = true;
91
+ break;
92
+ }
93
+ await asyncUtils.sleep(2_000);
94
+ }
95
+ if (!checksReported) {
96
+ throw new Error(`Release pull request reported no checks within ${timeoutMs}ms`);
97
+ }
118
98
  await runTaskCommandAsync(
119
99
  root,
120
100
  "gh",
@@ -145,64 +125,6 @@ async function waitForPullRequestMerge(
145
125
  throw new Error(`Release pull request did not merge within ${timeoutMs}ms`);
146
126
  }
147
127
 
148
- /** Find and watch the release workflow for an exact merged commit. */
149
- async function waitForReleaseWorkflow(
150
- root: string,
151
- baseBranch: string,
152
- mergeSha: string,
153
- env: NodeJS.ProcessEnv,
154
- timeoutMs: number,
155
- ): Promise<void> {
156
- const deadline = Date.now() + timeoutMs;
157
- let runId = "";
158
- while (Date.now() < deadline) {
159
- runId = captureTaskCommand(
160
- root,
161
- "gh",
162
- [
163
- "run",
164
- "list",
165
- "--workflow",
166
- "release.yml",
167
- "--branch",
168
- baseBranch,
169
- "--event",
170
- "push",
171
- "--commit",
172
- mergeSha,
173
- "--limit",
174
- "1",
175
- "--json",
176
- "databaseId",
177
- "--jq",
178
- ".[0].databaseId // empty",
179
- ],
180
- { env },
181
- );
182
- if (runId) break;
183
- await asyncUtils.sleep(5_000);
184
- }
185
- if (!runId) throw new Error(`Release workflow did not start within ${timeoutMs}ms`);
186
- await runTaskCommandAsync(root, "gh", ["run", "watch", runId, "--exit-status"], {
187
- env,
188
- signal: AbortSignal.timeout(timeoutMs),
189
- });
190
- }
191
-
192
- async function waitForGitHubRelease(
193
- root: string,
194
- tag: string,
195
- env: NodeJS.ProcessEnv,
196
- timeoutMs: number,
197
- ): Promise<void> {
198
- const deadline = Date.now() + timeoutMs;
199
- while (Date.now() < deadline) {
200
- if (taskCommandSucceeds(root, "gh", ["release", "view", tag], { env })) return;
201
- await asyncUtils.sleep(2_000);
202
- }
203
- throw new Error(`GitHub Release ${tag} did not appear within ${timeoutMs}ms`);
204
- }
205
-
206
128
  function githubAccount(root: string): {
207
129
  hostname: string;
208
130
  owner: string;
@@ -214,27 +136,22 @@ function githubAccount(root: string): {
214
136
  const repository = projectUtils.repositoryUrl(root);
215
137
  if (!repository) throw new Error("Release preparation requires a GitHub repository");
216
138
  const identity = githubRepositoryIdentity(repository);
217
- const status = exec
218
- .spawnSync("gh", ["auth", "status", "--hostname", identity.hostname, "--json", "hosts"], {
219
- cwd: root,
220
- stdout: "capture",
221
- stderr: "ignore",
222
- stdin: "ignore",
223
- check: false,
224
- })
225
- .stdout?.trim();
226
- 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);
227
148
  for (const account of accounts) {
228
149
  if (!githubAccountSupportsWorkflowChanges(account)) continue;
229
- const token = exec
230
- .spawnSync("gh", githubTokenArguments(identity.hostname, account.login), {
231
- cwd: root,
232
- stdout: "capture",
233
- stderr: "ignore",
234
- stdin: "ignore",
235
- check: false,
236
- })
237
- .stdout?.trim();
150
+ const token = captureTaskCommand(
151
+ root,
152
+ "gh",
153
+ githubTokenArguments(identity.hostname, account.login),
154
+ );
238
155
  if (!token) continue;
239
156
  const tokenEnvironment = {
240
157
  ...process.env,
@@ -242,16 +159,11 @@ function githubAccount(root: string): {
242
159
  GH_REPO: githubRepositorySpecifier(identity),
243
160
  [githubTokenEnvironmentName(identity.hostname)]: token,
244
161
  };
245
- const repositoryResult = exec.spawnSync("gh", ["api", githubRepositoryApiPath(identity)], {
246
- cwd: root,
247
- env: tokenEnvironment,
248
- stdout: "capture",
249
- stderr: "ignore",
250
- stdin: "ignore",
251
- check: false,
252
- });
253
- const repositoryData =
254
- 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
+ );
255
167
  const permissions = githubRepositoryPermissions(repositoryData);
256
168
  if (!permissions.push && !permissions.maintain && !permissions.admin) {
257
169
  continue;
@@ -303,12 +215,12 @@ program
303
215
  .addOption(releaseLevelOption())
304
216
  .option("--prefix <prefix>", "release tag prefix", "v")
305
217
  .option("--base <branch>", "release pull request base branch", "main")
218
+ .option("--python-root <path>", "Python package root", "packages/py")
306
219
  .option("--message <message>", "commit message for pending source work", "chore: prepare release")
307
220
  .addOption(releaseOperatingSystemOption())
308
221
  .addOption(releaseArchitectureOption())
309
222
  .option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
310
223
  .option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
311
- .option("--python-root <path>", "Python workspace package root")
312
224
  .option(
313
225
  "--validate-task <task>",
314
226
  "repository task to run before release validation; repeatable",
@@ -323,12 +235,11 @@ program
323
235
  .option("--no-validate", "skip repository validation tasks, Rust tests, and TypeScript compile")
324
236
  .option("--no-local-publish", "skip local npm, PyPI, and Cargo publication")
325
237
  .option("--no-local-cargo", "skip local Cargo publication")
326
- .option("--build <mode>", "native release build: auto, local, or remote", "auto")
327
238
  .option("--no-approve", "open the release pull request without enabling automatic merge")
328
239
  .option("--no-wait", "return after enabling automatic merge without watching publication")
329
240
  .option(
330
241
  "--wait-timeout-minutes <minutes>",
331
- "maximum time for pull request checks, merge, and publication",
242
+ "maximum time for pull request checks and merge",
332
243
  "120",
333
244
  )
334
245
  .action(
@@ -336,19 +247,18 @@ program
336
247
  level: VersionLevel;
337
248
  prefix: string;
338
249
  base: string;
250
+ pythonRoot: string;
339
251
  message: string;
340
252
  os: ReleaseOs[];
341
253
  arch: ReleaseArch[];
342
254
  localRegistry: string;
343
255
  localPypi: string;
344
- pythonRoot?: string;
345
256
  validateTask: string[];
346
257
  releaseSummary: boolean;
347
258
  releaseSummaryProviders?: string;
348
259
  validate: boolean;
349
260
  localPublish: boolean;
350
261
  localCargo: boolean;
351
- build: ReleaseBuildMode;
352
262
  approve: boolean;
353
263
  wait: boolean;
354
264
  waitTimeoutMinutes: string;
@@ -367,35 +277,26 @@ program
367
277
  throw new Error("--wait-timeout-minutes must be a positive integer");
368
278
  }
369
279
  const waitTimeoutMs = waitTimeoutMinutes * 60_000;
370
- const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
280
+ if (opts.approve && !opts.wait) {
281
+ throw new Error("automatic merge requires waiting so the draft candidate can be built");
282
+ }
283
+ const currentBranch = captureGitTaskCommand(root, ["branch", "--show-current"], {
284
+ check: true,
285
+ });
371
286
  if (!currentBranch) throw new Error("Release preparation requires a local branch");
372
287
  const account = githubAccount(root);
373
- if (!["auto", "local", "remote"].includes(opts.build)) {
374
- throw new Error("--build must be auto, local, or remote");
288
+ if (!account.canManage) {
289
+ throw new Error("Release preparation requires maintain or admin permission");
375
290
  }
376
- const inspectLocalTools =
377
- opts.build === "local" || (opts.build === "auto" && account.canManage);
378
- const localTools = inspectLocalTools ? missingLocalReleaseTools(root) : [];
379
- const buildMode = resolveReleaseBuildMode({
380
- requested: opts.build,
381
- canManage: account.canManage,
382
- localTools,
383
- hasLocalTargets: hasLocalReleaseTargets(root),
384
- approve: opts.approve,
385
- wait: opts.wait,
386
- });
387
- logger.info("selected native release build", {
388
- requested: opts.build,
389
- resolved: buildMode,
390
- manager: account.canManage,
391
- ...(localTools.length > 0 ? { missing: localTools } : {}),
392
- });
393
- git(root, ["fetch", "--tags", "origin", opts.base]);
394
- const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
395
- fetch: false,
396
- includeComponentTags: false,
397
- }).version;
398
- const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
291
+ if (hasLocalReleaseTargets(root)) {
292
+ const localTools = missingLocalReleaseTools(root);
293
+ if (localTools.length > 0) {
294
+ throw new Error(`Local release candidate build requires: ${localTools.join(", ")}`);
295
+ }
296
+ }
297
+ runGitTaskCommand(root, ["fetch", "--tags", "origin", opts.base]);
298
+ const comparisonBase = resolveBaseVersion(root);
299
+ const next = resolveNextVersion(root, opts.level);
399
300
  const releaseVersionScript = fileURLToPath(
400
301
  new URL("./release-version.ts", import.meta.url),
401
302
  );
@@ -409,236 +310,278 @@ program
409
310
  ]);
410
311
  const releaseTag = `${opts.prefix}${next.version}`;
411
312
  const releaseBranch = `release/${releaseTag}`;
412
- const releaseRoot = join(root, ".worktrees", releaseTag);
413
313
  if (currentBranch.startsWith("release/")) {
414
314
  throw new Error("Release preparation must start from a source branch");
415
315
  }
416
316
  if (
417
- git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
418
- capture: true,
419
- check: false,
420
- })
317
+ captureGitTaskCommand(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`])
421
318
  ) {
422
319
  throw new Error(`Release tag already exists: ${releaseTag}`);
423
320
  }
424
321
 
425
- const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
426
- capture: true,
427
- });
322
+ const status = captureGitTaskCommand(
323
+ root,
324
+ ["status", "--porcelain=v1", "--untracked-files=all"],
325
+ { check: true },
326
+ );
428
327
  if (status) {
429
- git(root, ["add", "-A"]);
430
- git(root, ["commit", "-m", opts.message]);
328
+ runGitTaskCommand(root, ["add", "-A"]);
329
+ runGitTaskCommand(root, ["commit", "-m", opts.message]);
431
330
  }
432
- if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
433
- 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}`]);
434
340
  }
435
341
  pushCurrentBranch(root, currentBranch);
436
-
437
- const worktreeExists = existsSync(join(releaseRoot, ".git"));
438
- if (!worktreeExists) {
439
- git(root, ["worktree", "prune"]);
440
- const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
441
- const remoteBranch = git(
442
- root,
443
- ["ls-remote", "--heads", "origin", `refs/heads/${releaseBranch}`],
444
- { capture: true, check: false },
445
- );
446
- if (localBranch || remoteBranch) {
447
- throw new Error(`Release branch already exists without its worktree: ${releaseBranch}`);
448
- }
449
- git(root, ["worktree", "add", "-b", releaseBranch, releaseRoot, "HEAD"]);
450
- } else {
451
- const releaseStatus = git(
452
- releaseRoot,
453
- ["status", "--porcelain=v1", "--untracked-files=all"],
454
- { capture: true },
455
- );
456
- if (releaseStatus) {
457
- git(releaseRoot, [
458
- "stash",
459
- "push",
460
- "--include-untracked",
461
- "--message",
462
- "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}`,
463
364
  ]);
365
+ } else {
366
+ runGitTaskCommand(root, ["switch", "-c", releaseBranch]);
464
367
  }
465
- if (!gitSucceeds(releaseRoot, ["merge-base", "--is-ancestor", currentBranch, "HEAD"])) {
466
- git(releaseRoot, ["merge", "--no-edit", currentBranch]);
467
- }
468
- if (releaseStatus) {
469
- 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]);
470
381
  }
471
- logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
472
- }
473
- runTaskCommand(releaseRoot, process.execPath, ["install"]);
474
-
475
- const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
476
- const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
477
- runTaskCommand(releaseRoot, process.execPath, [
478
- bumpScript,
479
- "--level",
480
- opts.level,
481
- "--prefix",
482
- opts.prefix,
483
- ...opts.os.flatMap((value) => ["--os", value]),
484
- ...opts.arch.flatMap((value) => ["--arch", value]),
485
- ]);
486
- if (readWorkspaceVersion(releaseRoot) !== next.version) {
487
- throw new Error(`Release preparation did not produce ${next.version}`);
488
- }
382
+ runTaskCommand(root, process.execPath, ["install"]);
489
383
 
490
- runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
491
- if (opts.validate) {
492
- for (const task of opts.validateTask) {
493
- runTaskCommand(releaseRoot, process.execPath, ["run", task]);
494
- }
495
- if (existsSync(join(releaseRoot, "Cargo.toml"))) {
496
- runTaskCommand(releaseRoot, "cargo", [
497
- "test",
498
- "--workspace",
499
- ...(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]),
500
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}`);
501
405
  }
502
- runTaskCommand(releaseRoot, process.execPath, ["run", "compile"]);
503
- } else {
504
- logger.warn("release validation skipped by --no-validate");
505
- }
506
- if (opts.localPublish) {
507
- await publishLocalRelease({
508
- root: releaseRoot,
509
- version: next.version,
510
- localRegistry: opts.localRegistry,
511
- localPypi: opts.localPypi,
512
- pythonRoot: opts.pythonRoot,
513
- localCargo: opts.localCargo,
514
- reuseValidatedNodeCompile: opts.validate,
515
- });
516
- } else {
517
- logger.info("local publication skipped by --no-local-publish");
518
- }
519
- runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
520
- const releaseSummary = opts.releaseSummary
521
- ? await generateReleaseSummary({
522
- root: releaseRoot,
523
- version: next.version,
524
- fromRef: `${opts.prefix}${comparisonBase}`,
525
- providers: releaseSummaryProviders(opts.releaseSummaryProviders),
526
- })
527
- : undefined;
528
406
 
529
- git(releaseRoot, ["add", "-A"]);
530
- const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
531
- if (staged) {
532
- git(releaseRoot, [
533
- "commit",
534
- "-m",
535
- `chore(release): ${next.version}`,
536
- "-m",
537
- `Release-Build: ${buildMode}`,
538
- ]);
539
- } else if (!worktreeExists) {
540
- throw new Error("Release preparation produced no changes");
541
- }
542
- // The source push and release commit hooks have already scanned every new
543
- // byte. A new remote branch has no upstream comparison point, so the
544
- // managed pre-push hook would rescan the repository's complete history.
545
- 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;
546
432
 
547
- const title = `chore(release): ${next.version}`;
548
- const body = [
549
- `Release ${releaseTag}.`,
550
- "",
551
- `Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
552
- "",
553
- "Merging this PR updates VERSION on main and starts the public release workflow.",
554
- "",
555
- `Native build: ${buildMode}.`,
556
- ...(releaseSummary ? ["", releaseSummary] : []),
557
- ].join("\n");
558
- const githubEnvironment = {
559
- ...process.env,
560
- GH_HOST: account.hostname,
561
- GH_REPO: githubRepositorySpecifier(account),
562
- [githubTokenEnvironmentName(account.hostname)]: account.token,
563
- };
564
- const ensurePullRequest = (): void => {
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]);
443
+
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
+ };
565
459
  if (
566
- taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
460
+ !taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
567
461
  env: githubEnvironment,
568
462
  })
569
463
  ) {
570
- return;
571
- }
572
- runTaskCommand(
573
- root,
574
- "gh",
575
- [
576
- "pr",
577
- "create",
578
- "--base",
579
- opts.base,
580
- "--head",
581
- releaseBranch,
582
- "--title",
583
- title,
584
- "--body",
585
- body,
586
- ],
587
- { env: githubEnvironment },
588
- );
589
- };
590
- ensurePullRequest();
591
- if (opts.approve) {
592
- runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
593
- env: githubEnvironment,
594
- });
595
- }
596
- if (opts.approve && opts.wait) {
597
- const mergeSha = await waitForPullRequestMerge(
598
- root,
599
- releaseBranch,
600
- githubEnvironment,
601
- waitTimeoutMs,
602
- );
603
- logger.info("release pull request merged", { releaseBranch, mergeSha });
604
- if (buildMode === "local") {
605
- git(root, ["fetch", "origin", opts.base]);
606
- git(releaseRoot, ["switch", "--detach", mergeSha]);
607
- runTaskCommand(releaseRoot, process.execPath, ["install"]);
608
- await waitForGitHubRelease(root, releaseTag, githubEnvironment, waitTimeoutMs);
609
- const releaseAssetsScript = join(
610
- releaseRoot,
611
- "node_modules/@dbx-tools/projen/tasks/release-assets.ts",
612
- );
613
464
  runTaskCommand(
614
- releaseRoot,
615
- process.execPath,
465
+ root,
466
+ "gh",
616
467
  [
617
- releaseAssetsScript,
618
- "--root",
619
- releaseRoot,
620
- "--version",
621
- next.version,
622
- "--tag",
623
- releaseTag,
624
- "--upload",
468
+ "pr",
469
+ "create",
470
+ "--base",
471
+ opts.base,
472
+ "--head",
473
+ releaseBranch,
474
+ "--title",
475
+ title,
476
+ "--body",
477
+ body,
625
478
  ],
626
479
  { env: githubEnvironment },
627
480
  );
628
481
  }
629
- git(root, ["worktree", "remove", "--force", releaseRoot]);
630
- git(root, ["branch", "--delete", "--force", releaseBranch]);
631
- await waitForReleaseWorkflow(root, opts.base, mergeSha, githubEnvironment, waitTimeoutMs);
632
- logger.success(`published ${releaseTag} from ${mergeSha}`);
633
- return;
482
+ if (opts.approve) {
483
+ runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
484
+ env: githubEnvironment,
485
+ });
486
+ }
487
+ if (opts.approve && opts.wait) {
488
+ const mergeSha = await waitForPullRequestMerge(
489
+ root,
490
+ releaseBranch,
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 = [
503
+ releaseCandidateScript,
504
+ "--root",
505
+ root,
506
+ "--version",
507
+ next.version,
508
+ "--tag",
509
+ releaseTag,
510
+ "--sha",
511
+ mergeSha,
512
+ "--python-root",
513
+ opts.pythonRoot,
514
+ "--notes-file",
515
+ `docs/releases/v${next.version}.md`,
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
+ }
634
584
  }
635
- git(root, ["worktree", "remove", "--force", releaseRoot]);
636
- git(root, ["branch", "--delete", "--force", releaseBranch]);
637
- logger.success(
638
- `${
639
- opts.approve ? "enabled automatic merge for" : "opened"
640
- } ${releaseBranch} for ${releaseTag}${opts.approve && !opts.wait ? " without waiting" : ""}`,
641
- );
642
585
  });
643
586
  },
644
587
  );