@dbx-tools/projen 0.6.194 → 0.6.196

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/tasks/bump.ts CHANGED
@@ -1,68 +1,16 @@
1
1
  #!/usr/bin/env -S bun
2
2
  /**
3
- * `projen bump` - compute the next release version, write it to the workspace
4
- * `VERSION` file, synth so every manifest copies it, then (by default) commit,
5
- * tag, and push. Pushing the tag is what triggers the release workflow.
6
- *
7
- * The base version is the HIGHEST published git tag across `<prefix>` and every
8
- * `--sibling` prefix (fetched from the remote so a release cut elsewhere wins),
9
- * falling back to the local `VERSION` file when the remote is unreachable or has
10
- * no matching tag. A local file that is ahead does NOT override an existing
11
- * remote tag. The base is then incremented by `--level` (patch | minor | major;
12
- * default patch). The remote is consulted only here and on VERSION bootstrap,
13
- * never on an ordinary synth.
14
- *
15
- * `--sibling <dir>:<tagPrefix>` (repeatable) releases an in-repo project that
16
- * publishes on its OWN tag namespace (e.g. `projen/`, tagged `projen-v*`) at the
17
- * SAME version as the root, in the same run: its `<tagPrefix><version>` tag is
18
- * cut and pushed (triggering its own workflow), and it is included in the
19
- * local-registry publish. Taking the base version from every prefix at once is
20
- * what keeps the two in lockstep: the engine sat at 0.1.24 while the packages
21
- * reached 0.3.41 precisely because each namespace only ever looked at its own
22
- * tags. Both draw the fallback from the one root `VERSION` file, so the engine
23
- * and the packages share a single source of truth.
24
- *
25
- * Flags (all default ON; negate with the `--no-` form, per commander):
26
- * --synth / --no-synth synth after writing VERSION so manifests copy it
27
- * --version / --no-version write the bumped version into `VERSION`
28
- * --commit / --no-commit commit the release (staged with `git add -A`)
29
- * --tag / --no-tag create the `<prefix><version>` git tag
30
- * --push / --no-push push the CURRENT branch + tag to origin
31
- *
32
- * `--publish` / `--no-publish` is an alias for `--push` (pushing the tag is
33
- * what publishes). The tag prefix comes from `--prefix` (default `v`).
34
- *
35
- * `--local-registry <value>` publishes npm packages to a LOCAL registry (e.g.
36
- * verdaccio) right after the tag push. `--local-pypi <value>` does the same for
37
- * Python packages through a writable devpi index. Values for both:
38
- * - `auto` (default): publish only when `npm config get registry` is a
39
- * loopback host, or when ANY active Python index — the primary
40
- * `index-url` OR any `extra-index-url`, across uv and pip — is a loopback
41
- * devpi URL. Scanning the extras is what lets the corp proxy stay the
42
- * primary index while a local devpi added as an extra is the detected
43
- * publish target. The deploy endpoint for that index is taken from the
44
- * GLOBAL uv config — an explicit `publish-url` on the matching `[[index]]`
45
- * (or `UV_PUBLISH_URL`) — falling back to deriving it from the `+simple`
46
- * URL shape when no such setting exists.
47
- * - `false`: never publish locally.
48
- * - a URL: always publish to that registry.
3
+ * Compute the next workspace version, write VERSION, and synthesize every
4
+ * generated version surface. This task has no git, registry, or publication
5
+ * side effects; release preparation owns those operations.
49
6
  */
50
- import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
51
- import { homedir } from "node:os";
52
- import { join, resolve } from "node:path";
53
- import { fileURLToPath } from "node:url";
54
7
  import { exec, project } from "@dbx-tools/core";
55
- import { log, net } from "@dbx-tools/shared-core";
8
+ import { log } from "@dbx-tools/shared-core";
56
9
  import { Command, Option } from "commander";
57
- import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
58
10
  import { releasePlatformFilter } from "../src/_release-platform.ts";
59
- import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
60
11
  import {
61
- type Semver,
62
- compareSemver,
63
- latestTagVersion,
64
- parseSemver,
65
- resolveBaseVersion,
12
+ type VersionLevel,
13
+ resolveNextVersion,
66
14
  writeWorkspaceVersion,
67
15
  } from "../src/workspace-version.ts";
68
16
 
@@ -70,129 +18,20 @@ const logger = log.logger("projen:bump");
70
18
  const LEVELS = ["patch", "minor", "major"] as const;
71
19
  const RELEASE_OSES = ["darwin", "linux", "win32"] as const;
72
20
  const RELEASE_ARCHES = ["arm64", "x64"] as const;
73
- type Level = (typeof LEVELS)[number];
74
21
  type ReleaseOs = (typeof RELEASE_OSES)[number];
75
22
  type ReleaseArch = (typeof RELEASE_ARCHES)[number];
76
23
 
77
- /** A standalone in-repo project released alongside the root, on its own tag prefix. */
78
- interface Sibling {
79
- /** Repo-relative directory, e.g. `projen`. */
80
- readonly dir: string;
81
- /** Git tag prefix, disjoint from the root's, e.g. `projen-v`. */
82
- readonly prefix: string;
83
- }
84
-
85
- /**
86
- * Commander collector for the repeatable `--sibling <dir>:<tagPrefix>`. Split on
87
- * the LAST colon so a directory containing one still parses.
88
- */
89
- function parseSibling(value: string, previous: Sibling[]): Sibling[] {
90
- const at = value.lastIndexOf(":");
91
- if (at <= 0 || at === value.length - 1) {
92
- throw new Error(`--sibling expects <dir>:<tagPrefix>, got "${value}"`);
93
- }
94
- return [...previous, { dir: value.slice(0, at), prefix: value.slice(at + 1) }];
95
- }
96
-
97
24
  function collectValue<T extends string>(value: T, previous: T[]): T[] {
98
25
  return [...previous, value];
99
26
  }
100
27
 
101
- function increment(v: Semver, level: Level): Semver {
102
- if (level === "major") return [v[0] + 1, 0, 0];
103
- if (level === "minor") return [v[0], v[1] + 1, 0];
104
- return [v[0], v[1], v[2] + 1];
105
- }
106
-
107
- function git(args: string[], capture = false): string {
108
- const res = exec.spawnSync("git", args, {
109
- cwd: process.cwd(),
110
- stdout: capture ? "capture" : "inherit",
111
- stderr: capture ? "ignore" : "inherit",
112
- stdin: "ignore",
113
- check: !capture,
114
- });
115
- return res.stdout?.trim() ?? "";
116
- }
117
-
118
- function assertReleaseTagsPointToHead(tags: readonly string[]): void {
119
- const head = git(["rev-parse", "HEAD"], true);
120
- for (const tag of tags) {
121
- const target = git(["rev-parse", `${tag}^{commit}`], true);
122
- if (!head || target !== head) {
123
- throw new Error(`Release tag ${tag} does not point to the pushed HEAD commit`);
124
- }
125
- }
126
- }
127
-
128
- /**
129
- * Write ONLY the `version` field into a manifest projen owns (read-only, so
130
- * bracketed by a chmod that restores the mode). Used for the ROOT and the
131
- * standalone `projen/` sibling so the committed release marks the version.
132
- *
133
- * It deliberately does NOT rewrite `@scope/*` dep ranges. `projen/` is a workspace
134
- * member with `workspace:*` sibling deps; baking a `^version` for the just-bumped
135
- * (not-yet-published) version into the COMMITTED manifest breaks the release
136
- * workflow's initial `bun install`, which checks out that commit before anything
137
- * is published. Sibling versions are resolved transiently at publish time instead:
138
- * The bump stamps every workspace member before committing so independently
139
- * synthesized members and the Bun lock resolve the same release version.
140
- */
141
- function writeManifestVersion(pkgPath: string, version: string): void {
142
- const { mode } = statSync(pkgPath);
143
- chmodSync(pkgPath, mode | 0o200);
144
- try {
145
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
146
- pkg.version = version;
147
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
148
- } finally {
149
- chmodSync(pkgPath, mode);
150
- }
151
- }
152
-
153
- /**
154
- * Resolve the `--local-registry` value to a registry URL to publish to, or
155
- * `undefined` to skip. `false` skips; a URL is used as-is; `auto` uses the
156
- * active npm registry only when it is a loopback host (a local verdaccio etc.).
157
- */
158
- function resolveLocalRegistry(value: string): string | undefined {
159
- const trimmed = value.trim();
160
- if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
161
- if (trimmed.toLowerCase() === "auto") {
162
- const registry = project.npmRegistry();
163
- return registry && net.isLoopbackHost(registry) ? registry.href : undefined;
164
- }
165
- return trimmed;
166
- }
167
-
168
- function localCargoRegistry(): string | undefined {
169
- if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
170
- const config = join(homedir(), ".cargo", "config.toml");
171
- if (!existsSync(config)) return undefined;
172
- const source = readFileSync(config, "utf8");
173
- const sections = [
174
- ...source.matchAll(/^\[registries\.([^\]]+)\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/gm),
175
- ];
176
- for (const section of sections) {
177
- const index = section[2]?.match(/^\s*index\s*=\s*["']([^"']+)["']/m)?.[1];
178
- if (index && net.isLoopbackHost(index.replace(/^sparse\+/, ""))) return section[1];
179
- }
180
- return undefined;
181
- }
182
-
183
28
  const program = new Command();
184
29
  program
185
- .description("Bump the release version, then commit, tag, and push it")
30
+ .description("Increment VERSION and synchronize generated workspace versions")
186
31
  .addOption(
187
32
  new Option("-l, --level <level>", "semver increment").choices([...LEVELS]).default("patch"),
188
33
  )
189
- .option("--prefix <prefix>", "git tag prefix", "v")
190
- .option(
191
- "--sibling <dir:prefix>",
192
- "standalone in-repo project (not a workspace member) to release at the same version, repeatable",
193
- parseSibling,
194
- [] as Sibling[],
195
- )
34
+ .option("--prefix <prefix>", "git tag prefix used to resolve the published base", "v")
196
35
  .addOption(
197
36
  new Option("--os <os>", "release operating system, repeatable; crossed with every --arch")
198
37
  .choices([...RELEASE_OSES])
@@ -205,97 +44,28 @@ program
205
44
  .argParser((value, previous: ReleaseArch[]) => collectValue(value as ReleaseArch, previous))
206
45
  .default([] as ReleaseArch[]),
207
46
  )
208
- // Declared in the `--no-` form so commander creates a boolean that defaults to
209
- // `true` and is turned off by `--no-synth` / `--no-version` / ... (the
210
- // positive `--synth` etc. also work and are no-ops on the default).
211
- .option("--no-synth", "do not run `projen` (synth) before bumping")
212
- .option("--no-version", "do not write the bumped version into package.json")
213
- .option("--no-commit", "do not commit the version change")
214
- .option("--no-tag", "do not create the git tag")
215
- .option("--no-push", "do not push the branch and tag to origin")
216
- // `--publish` is a friendlier alias for `--push` (pushing the tag publishes).
217
- .option("--no-publish", "alias for --no-push")
218
- .option(
219
- "--local-registry <value>",
220
- "publish locally after the tag push: 'auto' (only a loopback npm registry), 'false', or a registry URL",
221
- "auto",
222
- )
223
- .option(
224
- "--local-pypi <value>",
225
- "publish Python packages locally: 'auto' (any active index-url or extra-index-url that is a loopback devpi +simple), 'false', or a devpi URL",
226
- "auto",
227
- )
228
- .option("--python-root <path>", "Python workspace package root", "packages/py")
47
+ .option("--no-synth", "write VERSION without synchronizing generated files")
229
48
  .action(
230
- async (opts: {
231
- level: Level;
49
+ (opts: {
50
+ level: VersionLevel;
232
51
  prefix: string;
233
- sibling: Sibling[];
234
52
  os: ReleaseOs[];
235
53
  arch: ReleaseArch[];
236
54
  synth: boolean;
237
- version: boolean;
238
- commit: boolean;
239
- tag: boolean;
240
- push: boolean;
241
- publish: boolean;
242
- localPypi: string;
243
- localRegistry: string;
244
- pythonRoot: string;
245
55
  }) => {
246
- const pkgPath = resolve(process.cwd(), "package.json");
247
- if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
248
- const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
249
-
250
- const siblings = opts.sibling.map((s) => ({ ...s, pkgPath: resolve(s.dir, "package.json") }));
251
- for (const s of siblings) {
252
- if (!existsSync(s.pkgPath)) throw new Error(`--sibling ${s.dir}: no package.json there`);
253
- }
254
-
255
- // The `VERSION` file is the workspace source of truth and lives at the repo
256
- // root, even when this task runs from a subdirectory (`cd projen && bun run
257
- // bump`), so the engine and packages always share one number.
258
56
  const root = project.root() ?? process.cwd();
57
+ const next = resolveNextVersion(root, [opts.prefix], opts.level);
58
+ const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
259
59
 
260
- // Base = the highest published tag across EVERY namespace being released
261
- // (fetched from the remote so a release cut elsewhere wins), falling back to
262
- // the local `VERSION` file when the remote is unreachable or has no tag. A
263
- // local file that happens to be ahead does NOT override an existing remote.
264
- const prefixes = [opts.prefix, ...siblings.map((s) => s.prefix)];
265
- const baseInfo = resolveBaseVersion(root, prefixes);
266
- const base = parseSemver(baseInfo.version) ?? [0, 0, 1];
267
- const next = increment(base, opts.level);
268
- const version = next.join(".");
269
- const tags = prefixes.map((prefix) => `${prefix}${version}`);
270
60
  logger.info(
271
- `bump ${base.join(".")} -> ${version} (${opts.level}); tags ${tags.join(", ")}` +
272
- `${baseInfo.source === "remote" ? "" : " [no remote tag; used local VERSION]"}`,
61
+ `bump ${next.base} -> ${next.version} (${opts.level})` +
62
+ `${next.source === "remote" ? "" : " [no remote tag; used local VERSION]"}`,
273
63
  );
274
- // Note any tag namespace that trailed the base so a lockstep catch-up is visible.
275
- for (const prefix of prefixes) {
276
- const tagged = latestTagVersion(root, prefix);
277
- if (tagged && compareSemver(tagged, base) < 0) {
278
- logger.info(`${prefix}* was behind at ${tagged.join(".")}, catching it up`);
279
- }
280
- }
281
-
282
- const push = opts.push && opts.publish;
283
-
284
- // Write the source of truth first, then synth so every generated manifest
285
- // COPIES it - synth never invents, resets, or drifts a version on its own.
286
- if (opts.version) {
287
- writeWorkspaceVersion(root, version);
288
- // Keep the writable root/sibling manifests coherent even when synth is
289
- // skipped (`--no-synth`); synth would otherwise set the same value.
290
- writeManifestVersion(pkgPath, version);
291
- for (const s of siblings) writeManifestVersion(s.pkgPath, version);
292
- logger.info(`wrote version ${version} to ${root}/VERSION`);
293
- }
64
+ writeWorkspaceVersion(root, next.version);
294
65
 
295
66
  if (opts.synth) {
296
- logger.info("synthesizing (projen)");
297
- exec.spawnSync("bun", [".projenrc.ts"], {
298
- cwd: process.cwd(),
67
+ exec.spawnSync(process.execPath, [".projenrc.ts"], {
68
+ cwd: root,
299
69
  stdout: "inherit",
300
70
  stderr: "inherit",
301
71
  stdin: "ignore",
@@ -306,202 +76,7 @@ program
306
76
  },
307
77
  });
308
78
  }
309
- if (opts.version) {
310
- const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
311
- exec.spawnSync("bun", [publishScript, version, "--stamp-only"], {
312
- cwd: process.cwd(),
313
- stdout: "inherit",
314
- stderr: "inherit",
315
- stdin: "ignore",
316
- check: true,
317
- });
318
- if (existsSync(join(root, "Cargo.toml"))) {
319
- logger.info("refreshing Cargo.lock workspace versions");
320
- exec.spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
321
- cwd: process.cwd(),
322
- stdout: "ignore",
323
- stderr: "inherit",
324
- stdin: "ignore",
325
- check: true,
326
- });
327
- }
328
- }
329
-
330
- if (opts.commit) {
331
- // Stage the whole tree so the release commit captures the version bump
332
- // plus anything synth regenerated. Skip the commit when nothing changed.
333
- git(["add", "-A"]);
334
- const staged = git(["diff", "--cached", "--name-only"], true);
335
- if (staged) {
336
- git([
337
- "commit",
338
- "-m",
339
- `chore(release): ${version}`,
340
- "-m",
341
- "🌸 Shipped with Kanna — https://kanna.sh",
342
- "-m",
343
- "Co-Authored-By: Kanna <noreply@kanna.sh>\nKanna-Agent: codex/databricks-gpt-5-6-sol",
344
- ]);
345
- } else {
346
- logger.info("nothing to commit");
347
- }
348
- }
349
-
350
- if (opts.tag) {
351
- for (const t of tags) git(["tag", "-a", t, "-m", t]);
352
- logger.info(`tagged ${tags.join(", ")}`);
353
- }
354
-
355
- if (push) {
356
- git(["push", "origin", "HEAD"]);
357
- // The managed Databricks pre-push hook treats a newly created tag's
358
- // all-zero remote SHA as a branch with no upstream and scans hundreds of
359
- // historical commits. The release commit was already scanned by the
360
- // branch push immediately above, and each annotated tag points to that
361
- // exact commit, so bypass the hook only for the tag-ref transport.
362
- // Push every tag in one invocation so a partial push cannot release
363
- // only some namespaces at this version.
364
- if (opts.tag) {
365
- assertReleaseTagsPointToHead(tags);
366
- git(["push", "--no-verify", "origin", ...tags]);
367
- }
368
- logger.success(`pushed ${opts.tag ? tags.join(", ") : "HEAD"} to origin`);
369
- } else {
370
- logger.info("skipped push (--no-push / --no-publish)");
371
- }
372
-
373
- // Local registry (e.g. verdaccio): publish AFTER the tag push so the
374
- // GitHub release still owns the public registry. Skipped under
375
- // `--no-version` (nothing bumped to publish).
376
- const localRegistry = resolveLocalRegistry(opts.localRegistry);
377
- const publishToLocalRegistry = opts.version && localRegistry;
378
- if (opts.version === false && localRegistry) {
379
- logger.info("skipped local publish (--no-version left package.json unbumped)");
380
- }
381
- // Scan EVERY active index (primary index-url + every extra-index-url,
382
- // across uv and pip): auto-mode publishes to the first that is a loopback
383
- // devpi +simple, so the corp proxy stays primary and a local devpi added
384
- // as an extra index is the detected publish target.
385
- const activeIndexes = activePythonIndexes();
386
- const localPypi = resolveLocalPypi(opts.localPypi, activeIndexes);
387
- const pythonRoot = resolve(opts.pythonRoot);
388
- if (
389
- opts.localPypi.toLowerCase() === "auto" &&
390
- activeIndexes.some((index) => net.isLoopbackHost(index)) &&
391
- !localPypi
392
- ) {
393
- logger.info(
394
- `skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
395
- );
396
- }
397
- if (opts.version === false && localPypi) {
398
- logger.info("skipped local Python publish (--no-version left packages unstamped)");
399
- }
400
-
401
- // npm and Python touch disjoint package trees and registries. Start both
402
- // mirrors together so local publishing takes the slower duration rather
403
- // than the sum of both. Each child still owns its internal build/upload
404
- // ordering and restores its temporary manifest edits on exit.
405
- const localPublishes: Promise<unknown>[] = [];
406
- if (publishToLocalRegistry) {
407
- logger.info(`publishing ${version} to local registry ${localRegistry}`);
408
- // The shared driver skips stamping/install when synth already made the
409
- // workspace release-current, compiles publishable members once from the
410
- // root, then publishes through a bounded pool. Temporary publishConfig
411
- // edits are restored before this child exits.
412
- const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
413
- localPublishes.push(
414
- exec
415
- .spawn("bun", [publishScript, version, "--registry", localRegistry], {
416
- cwd: process.cwd(),
417
- stdout: "inherit",
418
- stderr: "inherit",
419
- stdin: "ignore",
420
- check: true,
421
- })
422
- .then(() => logger.success(`published ${version} to ${localRegistry}`)),
423
- );
424
- }
425
- if (opts.version && localPypi && existsSync(pythonRoot)) {
426
- logger.info(`publishing Python ${version} to local devpi ${localPypi.publishUrl}`);
427
- const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
428
- localPublishes.push(
429
- exec
430
- .spawn(
431
- "bun",
432
- [
433
- publishPythonScript,
434
- version,
435
- "--root",
436
- pythonRoot,
437
- "--index-url",
438
- localPypi.indexUrl,
439
- "--publish-url",
440
- localPypi.publishUrl,
441
- ],
442
- {
443
- cwd: process.cwd(),
444
- stdout: "inherit",
445
- stderr: "inherit",
446
- stdin: "ignore",
447
- check: true,
448
- },
449
- )
450
- .then(() => logger.success(`published Python ${version} to ${localPypi.publishUrl}`)),
451
- );
452
- }
453
- await Promise.all(localPublishes);
454
- if (opts.version && (publishToLocalRegistry || localPypi)) {
455
- const publishUniFFIScript = fileURLToPath(
456
- new URL("./publish-uniffi-local.ts", import.meta.url),
457
- );
458
- await exec.spawn(
459
- "bun",
460
- [
461
- publishUniFFIScript,
462
- "--version",
463
- version,
464
- ...(publishToLocalRegistry ? ["--registry", localRegistry] : []),
465
- ...(localPypi ? ["--pypi-publish-url", localPypi.publishUrl] : []),
466
- ],
467
- {
468
- cwd: process.cwd(),
469
- stdout: "inherit",
470
- stderr: "inherit",
471
- stdin: "ignore",
472
- check: true,
473
- },
474
- );
475
- logger.success(`published host-native bindings for ${version}`);
476
- }
477
- const rust = readDbxToolsConfig(repoRoot)?.rust;
478
- const hasRustCrates = Boolean(
479
- rust &&
480
- typeof rust === "object" &&
481
- !Array.isArray(rust) &&
482
- Array.isArray((rust as { crates?: unknown }).crates) &&
483
- (rust as { crates: unknown[] }).crates.length,
484
- );
485
- if (opts.version && hasRustCrates) {
486
- const cargoRegistry = localCargoRegistry();
487
- if (cargoRegistry) {
488
- const publishUniFFIScript = fileURLToPath(
489
- new URL("./publish-uniffi-local.ts", import.meta.url),
490
- );
491
- await exec.spawn(
492
- "bun",
493
- [publishUniFFIScript, "--version", version, "--cargo-registry", cargoRegistry],
494
- {
495
- cwd: process.cwd(),
496
- stdout: "inherit",
497
- stderr: "inherit",
498
- stdin: "ignore",
499
- check: true,
500
- },
501
- );
502
- logger.success(`published Rust ${version} to ${cargoRegistry}`);
503
- }
504
- }
79
+ logger.success(`workspace version synchronized at ${next.version}`);
505
80
  },
506
81
  );
507
82
 
package/tasks/clean.ts CHANGED
@@ -52,7 +52,7 @@ const picked = await clack.multiselect<string>({
52
52
  required: false,
53
53
  });
54
54
 
55
- if (clack.isCancel(picked)) {
55
+ if (typeof picked === "symbol") {
56
56
  clack.cancel("clean cancelled - nothing removed");
57
57
  process.exit(0);
58
58
  }