@dbx-tools/projen 0.6.78 → 0.6.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/package.json +4 -4
- package/tasks/bump.ts +52 -5
- package/tasks/publish-python.ts +146 -0
- package/tasks/python-registry.ts +87 -0
package/README.md
CHANGED
|
@@ -260,6 +260,19 @@ a new package is covered without a re-synth. Work from the root:
|
|
|
260
260
|
| `bun run barrels` | regenerate the read-only `index.ts` barrels |
|
|
261
261
|
| `bun run bump` | version, tag, and publish |
|
|
262
262
|
|
|
263
|
+
`bump` also mirrors a release into local registries when the active clients are
|
|
264
|
+
pointed at loopback services. npm uses `npm config get registry` and publishes
|
|
265
|
+
to a local Verdaccio automatically. Python prefers uv's default index and only
|
|
266
|
+
treats a loopback `.../+simple/` URL as writable devpi; a read-only cache such as
|
|
267
|
+
proxpi (`.../index/`) is deliberately ignored. The task stamps every Python
|
|
268
|
+
member and its sibling dependencies to the release version, builds the workspace
|
|
269
|
+
with uv, then runs `devpi upload --from-dir` against the derived writable index.
|
|
270
|
+
Devpi client authentication remains in its normal `~/.devpi` state.
|
|
271
|
+
|
|
272
|
+
Use `--local-registry false` or `--local-pypi false` to disable either local
|
|
273
|
+
publish. An explicit `--local-pypi http://localhost:3141/user/index/` overrides
|
|
274
|
+
auto-detection; `--python-root` defaults to `packages/py`.
|
|
275
|
+
|
|
263
276
|
Members intentionally keep only the tasks that something OTHER than a human
|
|
264
277
|
invokes, so there is no second place to run the same thing:
|
|
265
278
|
|
package/package.json
CHANGED
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^1.7.0",
|
|
29
|
-
"@dbx-tools/core": "0.6.
|
|
30
|
-
"@dbx-tools/path": "0.6.
|
|
31
|
-
"@dbx-tools/shared-core": "0.6.
|
|
29
|
+
"@dbx-tools/core": "0.6.81",
|
|
30
|
+
"@dbx-tools/path": "0.6.81",
|
|
31
|
+
"@dbx-tools/shared-core": "0.6.81",
|
|
32
32
|
"commander": "^15.0.0",
|
|
33
33
|
"concurrently": "^10.0.3",
|
|
34
34
|
"constructs": "^10.6.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"main": "index.ts",
|
|
49
49
|
"license": "Apache-2.0",
|
|
50
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.81",
|
|
51
51
|
"types": "index.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/tasks/bump.ts
CHANGED
|
@@ -30,12 +30,11 @@
|
|
|
30
30
|
* `--publish` / `--no-publish` is an alias for `--push` (pushing the tag is
|
|
31
31
|
* what publishes). The tag prefix comes from `--prefix` (default `v`).
|
|
32
32
|
*
|
|
33
|
-
* `--local-registry <value>` publishes
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* local registry. Values:
|
|
33
|
+
* `--local-registry <value>` publishes npm packages to a LOCAL registry (e.g.
|
|
34
|
+
* verdaccio) right after the tag push. `--local-pypi <value>` does the same for
|
|
35
|
+
* Python packages through a writable devpi index. Values for both:
|
|
37
36
|
* - `auto` (default): publish only when `npm config get registry` is a
|
|
38
|
-
* loopback host
|
|
37
|
+
* loopback host, or uv's default index is a loopback devpi `+simple` URL.
|
|
39
38
|
* - `false`: never publish locally.
|
|
40
39
|
* - a URL: always publish to that registry.
|
|
41
40
|
*/
|
|
@@ -45,6 +44,7 @@ import { fileURLToPath } from "node:url";
|
|
|
45
44
|
import { exec, project } from "@dbx-tools/core";
|
|
46
45
|
import { log, net } from "@dbx-tools/shared-core";
|
|
47
46
|
import { Command, Option } from "commander";
|
|
47
|
+
import { activePythonIndex, resolveLocalPypi } from "./python-registry.ts";
|
|
48
48
|
|
|
49
49
|
const logger = log.logger("projen:bump");
|
|
50
50
|
const LEVELS = ["patch", "minor", "major"] as const;
|
|
@@ -188,6 +188,12 @@ program
|
|
|
188
188
|
"publish locally after the tag push: 'auto' (only a loopback npm registry), 'false', or a registry URL",
|
|
189
189
|
"auto",
|
|
190
190
|
)
|
|
191
|
+
.option(
|
|
192
|
+
"--local-pypi <value>",
|
|
193
|
+
"publish Python packages locally: 'auto' (only a loopback devpi +simple index), 'false', or a devpi URL",
|
|
194
|
+
"auto",
|
|
195
|
+
)
|
|
196
|
+
.option("--python-root <path>", "Python workspace package root", "packages/py")
|
|
191
197
|
.action(
|
|
192
198
|
(opts: {
|
|
193
199
|
level: Level;
|
|
@@ -199,7 +205,9 @@ program
|
|
|
199
205
|
tag: boolean;
|
|
200
206
|
push: boolean;
|
|
201
207
|
publish: boolean;
|
|
208
|
+
localPypi: string;
|
|
202
209
|
localRegistry: string;
|
|
210
|
+
pythonRoot: string;
|
|
203
211
|
}) => {
|
|
204
212
|
const pkgPath = resolve(process.cwd(), "package.json");
|
|
205
213
|
if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
|
|
@@ -314,6 +322,45 @@ program
|
|
|
314
322
|
logger.success(`published ${version} to ${localRegistry}`);
|
|
315
323
|
}
|
|
316
324
|
|
|
325
|
+
const activeIndex = activePythonIndex();
|
|
326
|
+
const localPypi = resolveLocalPypi(opts.localPypi, activeIndex);
|
|
327
|
+
const pythonRoot = resolve(opts.pythonRoot);
|
|
328
|
+
if (
|
|
329
|
+
opts.localPypi.toLowerCase() === "auto" &&
|
|
330
|
+
activeIndex &&
|
|
331
|
+
net.isLoopbackHost(new URL(activeIndex)) &&
|
|
332
|
+
!localPypi
|
|
333
|
+
) {
|
|
334
|
+
logger.info(`skipped local Python publish: ${activeIndex} is not a devpi +simple index`);
|
|
335
|
+
}
|
|
336
|
+
if (opts.version === false && localPypi) {
|
|
337
|
+
logger.info("skipped local Python publish (--no-version left packages unstamped)");
|
|
338
|
+
} else if (opts.version && localPypi && existsSync(pythonRoot)) {
|
|
339
|
+
logger.info(`publishing Python ${version} to local devpi ${localPypi.publishUrl}`);
|
|
340
|
+
const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
|
|
341
|
+
exec.spawnSync(
|
|
342
|
+
"bun",
|
|
343
|
+
[
|
|
344
|
+
publishPythonScript,
|
|
345
|
+
version,
|
|
346
|
+
"--root",
|
|
347
|
+
pythonRoot,
|
|
348
|
+
"--index-url",
|
|
349
|
+
localPypi.indexUrl,
|
|
350
|
+
"--publish-url",
|
|
351
|
+
localPypi.publishUrl,
|
|
352
|
+
],
|
|
353
|
+
{
|
|
354
|
+
cwd: process.cwd(),
|
|
355
|
+
stdout: "inherit",
|
|
356
|
+
stderr: "inherit",
|
|
357
|
+
stdin: "ignore",
|
|
358
|
+
check: true,
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
logger.success(`published Python ${version} to ${localPypi.publishUrl}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
317
364
|
// Publishing can run package lifecycle hooks, including a standalone
|
|
318
365
|
// project's own projen synth, which rewrites its generated manifest back
|
|
319
366
|
// to 0.0.0. Re-assert the release version last so root and every sibling
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { basename, join, resolve } from "node:path";
|
|
14
|
+
import { exec } from "@dbx-tools/core";
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
|
|
17
|
+
interface PythonProjectFile {
|
|
18
|
+
readonly directory: string;
|
|
19
|
+
readonly mode: number;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly source: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeRegExp(value: string): string {
|
|
26
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function stampPythonProjects(root: string, version: string): () => void {
|
|
30
|
+
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
33
|
+
.filter(existsSync)
|
|
34
|
+
.sort();
|
|
35
|
+
const projects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
36
|
+
const source = readFileSync(path, "utf8");
|
|
37
|
+
const name = /^name = "([^"]+)"$/m.exec(source)?.[1];
|
|
38
|
+
if (!name) throw new Error(`Missing project name in ${path}`);
|
|
39
|
+
return {
|
|
40
|
+
directory: basename(resolve(path, "..")),
|
|
41
|
+
mode: statSync(path).mode,
|
|
42
|
+
name,
|
|
43
|
+
path,
|
|
44
|
+
source,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
for (const project of projects) {
|
|
51
|
+
let stamped = project.source.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
|
|
52
|
+
if (stamped === project.source) {
|
|
53
|
+
throw new Error(`Expected one project version in ${project.path}`);
|
|
54
|
+
}
|
|
55
|
+
for (const sibling of projects) {
|
|
56
|
+
stamped = stamped.replace(
|
|
57
|
+
new RegExp(
|
|
58
|
+
`${escapeRegExp(sibling.name)} @ git\\+[^" ]+#subdirectory=[^" ]+/${escapeRegExp(sibling.directory)}`,
|
|
59
|
+
"g",
|
|
60
|
+
),
|
|
61
|
+
`${sibling.name}==${version}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
65
|
+
writeFileSync(project.path, stamped);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
for (const project of projects) {
|
|
69
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
70
|
+
writeFileSync(project.path, project.source);
|
|
71
|
+
chmodSync(project.path, project.mode);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
for (const project of projects) {
|
|
78
|
+
chmodSync(project.path, project.mode | 0o200);
|
|
79
|
+
writeFileSync(project.path, project.source);
|
|
80
|
+
chmodSync(project.path, project.mode);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function publishPythonProjects(options: {
|
|
86
|
+
readonly dryRun?: boolean;
|
|
87
|
+
readonly indexUrl: string;
|
|
88
|
+
readonly publishUrl: string;
|
|
89
|
+
readonly root: string;
|
|
90
|
+
readonly version: string;
|
|
91
|
+
}): void {
|
|
92
|
+
const root = resolve(options.root);
|
|
93
|
+
const output = mkdtempSync(join(tmpdir(), "dbx-tools-python-publish-"));
|
|
94
|
+
const restore = stampPythonProjects(root, options.version);
|
|
95
|
+
try {
|
|
96
|
+
exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
stdout: "inherit",
|
|
99
|
+
stderr: "inherit",
|
|
100
|
+
stdin: "ignore",
|
|
101
|
+
check: true,
|
|
102
|
+
});
|
|
103
|
+
exec.spawnSync(
|
|
104
|
+
"uvx",
|
|
105
|
+
[
|
|
106
|
+
"--from",
|
|
107
|
+
"devpi-client",
|
|
108
|
+
"devpi",
|
|
109
|
+
"upload",
|
|
110
|
+
"--index",
|
|
111
|
+
options.publishUrl,
|
|
112
|
+
"--from-dir",
|
|
113
|
+
...(options.dryRun ? ["--dry-run"] : []),
|
|
114
|
+
output,
|
|
115
|
+
],
|
|
116
|
+
{
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl },
|
|
119
|
+
stdout: "inherit",
|
|
120
|
+
stderr: "inherit",
|
|
121
|
+
stdin: "ignore",
|
|
122
|
+
check: true,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
} finally {
|
|
126
|
+
restore();
|
|
127
|
+
rmSync(output, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
const program = new Command();
|
|
133
|
+
program
|
|
134
|
+
.argument("<version>", "Python package version")
|
|
135
|
+
.requiredOption("--index-url <url>", "devpi Simple API URL")
|
|
136
|
+
.requiredOption("--publish-url <url>", "devpi writable index URL")
|
|
137
|
+
.option("--root <path>", "Python workspace package root", "packages/py")
|
|
138
|
+
.option("--dry-run", "build and inspect distributions without uploading")
|
|
139
|
+
.action(
|
|
140
|
+
(
|
|
141
|
+
version: string,
|
|
142
|
+
options: { dryRun?: boolean; indexUrl: string; publishUrl: string; root: string },
|
|
143
|
+
) => publishPythonProjects({ ...options, version }),
|
|
144
|
+
);
|
|
145
|
+
await program.parseAsync();
|
|
146
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { exec } from "@dbx-tools/core";
|
|
5
|
+
import { net } from "@dbx-tools/shared-core";
|
|
6
|
+
|
|
7
|
+
export interface LocalPythonRegistry {
|
|
8
|
+
readonly indexUrl: string;
|
|
9
|
+
readonly publishUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Read the default index URL from uv's TOML configuration. */
|
|
13
|
+
export function parseUvDefaultIndex(source: string): string | undefined {
|
|
14
|
+
const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
|
|
15
|
+
for (const block of blocks) {
|
|
16
|
+
if (!/^\[\[index\]\]\s*$/m.test(block) || !/^\s*default\s*=\s*true\s*$/m.test(block)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
|
|
20
|
+
if (url) return url;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Convert a devpi Simple API URL into its writable index URL. */
|
|
26
|
+
export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
27
|
+
let url: URL;
|
|
28
|
+
try {
|
|
29
|
+
url = new URL(index);
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!net.isLoopbackHost(url)) return undefined;
|
|
34
|
+
|
|
35
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
36
|
+
if (!path.endsWith("/+simple")) return undefined;
|
|
37
|
+
url.pathname = `${path.slice(0, -"/+simple".length)}/`;
|
|
38
|
+
url.search = "";
|
|
39
|
+
url.hash = "";
|
|
40
|
+
return {
|
|
41
|
+
indexUrl: new URL("+simple/", url).href,
|
|
42
|
+
publishUrl: url.href,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The active Python package index, preferring uv because Python builds use uv. */
|
|
47
|
+
export function activePythonIndex(): string | undefined {
|
|
48
|
+
for (const value of [process.env.UV_DEFAULT_INDEX, process.env.UV_INDEX_URL]) {
|
|
49
|
+
if (value?.trim()) return value.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const uvConfig = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml");
|
|
53
|
+
if (existsSync(uvConfig)) {
|
|
54
|
+
const index = parseUvDefaultIndex(readFileSync(uvConfig, "utf8"));
|
|
55
|
+
if (index) return index;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.env.PIP_INDEX_URL?.trim()) return process.env.PIP_INDEX_URL.trim();
|
|
59
|
+
const pip = exec.spawnSync("python", ["-m", "pip", "config", "get", "global.index-url"], {
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
stdout: "capture",
|
|
62
|
+
stderr: "ignore",
|
|
63
|
+
stdin: "ignore",
|
|
64
|
+
check: false,
|
|
65
|
+
});
|
|
66
|
+
return pip.stdout?.trim() || undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Resolve `auto`, `false`, or an explicit devpi index/publish URL. */
|
|
70
|
+
export function resolveLocalPypi(
|
|
71
|
+
value: string,
|
|
72
|
+
activeIndex: string | undefined = activePythonIndex(),
|
|
73
|
+
): LocalPythonRegistry | undefined {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
76
|
+
if (trimmed.toLowerCase() === "auto") {
|
|
77
|
+
return activeIndex ? devpiRegistry(activeIndex) : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const derived = devpiRegistry(trimmed);
|
|
81
|
+
if (derived) return derived;
|
|
82
|
+
const publishUrl = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
|
83
|
+
return {
|
|
84
|
+
publishUrl,
|
|
85
|
+
indexUrl: new URL("+simple/", publishUrl).href,
|
|
86
|
+
};
|
|
87
|
+
}
|