@dbx-tools/projen 0.6.199 → 0.6.201

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/README.md CHANGED
@@ -130,8 +130,9 @@ without Rust crates start no Rust watcher. When Rust projects are detected,
130
130
  Cargo is required and the focused task fails immediately if it is unavailable.
131
131
  `rs:bindings` is explicit rather than part of root `pre-compile`, so ordinary
132
132
  JavaScript PR builds type-check committed generated bindings without performing
133
- a host Rust build. Release preparation runs Cargo workspace tests and
134
- regenerates bindings before JavaScript validation.
133
+ a host Rust build. Regenerate bindings through the focused watcher or explicit
134
+ task while changing a UniFFI API. Release preparation runs Cargo workspace tests
135
+ without invoking UBRN.
135
136
 
136
137
  The workspace generates one `release.yml` workflow for every ecosystem. A push
137
138
  to the configured release branch starts it only when `VERSION` changed. The
@@ -399,14 +400,17 @@ a new package is covered without a re-synth. Work from the root:
399
400
  | `bun run version:check` | verify every version surface matches `VERSION` |
400
401
  | `bun run release` | run full local preflight and open a reviewed release PR |
401
402
 
402
- `release` commits pending work on the current branch, pushes it, creates a
403
- dedicated `release/v<version>` branch, invokes the pure `bump` task, validates
404
- the complete workspace, runs local publication, and opens a PR into the
405
- configured release branch. `--message` sets the source commit message. The task
406
- never merges the PR unless `--approve` is passed; that option requests an
407
- immediate admin merge so the main release can start without waiting for PR
408
- checks. npm uses `npm config get registry` and publishes to a local Verdaccio
409
- automatically. Publishable JavaScript members compile once
403
+ `release` commits pending work on the current branch, merges the latest remote
404
+ release branch when needed, and pushes it. It prepares the dedicated
405
+ `release/v<version>` branch inside `.worktrees/<tag>`, leaving the source
406
+ checkout on its current branch throughout validation, local publication, and
407
+ PR creation. Failed preparation can resume from that worktree; success removes
408
+ it. `--message` sets the source commit message. The task never merges
409
+ the PR unless `--approve` is passed; that option merges the release branch
410
+ directly through GitHub's merge API so no PR checks are created. A repository
411
+ that blocks direct merges falls back to an immediate admin-merged PR. npm uses
412
+ `npm config get registry` and publishes to a local Verdaccio automatically.
413
+ Publishable JavaScript members compile once
410
414
  from the root in parallel, then upload through a bounded pool without rerunning
411
415
  their `prepack` tasks. Python prefers uv's default index and only
412
416
  treats a loopback `.../+simple/` URL as writable devpi; a read-only cache such as
@@ -422,8 +426,8 @@ auto-detection; `--python-root` defaults to `packages/py`.
422
426
 
423
427
  The GitHub PR workflow runs synth plus the workspace TypeScript compile rather
424
428
  than the complete root build. Release preparation has already run Rust tests,
425
- binding generation, workspace tests, and local package preflight before opening
426
- the PR.
429
+ workspace type-checking, and local package preflight before opening the PR.
430
+ JavaScript behavior tests remain an explicit developer task.
427
431
 
428
432
  The configured release branch publishes only when a reviewed PR changes
429
433
  `VERSION`. Its workflow creates the annotated `v*` public release boundary.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.6.199";
6
+ export const PACKAGE_VERSION = "0.6.201";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
package/package.json CHANGED
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@clack/prompts": "^1.7.0",
28
- "@dbx-tools/core": "0.6.199",
29
- "@dbx-tools/path": "0.6.199",
30
- "@dbx-tools/shared-core": "0.6.199",
28
+ "@dbx-tools/core": "0.6.201",
29
+ "@dbx-tools/path": "0.6.201",
30
+ "@dbx-tools/shared-core": "0.6.201",
31
31
  "commander": "^15.0.0",
32
32
  "concurrently": "^10.0.3",
33
33
  "constructs": "^10.6.0",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "main": "index.ts",
49
49
  "license": "Apache-2.0",
50
- "version": "0.6.199",
50
+ "version": "0.6.201",
51
51
  "types": "index.ts",
52
52
  "type": "module",
53
53
  "exports": {
package/src/project-js.ts CHANGED
@@ -863,17 +863,36 @@ class WorkflowDefaults extends Component {
863
863
  }
864
864
  }
865
865
  const build = this.project.tryFindObjectFile(".github/workflows/build.yml");
866
+ build?.addOverride("on.pull_request.types", ["opened", "synchronize", "reopened", "closed"]);
867
+ build?.addOverride(
868
+ "jobs.build.if",
869
+ "${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}",
870
+ );
866
871
  for (const job of ["build", "self-mutation"]) {
867
872
  build?.addOverride(`jobs.${job}.timeout-minutes`, 30);
868
873
  }
869
- this.project
870
- .tryFindObjectFile(".github/workflows/pull-request-lint.yml")
871
- ?.addOverride("jobs.validate.timeout-minutes", 10);
874
+ const pullRequestLint = this.project.tryFindObjectFile(
875
+ ".github/workflows/pull-request-lint.yml",
876
+ );
877
+ pullRequestLint?.addOverride("on.pull_request_target.types", [
878
+ "labeled",
879
+ "opened",
880
+ "synchronize",
881
+ "reopened",
882
+ "ready_for_review",
883
+ "edited",
884
+ "closed",
885
+ ]);
886
+ pullRequestLint?.addOverride(
887
+ "jobs.validate.if",
888
+ "(github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.action != 'closed'",
889
+ );
890
+ pullRequestLint?.addOverride("jobs.validate.timeout-minutes", 10);
872
891
  for (const name of ["build", "pull-request-lint"]) {
873
892
  const workflow = this.project.tryFindObjectFile(`.github/workflows/${name}.yml`);
874
893
  workflow?.addOverride("permissions", { contents: "read" });
875
894
  workflow?.addOverride("concurrency", {
876
- group: "${{ github.workflow }}-${{ github.ref }}",
895
+ group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}",
877
896
  "cancel-in-progress": true,
878
897
  });
879
898
  }
package/src/project-rs.ts CHANGED
@@ -340,7 +340,7 @@ if (process.env.GITHUB_OUTPUT) {
340
340
  /** Keep tracked workspace package versions in Cargo.lock aligned with VERSION. */
341
341
  class RustWorkspaceVersionLock extends Component {
342
342
  public override postSynthesize(): void {
343
- exec.spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
343
+ exec.spawnSync("cargo", ["metadata", "--format-version", "1"], {
344
344
  cwd: this.project.outdir,
345
345
  stdout: "ignore",
346
346
  stderr: "inherit",
@@ -95,11 +95,21 @@ function run(root: string, command: string, args: string[], env?: NodeJS.Process
95
95
  });
96
96
  }
97
97
 
98
- function githubAccount(root: string): { owner: string; token: string } {
98
+ function runIgnoringStdout(root: string, command: string, args: string[]): void {
99
+ exec.spawnSync(command, args, {
100
+ cwd: root,
101
+ stdout: "ignore",
102
+ stderr: "inherit",
103
+ stdin: "ignore",
104
+ check: true,
105
+ });
106
+ }
107
+
108
+ function githubAccount(root: string): { owner: string; repository: string; token: string } {
99
109
  const repository = project.repositoryUrl(root);
100
110
  if (!repository) throw new Error("Release preparation requires a GitHub repository");
101
- const owner = new URL(repository).pathname.split("/").filter(Boolean)[0];
102
- if (!owner) throw new Error(`Cannot determine GitHub owner from ${repository}`);
111
+ const [owner, name] = new URL(repository).pathname.split("/").filter(Boolean);
112
+ if (!owner || !name) throw new Error(`Cannot determine GitHub identity from ${repository}`);
103
113
  const token = exec
104
114
  .spawnSync("gh", ["auth", "token", "--user", owner], {
105
115
  cwd: root,
@@ -110,7 +120,7 @@ function githubAccount(root: string): { owner: string; token: string } {
110
120
  })
111
121
  .stdout?.trim();
112
122
  if (!token) throw new Error(`No GitHub CLI authentication found for ${owner}`);
113
- return { owner, token };
123
+ return { owner, repository: name.replace(/\.git$/, ""), token };
114
124
  }
115
125
 
116
126
  const program = new Command();
@@ -138,7 +148,7 @@ program
138
148
  .option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
139
149
  .option("--python-root <path>", "Python workspace package root", "packages/py")
140
150
  .option("--no-local-cargo", "skip local Cargo publication")
141
- .option("--approve", "merge the release PR immediately with admin bypass")
151
+ .option("--approve", "merge the release branch directly into the release base")
142
152
  .action(
143
153
  async (opts: {
144
154
  level: VersionLevel;
@@ -161,9 +171,9 @@ program
161
171
  const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
162
172
  const releaseTag = `${opts.prefix}${next.version}`;
163
173
  const releaseBranch = `release/${releaseTag}`;
164
- const resuming = currentBranch === releaseBranch;
165
- if (currentBranch.startsWith("release/") && !resuming) {
166
- throw new Error(`Expected release branch ${releaseBranch}, got ${currentBranch}`);
174
+ const releaseRoot = join(root, ".worktrees", releaseTag);
175
+ if (currentBranch.startsWith("release/")) {
176
+ throw new Error("Release preparation must start from a source branch");
167
177
  }
168
178
  if (
169
179
  git(root, ["ls-remote", "--tags", "origin", `refs/tags/${releaseTag}`], {
@@ -174,19 +184,21 @@ program
174
184
  throw new Error(`Release tag already exists: ${releaseTag}`);
175
185
  }
176
186
 
177
- if (!resuming) {
178
- if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
179
- throw new Error(`Current branch must contain origin/${opts.base}`);
180
- }
181
- const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
182
- capture: true,
183
- });
184
- if (status) {
185
- git(root, ["add", "-A"]);
186
- git(root, ["commit", "-m", opts.message]);
187
- }
188
- pushCurrentBranch(root, currentBranch);
187
+ const status = git(root, ["status", "--porcelain=v1", "--untracked-files=all"], {
188
+ capture: true,
189
+ });
190
+ if (status) {
191
+ git(root, ["add", "-A"]);
192
+ git(root, ["commit", "-m", opts.message]);
193
+ }
194
+ if (!gitSucceeds(root, ["merge-base", "--is-ancestor", `origin/${opts.base}`, "HEAD"])) {
195
+ git(root, ["merge", "--no-edit", `origin/${opts.base}`]);
196
+ }
197
+ pushCurrentBranch(root, currentBranch);
189
198
 
199
+ const worktreeExists = existsSync(join(releaseRoot, ".git"));
200
+ if (!worktreeExists) {
201
+ git(root, ["worktree", "prune"]);
190
202
  const localBranch = git(root, ["branch", "--list", releaseBranch], { capture: true });
191
203
  const remoteBranch = git(
192
204
  root,
@@ -194,16 +206,31 @@ program
194
206
  { capture: true, check: false },
195
207
  );
196
208
  if (localBranch || remoteBranch) {
197
- throw new Error(`Release branch already exists: ${releaseBranch}`);
209
+ throw new Error(`Release branch already exists without its worktree: ${releaseBranch}`);
198
210
  }
199
- git(root, ["switch", "--create", releaseBranch]);
211
+ git(root, ["worktree", "add", "-b", releaseBranch, releaseRoot, "HEAD"]);
212
+ run(releaseRoot, process.execPath, ["install"]);
200
213
  } else {
201
- logger.info(`resuming ${releaseBranch}`);
214
+ const releaseStatus = git(
215
+ releaseRoot,
216
+ ["status", "--porcelain=v1", "--untracked-files=all"],
217
+ { capture: true },
218
+ );
219
+ if (releaseStatus) {
220
+ git(releaseRoot, ["stash", "push", "--include-untracked", "--message", "release-resume"]);
221
+ }
222
+ if (!gitSucceeds(releaseRoot, ["merge-base", "--is-ancestor", currentBranch, "HEAD"])) {
223
+ git(releaseRoot, ["merge", "--no-edit", currentBranch]);
224
+ }
225
+ if (releaseStatus) {
226
+ git(releaseRoot, ["stash", "pop"]);
227
+ }
228
+ logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
202
229
  }
203
230
 
204
231
  const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
205
232
  const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
206
- run(root, process.execPath, [
233
+ run(releaseRoot, process.execPath, [
207
234
  bumpScript,
208
235
  "--level",
209
236
  opts.level,
@@ -212,50 +239,55 @@ program
212
239
  ...opts.os.flatMap((value) => ["--os", value]),
213
240
  ...opts.arch.flatMap((value) => ["--arch", value]),
214
241
  ]);
215
- if (readWorkspaceVersion(root) !== next.version) {
242
+ if (readWorkspaceVersion(releaseRoot) !== next.version) {
216
243
  throw new Error(`Release preparation did not produce ${next.version}`);
217
244
  }
218
245
 
219
- run(root, process.execPath, [versionCheckScript]);
220
- if (existsSync(join(root, "Cargo.toml"))) {
221
- run(root, "cargo", [
246
+ if (existsSync(join(releaseRoot, "Cargo.toml"))) {
247
+ runIgnoringStdout(releaseRoot, "cargo", ["metadata", "--format-version", "1"]);
248
+ }
249
+ run(releaseRoot, process.execPath, [versionCheckScript]);
250
+ if (existsSync(join(releaseRoot, "Cargo.toml"))) {
251
+ run(releaseRoot, "cargo", [
222
252
  "test",
223
253
  "--workspace",
224
- ...(existsSync(join(root, "Cargo.lock")) ? ["--locked"] : []),
254
+ ...(existsSync(join(releaseRoot, "Cargo.lock")) ? ["--locked"] : []),
225
255
  ]);
226
- run(root, process.execPath, ["run", "rs:bindings"]);
227
256
  }
228
- run(root, process.execPath, ["run", "compile"]);
229
- run(root, process.execPath, ["run", "test"]);
257
+ run(releaseRoot, process.execPath, ["run", "compile"]);
230
258
  await publishLocalRelease({
231
- root,
259
+ root: releaseRoot,
232
260
  version: next.version,
233
261
  localRegistry: opts.localRegistry,
234
262
  localPypi: opts.localPypi,
235
263
  pythonRoot: opts.pythonRoot,
236
264
  localCargo: opts.localCargo,
237
265
  });
238
- run(root, process.execPath, [versionCheckScript]);
266
+ run(releaseRoot, process.execPath, [versionCheckScript]);
239
267
 
240
- git(root, ["add", "-A"]);
241
- const staged = git(root, ["diff", "--cached", "--name-only"], { capture: true });
268
+ git(releaseRoot, ["add", "-A"]);
269
+ const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
242
270
  if (staged) {
243
- git(root, ["commit", "-m", `chore(release): ${next.version}`]);
244
- } else if (!resuming) {
271
+ git(releaseRoot, ["commit", "-m", `chore(release): ${next.version}`]);
272
+ } else if (!worktreeExists) {
245
273
  throw new Error("Release preparation produced no changes");
246
274
  }
247
- git(root, ["push", "--set-upstream", "origin", releaseBranch]);
275
+ // The source push and release commit hooks have already scanned every new
276
+ // byte. A new remote branch has no upstream comparison point, so the
277
+ // managed pre-push hook would rescan the repository's complete history.
278
+ git(releaseRoot, ["push", "--no-verify", "--set-upstream", "origin", releaseBranch]);
248
279
 
249
280
  const title = `chore(release): ${next.version}`;
250
281
  const body = [
251
282
  `Release ${releaseTag}.`,
252
283
  "",
253
- `Source commit: ${git(root, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
284
+ `Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
254
285
  "",
255
286
  "Merging this PR updates VERSION on main and starts the public release workflow.",
256
287
  ].join("\n");
257
288
  const githubEnvironment = { ...process.env, GH_TOKEN: account.token };
258
- if (!commandSucceeds(root, "gh", ["pr", "view", releaseBranch], githubEnvironment)) {
289
+ const ensurePullRequest = (): void => {
290
+ if (commandSucceeds(root, "gh", ["pr", "view", releaseBranch], githubEnvironment)) return;
259
291
  run(
260
292
  root,
261
293
  "gh",
@@ -273,11 +305,35 @@ program
273
305
  ],
274
306
  githubEnvironment,
275
307
  );
276
- }
308
+ };
277
309
  if (opts.approve) {
278
- run(root, "gh", ["pr", "merge", releaseBranch, "--admin", "--merge"], githubEnvironment);
310
+ const merged = commandSucceeds(
311
+ root,
312
+ "gh",
313
+ [
314
+ "api",
315
+ "--method",
316
+ "POST",
317
+ `repos/${account.owner}/${account.repository}/merges`,
318
+ "-f",
319
+ `base=${opts.base}`,
320
+ "-f",
321
+ `head=${releaseBranch}`,
322
+ "-f",
323
+ `commit_message=Merge ${releaseTag}`,
324
+ ],
325
+ githubEnvironment,
326
+ );
327
+ if (!merged) {
328
+ ensurePullRequest();
329
+ run(root, "gh", ["pr", "merge", releaseBranch, "--admin", "--merge"], githubEnvironment);
330
+ }
331
+ git(releaseRoot, ["push", "--no-verify", "origin", "--delete", releaseBranch]);
332
+ } else {
333
+ ensurePullRequest();
279
334
  }
280
- if (!resuming) git(root, ["switch", currentBranch]);
335
+ git(root, ["worktree", "remove", "--force", releaseRoot]);
336
+ git(root, ["branch", "--delete", "--force", releaseBranch]);
281
337
  logger.success(`${opts.approve ? "merged" : "opened"} ${releaseBranch} for ${releaseTag}`);
282
338
  },
283
339
  );