@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
@@ -2,6 +2,7 @@
2
2
  /** Idempotent npm archive publication for release recovery. */
3
3
  import { createHash } from "node:crypto";
4
4
  import {
5
+ chmodSync,
5
6
  lstatSync,
6
7
  mkdtempSync,
7
8
  readFileSync,
@@ -15,6 +16,7 @@ import { join, relative, resolve } from "node:path";
15
16
  import * as exec from "@dbx-tools/core/exec";
16
17
  import { log } from "@dbx-tools/shared-core";
17
18
  import { Command } from "commander";
19
+ import { runTaskCommand } from "../src/_task-command.ts";
18
20
 
19
21
  const DEFAULT_REGISTRY = "https://registry.npmjs.org";
20
22
  const logger = log.logger("projen:publish-npm");
@@ -28,6 +30,20 @@ export interface NpmReleaseIdentity {
28
30
  readonly version: string;
29
31
  }
30
32
 
33
+ interface NpmArchiveManifest {
34
+ readonly [key: string]: unknown;
35
+ readonly dependencies?: Readonly<Record<string, string>>;
36
+ readonly optionalDependencies?: Readonly<Record<string, string>>;
37
+ readonly name?: string;
38
+ readonly publishConfig?: { readonly access?: unknown };
39
+ readonly repository?: unknown;
40
+ readonly version?: string;
41
+ }
42
+
43
+ export type NpmArchiveManifestTransform = (
44
+ manifest: NpmArchiveManifest,
45
+ ) => Readonly<Record<string, unknown>>;
46
+
31
47
  function normalizedRepository(value: unknown): string | undefined {
32
48
  const url =
33
49
  typeof value === "string"
@@ -120,7 +136,7 @@ export async function publishedNpmRelease(
120
136
  };
121
137
  }
122
138
 
123
- export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
139
+ function readNpmArchiveManifest(path: string): NpmArchiveManifest {
124
140
  const result = exec.spawnSync("tar", ["-xOf", path, "package/package.json"], {
125
141
  cwd: process.cwd(),
126
142
  stdout: "capture",
@@ -131,12 +147,11 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
131
147
  if (result.exitCode !== 0 || !result.stdout) {
132
148
  throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
133
149
  }
134
- const manifest = JSON.parse(result.stdout) as {
135
- name?: string;
136
- publishConfig?: { access?: unknown };
137
- repository?: unknown;
138
- version?: string;
139
- };
150
+ return JSON.parse(result.stdout) as NpmArchiveManifest;
151
+ }
152
+
153
+ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
154
+ const manifest = readNpmArchiveManifest(path);
140
155
  if (!manifest.name || !manifest.version) {
141
156
  throw new Error(`npm archive has no package name or version: ${path}`);
142
157
  }
@@ -154,6 +169,48 @@ export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
154
169
  };
155
170
  }
156
171
 
172
+ function orderNpmArchives(archives: readonly string[]): string[] {
173
+ const byName = new Map(
174
+ archives.map((archive) => {
175
+ const manifest = readNpmArchiveManifest(archive);
176
+ if (!manifest.name) throw new Error(`npm archive has no package name: ${archive}`);
177
+ return [manifest.name, { archive, manifest }] as const;
178
+ }),
179
+ );
180
+ if (byName.size !== archives.length) throw new Error("Release contains duplicate npm packages");
181
+ const remaining = new Map(
182
+ [...byName].map(([name, value]) => [
183
+ name,
184
+ new Set(
185
+ [
186
+ ...Object.keys(value.manifest.dependencies ?? {}),
187
+ ...Object.keys(value.manifest.optionalDependencies ?? {}),
188
+ ].filter((dependency) => byName.has(dependency)),
189
+ ),
190
+ ]),
191
+ );
192
+ const ordered: string[] = [];
193
+ while (remaining.size > 0) {
194
+ const ready = [...remaining]
195
+ .filter(([, dependencies]) => dependencies.size === 0)
196
+ .map(([name]) => name)
197
+ .sort();
198
+ if (ready.length === 0) {
199
+ throw new Error(
200
+ `Cyclic npm release dependencies: ${[...remaining.keys()].sort().join(", ")}`,
201
+ );
202
+ }
203
+ for (const name of ready) {
204
+ ordered.push(byName.get(name)!.archive);
205
+ remaining.delete(name);
206
+ }
207
+ for (const dependencies of remaining.values()) {
208
+ for (const name of ready) dependencies.delete(name);
209
+ }
210
+ }
211
+ return ordered;
212
+ }
213
+
157
214
  /** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
158
215
  export function npmArchiveContentDigest(path: string): string {
159
216
  const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
@@ -191,25 +248,60 @@ export function packNpmPackage(
191
248
  directory: string,
192
249
  destination: string,
193
250
  path = process.env.PATH ?? "",
251
+ transformManifest?: NpmArchiveManifestTransform,
194
252
  ): string {
195
253
  const executable = process.versions.bun ? process.execPath : "bun";
196
- exec.spawnSync(
197
- executable,
198
- ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
199
- {
200
- cwd: directory,
201
- env: { ...process.env, PATH: path },
202
- stdout: "inherit",
203
- stderr: "inherit",
204
- stdin: "ignore",
205
- check: true,
206
- },
207
- );
254
+ const manifest = join(directory, "package.json");
255
+ const manifestMode = lstatSync(manifest).mode & 0o777;
256
+ const restoreManifestMode = (manifestMode & 0o200) === 0;
257
+ if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
258
+ try {
259
+ exec.spawnSync(
260
+ executable,
261
+ ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
262
+ {
263
+ cwd: directory,
264
+ env: { ...process.env, PATH: path },
265
+ stdout: "inherit",
266
+ stderr: "inherit",
267
+ stdin: "ignore",
268
+ check: true,
269
+ },
270
+ );
271
+ } finally {
272
+ if (restoreManifestMode) chmodSync(manifest, manifestMode);
273
+ }
208
274
  const archives = readdirSync(destination).filter((file) => file.endsWith(".tgz"));
209
275
  if (archives.length !== 1) {
210
276
  throw new Error(`Expected one packed npm archive, found ${archives.length}`);
211
277
  }
212
- return join(destination, archives[0]);
278
+ const archive = join(destination, archives[0]);
279
+ if (!transformManifest) return archive;
280
+
281
+ const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
282
+ try {
283
+ exec.spawnSync("tar", ["-xzf", archive, "-C", temp], {
284
+ cwd: process.cwd(),
285
+ stdout: "ignore",
286
+ stderr: "inherit",
287
+ stdin: "ignore",
288
+ check: true,
289
+ });
290
+ const manifestPath = join(temp, "package", "package.json");
291
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
292
+ writeFileSync(manifestPath, `${JSON.stringify(transformManifest(manifest), null, 2)}\n`);
293
+ rmSync(archive, { force: true });
294
+ exec.spawnSync("tar", ["-czf", archive, "-C", temp, "package"], {
295
+ cwd: process.cwd(),
296
+ stdout: "ignore",
297
+ stderr: "inherit",
298
+ stdin: "ignore",
299
+ check: true,
300
+ });
301
+ return archive;
302
+ } finally {
303
+ rmSync(temp, { recursive: true, force: true });
304
+ }
213
305
  }
214
306
 
215
307
  export async function publishNpmArchives(options: {
@@ -219,10 +311,12 @@ export async function publishNpmArchives(options: {
219
311
  readonly version?: string;
220
312
  }): Promise<void> {
221
313
  const directory = resolve(options.directory);
222
- const archives = readdirSync(directory)
223
- .filter((file) => file.endsWith(".tgz"))
224
- .sort()
225
- .map((file) => join(directory, file));
314
+ const archives = orderNpmArchives(
315
+ readdirSync(directory)
316
+ .filter((file) => file.endsWith(".tgz"))
317
+ .sort()
318
+ .map((file) => join(directory, file)),
319
+ );
226
320
  if (archives.length === 0) throw new Error(`No npm archives found in ${directory}`);
227
321
 
228
322
  for (const archive of archives) {
@@ -239,23 +333,13 @@ export async function publishNpmArchives(options: {
239
333
  continue;
240
334
  }
241
335
  }
242
- exec.spawnSync(
243
- "npm",
244
- [
245
- "publish",
246
- archive,
247
- ...(local.access ? ["--access", local.access] : []),
248
- ...(options.registry ? ["--registry", options.registry] : []),
249
- ...(options.dryRun ? ["--dry-run"] : []),
250
- ],
251
- {
252
- cwd: process.cwd(),
253
- stdout: "inherit",
254
- stderr: "inherit",
255
- stdin: "ignore",
256
- check: true,
257
- },
258
- );
336
+ runTaskCommand(process.cwd(), "npm", [
337
+ "publish",
338
+ archive,
339
+ ...(local.access ? ["--access", local.access] : []),
340
+ ...(options.registry ? ["--registry", options.registry] : []),
341
+ ...(options.dryRun ? ["--dry-run"] : []),
342
+ ]);
259
343
  }
260
344
  }
261
345
 
@@ -1,25 +1,24 @@
1
1
  #!/usr/bin/env -S bun
2
2
  import {
3
3
  chmodSync,
4
+ cpSync,
4
5
  existsSync,
6
+ mkdirSync,
5
7
  mkdtempSync,
6
8
  readFileSync,
7
9
  readdirSync,
8
10
  rmSync,
9
- statSync,
10
11
  writeFileSync,
11
12
  } from "node:fs";
12
13
  import { tmpdir } from "node:os";
13
- import { basename, join, resolve } from "node:path";
14
- import * as exec from "@dbx-tools/core/exec";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
15
  import { Command } from "commander";
16
16
  import { parse, stringify } from "smol-toml";
17
- import { pythonProjectInfo, stampPythonProject } from "./uniffi-python.js";
18
- import type { ReleasePlan } from "../src/release-plan.ts";
17
+ import { preparePythonProjectForPublication, pythonProjectInfo } from "./uniffi-python.js";
18
+ import { runTaskCommand } from "../src/_task-command.ts";
19
19
 
20
20
  interface PythonProjectFile {
21
21
  readonly directory: string;
22
- readonly mode: number;
23
22
  readonly name: string;
24
23
  readonly version: string;
25
24
  readonly path: string;
@@ -28,21 +27,15 @@ interface PythonProjectFile {
28
27
  readonly uniffi: boolean;
29
28
  }
30
29
 
31
- export interface StampPythonProjectsOptions {
32
- readonly rewriteDependencies?: boolean;
33
- readonly versions?: ReadonlyMap<string, string>;
30
+ /** Python wheel and source archives accepted by Twine and package indexes. */
31
+ export function pythonDistributionPaths(directory: string): string[] {
32
+ return readdirSync(directory)
33
+ .filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
34
+ .sort()
35
+ .map((file) => join(directory, file));
34
36
  }
35
37
 
36
- export interface RestorePythonProjects {
37
- (): void;
38
- readonly paths: readonly string[];
39
- }
40
-
41
- export function stampPythonProjects(
42
- root: string,
43
- version: string,
44
- options: StampPythonProjectsOptions = {},
45
- ): RestorePythonProjects {
38
+ function pythonProjects(root: string): PythonProjectFile[] {
46
39
  const packageFiles = readdirSync(root, { withFileTypes: true })
47
40
  .filter((entry) => entry.isDirectory())
48
41
  .map((entry) => resolve(root, entry.name, "pyproject.toml"))
@@ -53,7 +46,6 @@ export function stampPythonProjects(
53
46
  const info = pythonProjectInfo(source, { parse, stringify });
54
47
  return {
55
48
  directory: basename(resolve(path, "..")),
56
- mode: statSync(path).mode,
57
49
  name: info.name,
58
50
  version: info.version,
59
51
  path,
@@ -62,44 +54,7 @@ export function stampPythonProjects(
62
54
  uniffi: info.uniffi,
63
55
  };
64
56
  });
65
- const projects = allProjects.filter(
66
- (project) => !project.private && (!options.versions || options.versions.has(project.directory)),
67
- );
68
- if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
69
-
70
- try {
71
- for (const project of projects) {
72
- const stamped = stampPythonProject(project.source, {
73
- packages: allProjects,
74
- rewriteDependencies: options.rewriteDependencies,
75
- usePackageVersions: options.versions !== undefined,
76
- toml: { parse, stringify },
77
- version: options.versions?.get(project.directory) ?? version,
78
- });
79
- chmodSync(project.path, project.mode | 0o200);
80
- writeFileSync(project.path, stamped);
81
- chmodSync(project.path, project.mode);
82
- }
83
- } catch (error) {
84
- for (const project of projects) {
85
- chmodSync(project.path, project.mode | 0o200);
86
- writeFileSync(project.path, project.source);
87
- chmodSync(project.path, project.mode);
88
- }
89
- throw error;
90
- }
91
-
92
- const restore = () => {
93
- for (const project of projects) {
94
- chmodSync(project.path, project.mode | 0o200);
95
- writeFileSync(project.path, project.source);
96
- chmodSync(project.path, project.mode);
97
- }
98
- };
99
- Object.defineProperty(restore, "paths", {
100
- value: projects.map((project) => project.path),
101
- });
102
- return restore as RestorePythonProjects;
57
+ return allProjects;
103
58
  }
104
59
 
105
60
  export function publishPythonProjects(options: {
@@ -108,38 +63,12 @@ export function publishPythonProjects(options: {
108
63
  readonly publishUrl: string;
109
64
  readonly root: string;
110
65
  readonly version: string;
111
- readonly plan?: ReleasePlan;
112
66
  }): void {
113
- const root = resolve(options.root);
114
67
  const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
115
- const planned = new Map(
116
- options.plan?.pythonPackages.map((pkg) => [pkg.path.replace(/^.*\//, ""), pkg.version]) ?? [],
117
- );
118
- const stamp = stampPythonProjects(root, options.version, {
119
- ...(options.plan ? { versions: planned } : {}),
120
- });
121
68
  try {
122
- const packages = options.plan?.pythonPackages ?? [];
123
- if (packages.length > 0) {
124
- for (const pkg of packages) {
125
- exec.spawnSync("uv", ["build", "--package", pkg.identity, "--out-dir", output], {
126
- cwd: process.cwd(),
127
- stdout: "inherit",
128
- stderr: "inherit",
129
- stdin: "ignore",
130
- check: true,
131
- });
132
- }
133
- } else {
134
- exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
135
- cwd: process.cwd(),
136
- stdout: "inherit",
137
- stderr: "inherit",
138
- stdin: "ignore",
139
- check: true,
140
- });
141
- }
142
- exec.spawnSync(
69
+ buildPythonProjects({ ...options, output });
70
+ runTaskCommand(
71
+ process.cwd(),
143
72
  "uvx",
144
73
  [
145
74
  "--from",
@@ -152,47 +81,91 @@ export function publishPythonProjects(options: {
152
81
  ...(options.dryRun ? ["--dry-run"] : []),
153
82
  output,
154
83
  ],
155
- {
156
- cwd: process.cwd(),
157
- env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
158
- stdout: "inherit",
159
- stderr: "inherit",
160
- stdin: "ignore",
161
- check: true,
162
- },
84
+ { env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
163
85
  );
164
86
  } finally {
165
- stamp();
166
87
  rmSync(output, { recursive: true, force: true });
167
88
  }
168
89
  }
169
90
 
91
+ /** Build the exact Python release distributions without publishing them. */
92
+ export function buildPythonProjects(options: {
93
+ readonly allowEmpty?: boolean;
94
+ readonly excludeUniFFI?: boolean;
95
+ readonly output: string;
96
+ readonly root: string;
97
+ readonly version: string;
98
+ }): void {
99
+ const root = resolve(options.root);
100
+ const output = resolve(options.output);
101
+ rmSync(output, { recursive: true, force: true });
102
+ const allProjects = pythonProjects(root);
103
+ const projects = allProjects.filter(
104
+ (project) => !project.private && (!options.excludeUniFFI || !project.uniffi),
105
+ );
106
+ if (projects.length === 0 && options.allowEmpty) {
107
+ mkdirSync(output, { recursive: true });
108
+ return;
109
+ }
110
+ if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
111
+
112
+ const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
113
+ try {
114
+ mkdirSync(output, { recursive: true });
115
+ for (const project of projects) {
116
+ const packageRoot = join(temporaryRoot, project.directory);
117
+ cpSync(dirname(project.path), packageRoot, { recursive: true });
118
+ const manifestPath = join(packageRoot, "pyproject.toml");
119
+ chmodSync(manifestPath, 0o644);
120
+ writeFileSync(
121
+ manifestPath,
122
+ preparePythonProjectForPublication(project.source, {
123
+ packages: allProjects,
124
+ toml: { parse, stringify },
125
+ version: options.version,
126
+ }),
127
+ );
128
+ runTaskCommand(root, "uv", ["build", "--out-dir", output, packageRoot]);
129
+ }
130
+ const distributions = pythonDistributionPaths(output);
131
+ if (distributions.length === 0) {
132
+ throw new Error(`No Python distributions found in ${output}`);
133
+ }
134
+ runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
135
+ } finally {
136
+ rmSync(temporaryRoot, { recursive: true, force: true });
137
+ }
138
+ }
139
+
170
140
  if (import.meta.main) {
171
141
  const program = new Command();
172
142
  program
173
- .argument("[version]", "Python package version")
143
+ .argument("<version>", "Python package version")
174
144
  .requiredOption("--index-url <url>", "devpi Simple API URL")
175
145
  .requiredOption("--publish-url <url>", "devpi writable index URL")
176
146
  .option("--root <path>", "Python workspace package root", "packages/py")
177
- .option("--plan <path>", "affected release plan")
147
+ .option("--output <path>", "Build distributions into a directory without publishing")
178
148
  .option("--dry-run", "build and inspect distributions without uploading")
179
149
  .action(
180
150
  (
181
- version: string | undefined,
151
+ version: string,
182
152
  options: {
183
153
  dryRun?: boolean;
184
154
  indexUrl: string;
185
155
  publishUrl: string;
186
156
  root: string;
187
- plan?: string;
157
+ output?: string;
188
158
  },
189
159
  ) => {
190
- const plan = options.plan
191
- ? (JSON.parse(readFileSync(options.plan, "utf8")) as ReleasePlan)
192
- : undefined;
193
- const fallbackVersion = version ?? plan?.pythonPackages[0]?.version;
194
- if (!fallbackVersion) throw new Error("Python publication requires a version or plan");
195
- publishPythonProjects({ ...options, version: fallbackVersion, plan });
160
+ if (options.output) {
161
+ buildPythonProjects({
162
+ output: options.output,
163
+ root: options.root,
164
+ version,
165
+ });
166
+ } else {
167
+ publishPythonProjects({ ...options, version });
168
+ }
196
169
  },
197
170
  );
198
171
  await program.parseAsync();