@dbx-tools/projen 0.9.19 → 0.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +83 -229
  2. package/index.ts +11 -18
  3. package/package.json +9 -5
  4. package/src/_task-command.ts +75 -0
  5. package/src/barrels.ts +1 -74
  6. package/src/module-exports.ts +175 -1
  7. package/src/openapi.ts +5 -72
  8. package/src/packages.ts +45 -0
  9. package/src/project-js.ts +5 -48
  10. package/src/project-predicate.ts +1 -2
  11. package/src/project-py.ts +87 -199
  12. package/src/project.ts +2 -2
  13. package/src/python-node-bindings.ts +138 -0
  14. package/src/python-node-bundle.ts +92 -0
  15. package/src/release-catalog.ts +40 -208
  16. package/src/{release-dispatch.ts → release-context.ts} +0 -11
  17. package/src/release.ts +74 -778
  18. package/src/scaffold.ts +2 -6
  19. package/src/workspace-version.ts +9 -121
  20. package/tasks/bump.ts +17 -34
  21. package/tasks/databricks-cli-assets.ts +129 -0
  22. package/tasks/local-publish.ts +61 -138
  23. package/tasks/publish-npm.ts +71 -30
  24. package/tasks/publish-python.ts +61 -148
  25. package/tasks/publish.ts +32 -182
  26. package/tasks/python-node-bindings-watch.ts +47 -0
  27. package/tasks/python-node-bindings.ts +1017 -0
  28. package/tasks/python-registry.ts +2 -9
  29. package/tasks/python-release.ts +74 -0
  30. package/tasks/release-version.ts +23 -2
  31. package/tasks/release.ts +99 -0
  32. package/tasks/sync.ts +13 -18
  33. package/tasks/version-check.ts +13 -72
  34. package/src/_rust-project.ts +0 -429
  35. package/src/_rust-release-workflow.ts +0 -698
  36. package/src/project-rs.ts +0 -1127
  37. package/src/release-plan.ts +0 -269
  38. package/src/release-please.ts +0 -242
  39. package/src/uniffi.ts +0 -65
  40. package/tasks/publish-uniffi-local.ts +0 -314
  41. package/tasks/release-assets.ts +0 -476
  42. package/tasks/release-bootstrap.ts +0 -59
  43. package/tasks/release-candidate.ts +0 -383
  44. package/tasks/release-manifest.ts +0 -273
  45. package/tasks/release-plan.ts +0 -89
  46. package/tasks/release-please.ts +0 -162
  47. package/tasks/release-pr.ts +0 -563
  48. package/tasks/release-request.ts +0 -166
  49. package/tasks/release-summary-units.ts +0 -75
  50. package/tasks/release-summary.ts +0 -381
  51. package/tasks/rust.ts +0 -178
  52. package/tasks/stamp-python.ts +0 -13
  53. package/tasks/uniffi-python.d.ts +0 -61
  54. package/tasks/uniffi-python.js +0 -191
  55. package/tasks/uniffi-release.mjs +0 -573
  56. package/tasks/uniffi.ts +0 -271
package/src/scaffold.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Runs a projen re-synth, for the `sync` task and its watchers.
3
3
  */
4
4
  import { join } from "node:path";
5
- import * as exec from "@dbx-tools/core/exec";
5
+ import { runTaskCommand } from "./_task-command.ts";
6
6
  import { repoRoot } from "./packages.ts";
7
7
 
8
8
  /**
@@ -34,9 +34,5 @@ export function runSynth(options: { post?: boolean } = {}): void {
34
34
  else env.PROJEN_DISABLE_POST = "true";
35
35
  const projenrc = join(repoRoot, ".projenrc.ts");
36
36
  const args = process.versions.bun ? [projenrc] : ["--import", "tsx", projenrc];
37
- exec.spawnSync(process.execPath, args, {
38
- cwd: repoRoot,
39
- env,
40
- check: true,
41
- });
37
+ runTaskCommand(repoRoot, process.execPath, args, { env });
42
38
  }
@@ -8,22 +8,17 @@
8
8
  * DEFAULT_VERSION} when it is absent on a fresh tree); it never rewrites it, so an
9
9
  * ordinary `bunx projen` cannot move a package version up or down.
10
10
  *
11
- * Only two callers change the number: the pure `bump` task used while preparing
12
- * a reviewed release PR, and the one-time bootstrap of a workspace that has no
13
- * `VERSION` yet. Both resolve the base from remote git tags first ({@link
14
- * resolveRemoteVersion}) so a release cut elsewhere is respected, and fall back
15
- * to the local file (or {@link DEFAULT_VERSION}) when the remote is unreachable
16
- * or has no tags. The remote is consulted only on those paths.
11
+ * Only the pure `bump` task changes the number. It increments the checked-in
12
+ * file while release preparation owns the surrounding Git transaction.
17
13
  */
18
14
  import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
19
15
  import { join } from "node:path";
20
16
  import { parse as parseVersion } from "semver";
21
- import { captureTaskCommand } from "./_task-command.ts";
22
17
 
23
18
  /** Name of the repo-root file holding the workspace version. */
24
19
  export const VERSION_FILE = "VERSION";
25
20
 
26
- /** Version a fresh workspace starts at when no `VERSION` file and no remote tag exist. */
21
+ /** Version a fresh workspace reads before its first explicit VERSION write. */
27
22
  export const DEFAULT_VERSION = "0.0.1";
28
23
 
29
24
  const SEMVER = /^\d+\.\d+\.\d+$/;
@@ -115,127 +110,20 @@ export function syncWorkspaceManifestVersion(manifestPath: string, version: stri
115
110
  return true;
116
111
  }
117
112
 
118
- /** Run git in `cwd`, capturing stdout and swallowing failure (offline, no repo). */
119
- function gitCapture(cwd: string, args: string[]): string {
120
- try {
121
- return captureTaskCommand(cwd, "git", args);
122
- } catch {
123
- return "";
124
- }
125
- }
126
-
127
- /** Highest tag matching `<prefix><semver>` in the local tag list, or `undefined`. */
128
- export function latestTagVersion(cwd: string, prefix: string): Semver | undefined {
129
- const out = gitCapture(cwd, [
130
- "-c",
131
- "versionsort.suffix=-",
132
- "tag",
133
- "--sort=-version:refname",
134
- "--list",
135
- `${prefix}*`,
136
- ]);
137
- for (const tag of out.split("\n")) {
138
- if (!tag.startsWith(prefix)) continue;
139
- const v = parseSemver(tag.slice(prefix.length));
140
- if (v) return v;
141
- }
142
- return undefined;
143
- }
144
-
145
- /** Highest historical `<component>-<prefix><semver>` tag, or `undefined`. */
146
- export function latestComponentTagVersion(cwd: string, prefix: string): Semver | undefined {
147
- const marker = `-${prefix}`;
148
- const out = gitCapture(cwd, [
149
- "-c",
150
- "versionsort.suffix=-",
151
- "tag",
152
- "--sort=-version:refname",
153
- "--list",
154
- `*${marker}*`,
155
- ]);
156
- let best: Semver | undefined;
157
- for (const tag of out.split("\n")) {
158
- const index = tag.lastIndexOf(marker);
159
- const raw = index >= 0 ? tag.slice(index + marker.length) : "";
160
- if (!/^\d+\.\d+\.\d+$/.test(raw)) continue;
161
- const version = parseSemver(raw);
162
- if (version && (!best || compareSemver(version, best) > 0)) best = version;
163
- }
164
- return best;
165
- }
166
-
167
- /**
168
- * Highest published version across repository and historical component tags,
169
- * or `undefined` when the remote is unreachable or no matching tag exists.
170
- * Fetches tags first (best effort) so a release made elsewhere is respected; a
171
- * fetch failure just means the local tag list is used, and callers fall back to
172
- * the `VERSION` file.
173
- */
174
- export function resolveRemoteVersion(
175
- cwd: string,
176
- prefixes: readonly string[],
177
- {
178
- fetch = true,
179
- includeComponentTags = true,
180
- }: { fetch?: boolean; includeComponentTags?: boolean } = {},
181
- ): string | undefined {
182
- if (fetch) gitCapture(cwd, ["fetch", "--tags", "--quiet"]);
183
- let best: Semver | undefined;
184
- for (const prefix of prefixes) {
185
- const versions = [
186
- latestTagVersion(cwd, prefix),
187
- ...(includeComponentTags ? [latestComponentTagVersion(cwd, prefix)] : []),
188
- ];
189
- for (const version of versions) {
190
- if (version && (!best || compareSemver(version, best) > 0)) best = version;
191
- }
192
- }
193
- return best ? best.join(".") : undefined;
194
- }
195
-
196
- /**
197
- * The base version a `bump` increments from: the highest remote tag if any exists
198
- * (a local file that is ahead does NOT win), else the local `VERSION` file, else
199
- * {@link DEFAULT_VERSION}.
200
- */
201
- export function resolveBaseVersion(
202
- root: string,
203
- prefixes: readonly string[],
204
- options: { fetch?: boolean; includeComponentTags?: boolean } = {},
205
- ): { version: string; source: "remote" | "local" } {
206
- const remote = resolveRemoteVersion(root, prefixes, options);
207
- if (remote) return { version: remote, source: "remote" };
208
- return { version: readWorkspaceVersion(root), source: "local" };
113
+ /** The checked-in version a release increment starts from. */
114
+ export function resolveBaseVersion(root: string): string {
115
+ return readWorkspaceVersion(root);
209
116
  }
210
117
 
211
118
  /** Resolve the next release version without mutating the workspace. */
212
119
  export function resolveNextVersion(
213
120
  root: string,
214
- prefixes: readonly string[],
215
121
  level: VersionLevel,
216
- options: { fetch?: boolean; includeComponentTags?: boolean } = {},
217
- ): { base: string; version: string; source: "remote" | "local" } {
218
- const base = resolveBaseVersion(root, prefixes, options);
219
- const parsed = parseSemver(base.version) ?? [0, 0, 1];
122
+ ): { base: string; version: string } {
123
+ const base = resolveBaseVersion(root);
124
+ const parsed = parseSemver(base) ?? [0, 0, 1];
220
125
  return {
221
126
  base: parsed.join("."),
222
127
  version: incrementSemver(parsed, level).join("."),
223
- source: base.source,
224
128
  };
225
129
  }
226
-
227
- /**
228
- * Create the `VERSION` file when it does not yet exist, seeding it from the remote
229
- * tags (or {@link DEFAULT_VERSION} when the remote is unreachable / has no tag). An
230
- * existing file is left untouched - only `bump` moves an established version, so
231
- * bootstrap never upgrades or downgrades one. Returns the current version.
232
- */
233
- export function ensureWorkspaceVersion(
234
- root: string,
235
- { prefixes = ["v"], fetch = true }: { prefixes?: readonly string[]; fetch?: boolean } = {},
236
- ): string {
237
- if (!existsSync(versionPath(root))) {
238
- writeWorkspaceVersion(root, resolveRemoteVersion(root, prefixes, { fetch }) ?? DEFAULT_VERSION);
239
- }
240
- return readWorkspaceVersion(root);
241
- }
package/tasks/bump.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * generated version surface. This task has no git, registry, or publication
5
5
  * side effects; release preparation owns those operations.
6
6
  */
7
- import * as exec from "@dbx-tools/core/exec";
8
7
  import * as projectUtils from "@dbx-tools/core/project-utils";
9
8
  import { log } from "@dbx-tools/shared-core";
10
9
  import { Command } from "commander";
@@ -17,6 +16,7 @@ import {
17
16
  type ReleaseOs,
18
17
  type VersionLevel,
19
18
  } from "../src/_release-platform.ts";
19
+ import { runTaskCommand } from "../src/_task-command.ts";
20
20
  import { resolveNextVersion, writeWorkspaceVersion } from "../src/workspace-version.ts";
21
21
 
22
22
  const logger = log.logger("projen:bump");
@@ -25,43 +25,26 @@ const program = new Command();
25
25
  program
26
26
  .description("Increment VERSION and synchronize generated workspace versions")
27
27
  .addOption(releaseLevelOption())
28
- .option("--prefix <prefix>", "git tag prefix used to resolve the published base", "v")
29
28
  .addOption(releaseOperatingSystemOption())
30
29
  .addOption(releaseArchitectureOption())
31
30
  .option("--no-synth", "write VERSION without synchronizing generated files")
32
- .action(
33
- (opts: {
34
- level: VersionLevel;
35
- prefix: string;
36
- os: ReleaseOs[];
37
- arch: ReleaseArch[];
38
- synth: boolean;
39
- }) => {
40
- const root = projectUtils.root() ?? process.cwd();
41
- const next = resolveNextVersion(root, [opts.prefix], opts.level);
42
- const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
31
+ .action((opts: { level: VersionLevel; os: ReleaseOs[]; arch: ReleaseArch[]; synth: boolean }) => {
32
+ const root = projectUtils.root() ?? process.cwd();
33
+ const next = resolveNextVersion(root, opts.level);
34
+ const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
43
35
 
44
- logger.info(
45
- `bump ${next.base} -> ${next.version} (${opts.level})` +
46
- `${next.source === "remote" ? "" : " [no remote tag; used local VERSION]"}`,
47
- );
48
- writeWorkspaceVersion(root, next.version);
36
+ logger.info(`bump ${next.base} -> ${next.version} (${opts.level})`);
37
+ writeWorkspaceVersion(root, next.version);
49
38
 
50
- if (opts.synth) {
51
- exec.spawnSync(process.execPath, [".projenrc.ts"], {
52
- cwd: root,
53
- stdout: "inherit",
54
- stderr: "inherit",
55
- stdin: "ignore",
56
- check: true,
57
- env: {
58
- ...process.env,
59
- ...(releasePlatforms ? { DBX_TOOLS_RELEASE_PLATFORMS: releasePlatforms } : {}),
60
- },
61
- });
62
- }
63
- logger.success(`workspace version synchronized at ${next.version}`);
64
- },
65
- );
39
+ if (opts.synth) {
40
+ runTaskCommand(root, process.execPath, [".projenrc.ts"], {
41
+ env: {
42
+ ...process.env,
43
+ ...(releasePlatforms ? { DBX_TOOLS_RELEASE_PLATFORMS: releasePlatforms } : {}),
44
+ },
45
+ });
46
+ }
47
+ logger.success(`workspace version synchronized at ${next.version}`);
48
+ });
66
49
 
67
50
  await program.parseAsync();
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env -S bun
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+
6
+ import * as projectUtils from "@dbx-tools/core/project-utils";
7
+
8
+ import { makeReadonly, makeWritable } from "../src/generated.ts";
9
+
10
+ const MINIMUM_VERSION = "0.296.0";
11
+ const REPOSITORY = "databricks/cli";
12
+ const PLATFORM_ASSETS = {
13
+ "darwin-arm64": "darwin_arm64",
14
+ "darwin-x64": "darwin_amd64",
15
+ "linux-arm64": "linux_arm64",
16
+ "linux-x64": "linux_amd64",
17
+ "win32-arm64": "windows_arm64",
18
+ "win32-x64": "windows_amd64",
19
+ } as const;
20
+
21
+ interface GitHubAsset {
22
+ readonly browser_download_url: string;
23
+ readonly digest?: string;
24
+ readonly name: string;
25
+ }
26
+
27
+ interface GitHubRelease {
28
+ readonly assets: GitHubAsset[];
29
+ readonly published_at: string;
30
+ readonly tag_name: string;
31
+ }
32
+
33
+ interface CliAsset {
34
+ readonly sha256: string;
35
+ readonly url: string;
36
+ }
37
+
38
+ interface CliAssetManifest {
39
+ readonly schemaVersion: 1;
40
+ readonly dbxToolsVersion: string;
41
+ readonly minimumVersion: string;
42
+ readonly version: string;
43
+ readonly publishedAt: string;
44
+ readonly assets: Record<keyof typeof PLATFORM_ASSETS, CliAsset>;
45
+ }
46
+
47
+ function validManifest(value: unknown, dbxToolsVersion: string): value is CliAssetManifest {
48
+ if (!value || typeof value !== "object") return false;
49
+ const manifest = value as Partial<CliAssetManifest>;
50
+ return (
51
+ manifest.schemaVersion === 1 &&
52
+ manifest.dbxToolsVersion === dbxToolsVersion &&
53
+ manifest.minimumVersion === MINIMUM_VERSION &&
54
+ typeof manifest.version === "string" &&
55
+ typeof manifest.publishedAt === "string" &&
56
+ Boolean(manifest.assets) &&
57
+ Object.keys(PLATFORM_ASSETS).every((key) => {
58
+ const asset = manifest.assets?.[key as keyof typeof PLATFORM_ASSETS];
59
+ return (
60
+ typeof asset?.url === "string" &&
61
+ typeof asset.sha256 === "string" &&
62
+ /^[0-9a-f]{64}$/.test(asset.sha256)
63
+ );
64
+ })
65
+ );
66
+ }
67
+
68
+ async function latestManifest(dbxToolsVersion: string): Promise<CliAssetManifest> {
69
+ const response = await fetch(`https://api.github.com/repos/${REPOSITORY}/releases/latest`, {
70
+ headers: {
71
+ accept: "application/vnd.github+json",
72
+ "user-agent": `dbx-tools/${dbxToolsVersion}`,
73
+ },
74
+ });
75
+ if (!response.ok) {
76
+ throw new Error(`Could not resolve the latest Databricks CLI release (${response.status})`);
77
+ }
78
+ const release = (await response.json()) as GitHubRelease;
79
+ const version = release.tag_name.replace(/^v/, "");
80
+ const assets = Object.fromEntries(
81
+ Object.entries(PLATFORM_ASSETS).map(([platform, suffix]) => {
82
+ const name = `databricks_cli_${version}_${suffix}.tar.gz`;
83
+ const asset = release.assets.find((candidate) => candidate.name === name);
84
+ const sha256 = asset?.digest?.match(/^sha256:([0-9a-f]{64})$/i)?.[1]?.toLowerCase();
85
+ if (!asset || !sha256)
86
+ throw new Error(`Release ${release.tag_name} has no digest for ${name}`);
87
+ return [platform, { url: asset.browser_download_url, sha256 }];
88
+ }),
89
+ ) as CliAssetManifest["assets"];
90
+ return {
91
+ schemaVersion: 1,
92
+ dbxToolsVersion,
93
+ minimumVersion: MINIMUM_VERSION,
94
+ version,
95
+ publishedAt: release.published_at,
96
+ assets,
97
+ };
98
+ }
99
+
100
+ const { values } = parseArgs({
101
+ options: {
102
+ check: { type: "boolean", default: false },
103
+ root: { type: "string" },
104
+ },
105
+ strict: true,
106
+ });
107
+ const root = resolve(values.root ?? projectUtils.root() ?? process.cwd());
108
+ const output = join(root, "packages/js/node/auth/src/generated/databricks-cli-assets.json");
109
+ const dbxToolsVersion = readFileSync(join(root, "VERSION"), "utf8").trim();
110
+ let current: unknown;
111
+ if (existsSync(output)) {
112
+ try {
113
+ current = JSON.parse(readFileSync(output, "utf8"));
114
+ } catch {
115
+ current = undefined;
116
+ }
117
+ }
118
+ if (validManifest(current, dbxToolsVersion)) {
119
+ console.log(`verified ${output.slice(root.length + 1)}`);
120
+ } else if (values.check) {
121
+ throw new Error("Databricks CLI asset manifest is missing or stale");
122
+ } else {
123
+ const manifest = await latestManifest(dbxToolsVersion);
124
+ mkdirSync(dirname(output), { recursive: true });
125
+ makeWritable(output);
126
+ writeFileSync(output, `${JSON.stringify(manifest, undefined, 2)}\n`);
127
+ makeReadonly(output);
128
+ console.log(`generated ${output.slice(root.length + 1)}`);
129
+ }
@@ -1,29 +1,24 @@
1
- /** Local registry preflight shared by release preparation. */
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
1
+ /** Build and publish the current workspace to configured loopback registries. */
2
+ import { resolve } from "node:path";
5
3
  import { fileURLToPath } from "node:url";
6
- import * as exec from "@dbx-tools/core/exec";
7
4
  import * as projectUtils from "@dbx-tools/core/project-utils";
8
5
  import { log, net } from "@dbx-tools/shared-core";
6
+ import { Command } from "commander";
9
7
  import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
10
- import { readDbxToolsConfig } from "../src/packages.ts";
8
+ import { runTaskCommandAsync } from "../src/_task-command.ts";
11
9
 
12
10
  const logger = log.logger("projen:local-publish");
13
11
 
14
- /** Options for publishing one release candidate to configured loopback registries. */
12
+ /** Options for publishing the current workspace to local registries. */
15
13
  export interface LocalPublishOptions {
14
+ readonly localPypi: string;
15
+ readonly localRegistry: string;
16
+ readonly pythonRoot: string;
16
17
  readonly root: string;
17
18
  readonly version: string;
18
- readonly localRegistry: string;
19
- readonly localPypi: string;
20
- readonly pythonRoot?: string;
21
- readonly localCargo: boolean;
22
- /** Reuse the root TypeScript compile that immediately preceded local publication. */
23
- readonly reuseValidatedNodeCompile?: boolean;
24
19
  }
25
20
 
26
- function resolveLocalRegistry(value: string): string | undefined {
21
+ export function resolveLocalRegistry(value: string): string | undefined {
27
22
  const trimmed = value.trim();
28
23
  if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
29
24
  if (trimmed.toLowerCase() === "auto") {
@@ -33,43 +28,13 @@ function resolveLocalRegistry(value: string): string | undefined {
33
28
  return trimmed;
34
29
  }
35
30
 
36
- function localCargoRegistry(): string | undefined {
37
- if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
38
- const config = join(homedir(), ".cargo", "config.toml");
39
- if (!existsSync(config)) return undefined;
40
- const source = readFileSync(config, "utf8");
41
- const sections = [
42
- ...source.matchAll(/^\[registries\.([^\]]+)\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/gm),
43
- ];
44
- for (const section of sections) {
45
- const index = section[2]?.match(/^\s*index\s*=\s*["']([^"']+)["']/m)?.[1];
46
- if (index && net.isLoopbackHost(index.replace(/^sparse\+/, ""))) return section[1];
47
- }
48
- return undefined;
49
- }
50
-
51
- /** Arguments for the workspace npm publisher used by local release preflight. */
52
- export function localNodePublishArguments(
53
- publishScript: string,
54
- options: Pick<LocalPublishOptions, "version" | "reuseValidatedNodeCompile">,
55
- registry: string,
56
- ): string[] {
57
- return [
58
- publishScript,
59
- options.version,
60
- "--registry",
61
- registry,
62
- ...(options.reuseValidatedNodeCompile ? ["--skip-compile"] : []),
63
- ];
64
- }
65
-
66
- /** Publish the exact release candidate to local npm, PyPI, and Cargo mirrors. */
31
+ /** Build and publish the current workspace directly to local registries. */
67
32
  export async function publishLocalRelease(options: LocalPublishOptions): Promise<void> {
33
+ const root = resolve(options.root);
68
34
  const localRegistry = resolveLocalRegistry(options.localRegistry);
69
35
  const activeIndexes = activePythonIndexes();
70
36
  const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
71
- const pythonRoot = options.pythonRoot ? resolve(options.root, options.pythonRoot) : undefined;
72
- const localPublishes: Promise<unknown>[] = [];
37
+ const publishes: Promise<unknown>[] = [];
73
38
 
74
39
  if (
75
40
  options.localPypi.toLowerCase() === "auto" &&
@@ -82,103 +47,61 @@ export async function publishLocalRelease(options: LocalPublishOptions): Promise
82
47
  }
83
48
 
84
49
  if (localRegistry) {
85
- const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
86
- logger.info(`publishing ${options.version} to local registry ${localRegistry}`);
87
- localPublishes.push(
88
- exec
89
- .spawn(process.execPath, localNodePublishArguments(publishScript, options, localRegistry), {
90
- cwd: options.root,
91
- stdout: "inherit",
92
- stderr: "inherit",
93
- stdin: "ignore",
94
- check: true,
95
- })
96
- .then(() => logger.success(`published ${options.version} to ${localRegistry}`)),
50
+ const publishNpmScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
51
+ logger.info(`building and publishing npm workspace to ${localRegistry}`);
52
+ publishes.push(
53
+ runTaskCommandAsync(root, process.execPath, [
54
+ publishNpmScript,
55
+ options.version,
56
+ "--registry",
57
+ localRegistry,
58
+ ]),
97
59
  );
98
60
  }
99
61
 
100
- if (localPypi && pythonRoot && existsSync(pythonRoot)) {
62
+ if (localPypi) {
101
63
  const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
102
- logger.info(`publishing Python ${options.version} to local devpi ${localPypi.publishUrl}`);
103
- localPublishes.push(
104
- exec
105
- .spawn(
106
- process.execPath,
107
- [
108
- publishPythonScript,
109
- options.version,
110
- "--root",
111
- pythonRoot,
112
- "--index-url",
113
- localPypi.indexUrl,
114
- "--publish-url",
115
- localPypi.publishUrl,
116
- ],
117
- {
118
- cwd: options.root,
119
- stdout: "inherit",
120
- stderr: "inherit",
121
- stdin: "ignore",
122
- check: true,
123
- },
124
- )
125
- .then(() =>
126
- logger.success(`published Python ${options.version} to ${localPypi.publishUrl}`),
127
- ),
128
- );
129
- }
130
-
131
- await Promise.all(localPublishes);
132
- if (localRegistry || localPypi) {
133
- const publishUniFFIScript = fileURLToPath(
134
- new URL("./publish-uniffi-local.ts", import.meta.url),
135
- );
136
- await exec.spawn(
137
- process.execPath,
138
- [
139
- publishUniFFIScript,
140
- "--version",
64
+ logger.info(`building and publishing Python workspace to ${localPypi.publishUrl}`);
65
+ publishes.push(
66
+ runTaskCommandAsync(root, process.execPath, [
67
+ publishPythonScript,
141
68
  options.version,
142
- ...(localRegistry ? ["--registry", localRegistry] : []),
143
- ...(localPypi ? ["--pypi-publish-url", localPypi.publishUrl] : []),
144
- ],
145
- {
146
- cwd: options.root,
147
- stdout: "inherit",
148
- stderr: "inherit",
149
- stdin: "ignore",
150
- check: true,
151
- },
69
+ "--root",
70
+ options.pythonRoot,
71
+ "--index-url",
72
+ localPypi.indexUrl,
73
+ "--publish-url",
74
+ localPypi.publishUrl,
75
+ ]),
152
76
  );
153
- logger.success(`published host-native bindings for ${options.version}`);
154
77
  }
155
78
 
156
- const rust = readDbxToolsConfig(options.root)?.rust;
157
- const hasRustCrates = Boolean(
158
- rust &&
159
- typeof rust === "object" &&
160
- !Array.isArray(rust) &&
161
- Array.isArray((rust as { crates?: unknown }).crates) &&
162
- (rust as { crates: unknown[] }).crates.length > 0,
163
- );
164
- if (options.localCargo && hasRustCrates) {
165
- const cargoRegistry = localCargoRegistry();
166
- if (cargoRegistry) {
167
- const publishUniFFIScript = fileURLToPath(
168
- new URL("./publish-uniffi-local.ts", import.meta.url),
169
- );
170
- await exec.spawn(
171
- process.execPath,
172
- [publishUniFFIScript, "--version", options.version, "--cargo-registry", cargoRegistry],
173
- {
174
- cwd: options.root,
175
- stdout: "inherit",
176
- stderr: "inherit",
177
- stdin: "ignore",
178
- check: true,
179
- },
180
- );
181
- logger.success(`published Rust ${options.version} to ${cargoRegistry}`);
182
- }
183
- }
79
+ await Promise.all(publishes);
80
+ logger.success(`published local workspace ${options.version}`);
81
+ }
82
+
83
+ if (import.meta.main) {
84
+ await new Command()
85
+ .requiredOption("--version <version>", "release version")
86
+ .option("--root <path>", "repository root")
87
+ .option("--python-root <path>", "Python package root", "packages/py")
88
+ .option("--local-registry <url>", "local npm registry", "auto")
89
+ .option("--local-pypi <url>", "local devpi index", "auto")
90
+ .action(
91
+ (options: {
92
+ version: string;
93
+ root?: string;
94
+ pythonRoot: string;
95
+ localRegistry: string;
96
+ localPypi: string;
97
+ }) =>
98
+ publishLocalRelease({
99
+ localPypi: options.localPypi,
100
+ localRegistry: options.localRegistry,
101
+ pythonRoot: options.pythonRoot,
102
+ root: options.root ?? projectUtils.root() ?? process.cwd(),
103
+ version: options.version,
104
+ }),
105
+ )
106
+ .parseAsync();
184
107
  }