@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
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
- }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
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();
@@ -1,29 +1,31 @@
1
- /** Local registry preflight shared by release preparation. */
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
1
+ /** Publish one verified release candidate to configured loopback registries. */
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import * as exec from "@dbx-tools/core/exec";
7
6
  import * as projectUtils from "@dbx-tools/core/project-utils";
8
7
  import { log, net } from "@dbx-tools/shared-core";
8
+ import { Command } from "commander";
9
9
  import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
10
+ import { verifyReleaseManifest } from "./release-manifest.ts";
11
+ import { runTaskCommandAsync } from "../src/_task-command.ts";
10
12
  import { readDbxToolsConfig } from "../src/packages.ts";
11
13
 
12
14
  const logger = log.logger("projen:local-publish");
13
15
 
14
- /** Options for publishing one release candidate to configured loopback registries. */
16
+ /** Options for publishing an immutable release candidate to local registries. */
15
17
  export interface LocalPublishOptions {
18
+ readonly candidateDirectory: string;
19
+ readonly gitSha: string;
20
+ readonly localCargo: boolean;
21
+ readonly localPypi: string;
22
+ readonly localRegistry: string;
16
23
  readonly root: string;
24
+ readonly tag: string;
17
25
  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
26
  }
25
27
 
26
- function resolveLocalRegistry(value: string): string | undefined {
28
+ export function resolveLocalRegistry(value: string): string | undefined {
27
29
  const trimmed = value.trim();
28
30
  if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
29
31
  if (trimmed.toLowerCase() === "auto") {
@@ -33,7 +35,7 @@ function resolveLocalRegistry(value: string): string | undefined {
33
35
  return trimmed;
34
36
  }
35
37
 
36
- function localCargoRegistry(): string | undefined {
38
+ export function localCargoRegistry(): string | undefined {
37
39
  if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
38
40
  const config = join(homedir(), ".cargo", "config.toml");
39
41
  if (!existsSync(config)) return undefined;
@@ -48,28 +50,32 @@ function localCargoRegistry(): string | undefined {
48
50
  return undefined;
49
51
  }
50
52
 
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
- ];
53
+ function hasRustCrates(root: string): boolean {
54
+ const rust = readDbxToolsConfig(root)?.rust;
55
+ return Boolean(
56
+ rust &&
57
+ typeof rust === "object" &&
58
+ !Array.isArray(rust) &&
59
+ Array.isArray((rust as { crates?: unknown }).crates) &&
60
+ (rust as { crates: unknown[] }).crates.length > 0,
61
+ );
64
62
  }
65
63
 
66
- /** Publish the exact release candidate to local npm, PyPI, and Cargo mirrors. */
64
+ /** Publish the exact candidate archives before they are attached to the draft release. */
67
65
  export async function publishLocalRelease(options: LocalPublishOptions): Promise<void> {
66
+ const root = resolve(options.root);
67
+ const candidateDirectory = resolve(options.candidateDirectory);
68
+ verifyReleaseManifest({
69
+ directory: candidateDirectory,
70
+ gitSha: options.gitSha,
71
+ tag: options.tag,
72
+ version: options.version,
73
+ });
74
+
68
75
  const localRegistry = resolveLocalRegistry(options.localRegistry);
69
76
  const activeIndexes = activePythonIndexes();
70
77
  const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
71
- const pythonRoot = options.pythonRoot ? resolve(options.root, options.pythonRoot) : undefined;
72
- const localPublishes: Promise<unknown>[] = [];
78
+ const publishes: Promise<unknown>[] = [];
73
79
 
74
80
  if (
75
81
  options.localPypi.toLowerCase() === "auto" &&
@@ -82,103 +88,104 @@ export async function publishLocalRelease(options: LocalPublishOptions): Promise
82
88
  }
83
89
 
84
90
  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}`)),
91
+ const publishNpmScript = fileURLToPath(new URL("./publish-npm.ts", import.meta.url));
92
+ logger.info(`publishing approved npm archives to ${localRegistry}`);
93
+ publishes.push(
94
+ runTaskCommandAsync(root, process.execPath, [
95
+ publishNpmScript,
96
+ "--directory",
97
+ candidateDirectory,
98
+ "--version",
99
+ options.version,
100
+ "--registry",
101
+ localRegistry,
102
+ ]),
97
103
  );
98
104
  }
99
105
 
100
- if (localPypi && pythonRoot && existsSync(pythonRoot)) {
101
- 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
- ),
106
+ if (localPypi) {
107
+ const pythonDirectory = mkdtempSync(join(tmpdir(), "dbx-tools-local-pypi-"));
108
+ verifyReleaseManifest({
109
+ directory: candidateDirectory,
110
+ gitSha: options.gitSha,
111
+ kind: "pypi",
112
+ output: pythonDirectory,
113
+ tag: options.tag,
114
+ version: options.version,
115
+ });
116
+ logger.info(`publishing approved Python distributions to ${localPypi.publishUrl}`);
117
+ publishes.push(
118
+ runTaskCommandAsync(
119
+ root,
120
+ "uvx",
121
+ [
122
+ "--from",
123
+ "devpi-client",
124
+ "devpi",
125
+ "upload",
126
+ "--index",
127
+ localPypi.publishUrl,
128
+ "--from-dir",
129
+ pythonDirectory,
130
+ ],
131
+ { env: { ...process.env, UV_DEFAULT_INDEX: localPypi.indexUrl } },
132
+ ).finally(() => rmSync(pythonDirectory, { recursive: true, force: true })),
128
133
  );
129
134
  }
130
135
 
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",
141
- 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
- },
152
- );
153
- logger.success(`published host-native bindings for ${options.version}`);
154
- }
136
+ await Promise.all(publishes);
155
137
 
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(
138
+ if (options.localCargo && hasRustCrates(root)) {
139
+ const registry = localCargoRegistry();
140
+ if (registry) {
141
+ const publishCargoScript = fileURLToPath(
168
142
  new URL("./publish-uniffi-local.ts", import.meta.url),
169
143
  );
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}`);
144
+ await runTaskCommandAsync(root, process.execPath, [
145
+ publishCargoScript,
146
+ "--version",
147
+ options.version,
148
+ "--cargo-registry",
149
+ registry,
150
+ ]);
151
+ logger.success(`published Rust ${options.version} to ${registry}`);
182
152
  }
183
153
  }
154
+
155
+ logger.success(`published approved local candidate ${options.tag}`);
156
+ }
157
+
158
+ if (import.meta.main) {
159
+ await new Command()
160
+ .requiredOption("--candidate-directory <path>", "verified release candidate directory")
161
+ .requiredOption("--sha <sha>", "exact release commit")
162
+ .requiredOption("--tag <tag>", "annotated release tag")
163
+ .requiredOption("--version <version>", "release version")
164
+ .option("--root <path>", "repository root")
165
+ .option("--local-registry <url>", "local npm registry", "auto")
166
+ .option("--local-pypi <url>", "local devpi index", "auto")
167
+ .option("--local-cargo", "publish Cargo crates to a configured local registry")
168
+ .action(
169
+ (options: {
170
+ candidateDirectory: string;
171
+ sha: string;
172
+ tag: string;
173
+ version: string;
174
+ root?: string;
175
+ localRegistry: string;
176
+ localPypi: string;
177
+ localCargo?: boolean;
178
+ }) =>
179
+ publishLocalRelease({
180
+ candidateDirectory: options.candidateDirectory,
181
+ gitSha: options.sha,
182
+ localCargo: Boolean(options.localCargo),
183
+ localPypi: options.localPypi,
184
+ localRegistry: options.localRegistry,
185
+ root: options.root ?? projectUtils.root() ?? process.cwd(),
186
+ tag: options.tag,
187
+ version: options.version,
188
+ }),
189
+ )
190
+ .parseAsync();
184
191
  }