@dbx-tools/projen 0.6.90 → 0.6.92
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 +19 -3
- package/index.ts +4 -0
- package/package.json +4 -4
- package/src/barrels.ts +45 -6
- package/src/openapi.ts +3 -1
- package/src/packages.ts +3 -2
- package/src/project-js.ts +9 -1
- package/src/project-predicate.ts +1 -1
- package/src/project-py.ts +15 -3
- package/src/project.ts +1 -1
- package/src/release.ts +3 -3
- package/src/workspace-version.ts +155 -0
- package/tasks/bump.ts +90 -107
- package/tasks/publish-python.ts +35 -14
- package/tasks/publish.ts +14 -8
- package/tasks/python-registry.ts +149 -18
package/README.md
CHANGED
|
@@ -293,6 +293,22 @@ invokes, so there is no second place to run the same thing:
|
|
|
293
293
|
|
|
294
294
|
Do NOT run `projen default` (or `bunx projen`) from inside a member. Synth is a
|
|
295
295
|
whole-tree operation driven by the ROOT `.projenrc.ts`, so a member-level run
|
|
296
|
-
re-synths the entire workspace from the member's directory
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
re-synths the entire workspace from the member's directory. Run `bun run sync`
|
|
297
|
+
from the root instead.
|
|
298
|
+
|
|
299
|
+
## Versioning
|
|
300
|
+
|
|
301
|
+
The whole repo shares ONE version, stored in the root `VERSION` file (a plain
|
|
302
|
+
`x.y.z` string; a fresh tree with no file defaults to `0.0.1`). Synth COPIES that
|
|
303
|
+
value into every generated manifest - the root and `projen/` `package.json`, every
|
|
304
|
+
JS member, every Python `pyproject.toml`, the generated openapi packages, and the
|
|
305
|
+
example apps - so the packages, the engine, and the examples always match.
|
|
306
|
+
`src/workspace-version.ts` owns reading and writing it. Synth only ever reads it;
|
|
307
|
+
it never resets, upgrades, or downgrades a version on its own.
|
|
308
|
+
|
|
309
|
+
`bun run bump` is the only command that changes the number: it fetches the remote
|
|
310
|
+
tags once, takes the highest tag across `v*` and every sibling prefix as the base
|
|
311
|
+
(falling back to the local `VERSION` file when the remote is unreachable or has no
|
|
312
|
+
tag), increments by `--level`, writes `VERSION`, then synths so every manifest
|
|
313
|
+
copies it. The remote is consulted only on `bump` and on one-time creation of a
|
|
314
|
+
missing `VERSION` file - never on an ordinary synth.
|
package/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Regenerated from the exporting modules in ./src.
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
|
+
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
5
6
|
export * as barrels from "./src/barrels.ts";
|
|
6
7
|
export * as bunApp from "./src/bun-app.ts";
|
|
7
8
|
export * as clean from "./src/clean.ts";
|
|
@@ -25,6 +26,7 @@ export * as tags from "./src/tags.ts";
|
|
|
25
26
|
export * as tsconfig from "./src/tsconfig.ts";
|
|
26
27
|
export * as vscode from "./src/vscode.ts";
|
|
27
28
|
export * as watch from "./src/watch.ts";
|
|
29
|
+
export * as workspaceVersion from "./src/workspace-version.ts";
|
|
28
30
|
export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
|
|
29
31
|
export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
|
|
30
32
|
export type { DBXToolsConfigOptions } from "./src/dbx-tools-config.ts";
|
|
@@ -49,3 +51,5 @@ export type { PackageTag } from "./src/tags.ts";
|
|
|
49
51
|
export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
|
|
50
52
|
export { DBXToolsVsCode } from "./src/vscode.ts";
|
|
51
53
|
export type { IgnoreGroupOptions } from "./src/watch.ts";
|
|
54
|
+
export { VERSION_FILE, DEFAULT_VERSION } from "./src/workspace-version.ts";
|
|
55
|
+
export type { Semver } from "./src/workspace-version.ts";
|
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.92",
|
|
30
|
+
"@dbx-tools/path": "0.6.92",
|
|
31
|
+
"@dbx-tools/shared-core": "0.6.92",
|
|
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.92",
|
|
51
51
|
"types": "index.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/src/barrels.ts
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* `export { ... }`. The module namespaces stay either way, so a namespaced call
|
|
28
28
|
* site keeps working.
|
|
29
29
|
*
|
|
30
|
+
* Every generated barrel also exports `PACKAGE_IDENTIFIER` from the package's own
|
|
31
|
+
* `package.json`. Runtime helpers can retain the package specifier without
|
|
32
|
+
* trying to recover it from an ESM namespace object or loader function.
|
|
33
|
+
*
|
|
30
34
|
* Uniqueness is tallied over types and values TOGETHER: a name carried by two
|
|
31
35
|
* modules is ambiguous whichever kind it is, and hoisting one module's value
|
|
32
36
|
* beside another's same-named type would emit two conflicting re-exports. Such a
|
|
@@ -47,7 +51,7 @@
|
|
|
47
51
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
48
52
|
import { join, relative } from "node:path";
|
|
49
53
|
import { find } from "@dbx-tools/path";
|
|
50
|
-
import { string } from "@dbx-tools/shared-core";
|
|
54
|
+
import { json, string } from "@dbx-tools/shared-core";
|
|
51
55
|
import isIdentifier from "is-identifier";
|
|
52
56
|
import { header, isGenerated, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
|
|
53
57
|
import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
|
|
@@ -101,6 +105,11 @@ const BARREL_HEADER: HeaderOpts = {
|
|
|
101
105
|
source: "the exporting modules in ./src",
|
|
102
106
|
};
|
|
103
107
|
|
|
108
|
+
/** Generated package identity export, reserved against source-module hoisting. */
|
|
109
|
+
const PACKAGE_IDENTIFIER_EXPORT = "PACKAGE_IDENTIFIER";
|
|
110
|
+
const PACKAGE_IDENTIFIER_LINE = `export const ${PACKAGE_IDENTIFIER_EXPORT} = "";`;
|
|
111
|
+
const PACKAGE_IDENTIFIER_LINE_RE = /^export const PACKAGE_IDENTIFIER = .*;$/m;
|
|
112
|
+
|
|
104
113
|
/** `pnpm-workspace` -> `pnpmWorkspace`; `local-fs` -> `localFS` (`fs` -> `FS`). */
|
|
105
114
|
function kebabToCamel(segment: string): string {
|
|
106
115
|
const tokens = [...string.tokenizeWithOptions({ lowerCase: true, capitalize: true }, segment)];
|
|
@@ -262,6 +271,31 @@ function mergeCustomExports(content: string, pkgDir: string): string {
|
|
|
262
271
|
return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports.ts";\n`;
|
|
263
272
|
}
|
|
264
273
|
|
|
274
|
+
/** Read the authoritative npm package name emitted by the package project. */
|
|
275
|
+
function packageIdentifier(pkgDir: string): string {
|
|
276
|
+
const manifestPath = join(pkgDir, "package.json");
|
|
277
|
+
const name = existsSync(manifestPath)
|
|
278
|
+
? json.parseRecord(readFileSync(manifestPath, "utf8"))?.name
|
|
279
|
+
: undefined;
|
|
280
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
281
|
+
throw new Error(`Cannot generate barrel without package.json name: ${manifestPath}`);
|
|
282
|
+
}
|
|
283
|
+
return name;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Normalize package identity so structural barrel comparisons need no manifest read. */
|
|
287
|
+
function withoutPackageIdentifier(content: string): string {
|
|
288
|
+
return content.replace(PACKAGE_IDENTIFIER_LINE_RE, PACKAGE_IDENTIFIER_LINE);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Resolve and insert package identity only when a barrel is about to be written. */
|
|
292
|
+
function withPackageIdentifier(content: string, pkgDir: string): string {
|
|
293
|
+
const line = `export const ${PACKAGE_IDENTIFIER_EXPORT} = ${JSON.stringify(
|
|
294
|
+
packageIdentifier(pkgDir),
|
|
295
|
+
)};`;
|
|
296
|
+
return content.replace(PACKAGE_IDENTIFIER_LINE_RE, () => line);
|
|
297
|
+
}
|
|
298
|
+
|
|
265
299
|
/**
|
|
266
300
|
* Rebuild one package's root barrel. Returns 1 only if the barrel's contents
|
|
267
301
|
* actually changed - a module was added, removed, renamed, or toggled its
|
|
@@ -317,30 +351,35 @@ function generateForPackage(pkgDir: string): number {
|
|
|
317
351
|
// extension is written because `tsc` rewrites it on emit
|
|
318
352
|
// (`rewriteRelativeImportExtensions`) - an extensionless specifier would be
|
|
319
353
|
// copied through verbatim and Node's ESM resolver cannot probe for it.
|
|
320
|
-
|
|
354
|
+
const namespaceExports = modulePaths
|
|
321
355
|
.map((stem) => {
|
|
322
356
|
const modulePath = `./src/${byModulePath.get(stem)!}`;
|
|
323
357
|
return `export * as ${modulePathToNamespace(modulePath)} from "${modulePath}";`;
|
|
324
358
|
})
|
|
325
359
|
.join("\n");
|
|
360
|
+
let content = `${PACKAGE_IDENTIFIER_LINE}\n${namespaceExports}`;
|
|
326
361
|
// Hoist package-unique named exports to the top level. Names a hand-authored
|
|
327
362
|
// `exports.ts` declares are suppressed so that file stays authoritative.
|
|
328
363
|
const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
|
|
329
364
|
const suppress = existsSync(customPath) ? customExportNames(customPath) : new Set<string>();
|
|
365
|
+
suppress.add(PACKAGE_IDENTIFIER_EXPORT);
|
|
330
366
|
content = hoistUniqueExports(content, pkgDir, suppress);
|
|
331
367
|
// A sibling `exports.ts` overrides/extends the generated barrel and wins on conflict.
|
|
332
368
|
content = mergeCustomExports(content, pkgDir);
|
|
333
369
|
|
|
334
|
-
// The barrel only *changes* when its
|
|
335
|
-
//
|
|
370
|
+
// The barrel only *changes* when its export structure does. Package identity is
|
|
371
|
+
// blanked on both sides of this comparison, so a no-op cycle never reads
|
|
372
|
+
// package.json merely to reconstruct a value the existing barrel already has.
|
|
373
|
+
// If the structural result matches what's on disk, leave the file - and its
|
|
336
374
|
// read-only bit - completely untouched and report no change (0). This keeps the
|
|
337
375
|
// watcher quiet on ordinary in-file edits (which leave the export * as … list
|
|
338
376
|
// identical), and it is also what keeps a no-op cycle off the read-only bit
|
|
339
377
|
// entirely: see {@link writeBarrel} for why unlocking a barrel we are not about
|
|
340
378
|
// to rewrite is what produced spurious EACCES failures.
|
|
341
379
|
content = `${content.replace(/\n+$/, "")}\n`;
|
|
342
|
-
const
|
|
343
|
-
if (before ===
|
|
380
|
+
const template = `${header(BARREL_HEADER)}\n${content}`;
|
|
381
|
+
if (before !== undefined && withoutPackageIdentifier(before) === template) return 0;
|
|
382
|
+
const next = withPackageIdentifier(template, pkgDir);
|
|
344
383
|
|
|
345
384
|
// Written whole (header included) rather than via `stampGenerated`, which would
|
|
346
385
|
// re-read and rewrite the file to prepend the same header - a second write, and
|
package/src/openapi.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
toPosix,
|
|
49
49
|
recordedPackages,
|
|
50
50
|
} from "./packages.ts";
|
|
51
|
+
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
51
52
|
|
|
52
53
|
const logger = log.logger("projen:openapi");
|
|
53
54
|
|
|
@@ -173,6 +174,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
173
174
|
skipLibCheck: true,
|
|
174
175
|
};
|
|
175
176
|
|
|
177
|
+
const specVersion = readWorkspaceVersion(repoRoot);
|
|
176
178
|
const written: string[] = [];
|
|
177
179
|
for (const p of pkgs) {
|
|
178
180
|
// The generated package's folder is the source's leaf folder name (`api`), not
|
|
@@ -197,7 +199,7 @@ export async function generateOpenapi(): Promise<string[]> {
|
|
|
197
199
|
specFileBaseName: "openapi",
|
|
198
200
|
specVersion: 3,
|
|
199
201
|
name: `${p.relPath} API`,
|
|
200
|
-
version:
|
|
202
|
+
version: specVersion,
|
|
201
203
|
},
|
|
202
204
|
compilerOptions,
|
|
203
205
|
);
|
package/src/packages.ts
CHANGED
|
@@ -177,10 +177,11 @@ function readRecordedMembers(projectRoot: string = repoRoot): string[] {
|
|
|
177
177
|
return doc?.packages ?? [];
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
/** A member path
|
|
180
|
+
/** A workspace member path as a {@link DiscoveredPackage}, including root-level members. */
|
|
181
181
|
function packageOfMember(projectRoot: string, member: string): DiscoveredPackage | undefined {
|
|
182
182
|
const segs = toPosix(member).split("/").filter(Boolean);
|
|
183
|
-
if (segs.length
|
|
183
|
+
if (segs.length === 0) return undefined;
|
|
184
|
+
if (segs.length === 1) return new DiscoveredPackage(projectRoot, segs[0]!, []);
|
|
184
185
|
return new DiscoveredPackage(projectRoot, segs[0]!, segs.slice(1));
|
|
185
186
|
}
|
|
186
187
|
|
package/src/project-js.ts
CHANGED
|
@@ -36,12 +36,13 @@ import {
|
|
|
36
36
|
toPosix,
|
|
37
37
|
} from "./packages.ts";
|
|
38
38
|
import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
|
|
39
|
+
import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
|
|
39
40
|
import { applyCompiledPublish } from "./publish.ts";
|
|
40
41
|
import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
|
|
41
42
|
import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
|
|
42
43
|
import { DBXToolsRootTsconfig } from "./tsconfig.ts";
|
|
43
44
|
import { DBXToolsVsCode } from "./vscode.ts";
|
|
44
|
-
import
|
|
45
|
+
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* The dbx-tools project surface, backed by projen's Node toolchain. A single
|
|
@@ -584,6 +585,9 @@ export class DBXToolsNodeProject
|
|
|
584
585
|
// component, but the file is still required by the Databricks Apps platform
|
|
585
586
|
// (its build phase installs with pnpm and reads catalog + `allowBuilds`).
|
|
586
587
|
pnpmWorkspace.attachWorkspaceFile(this);
|
|
588
|
+
// Copy the single workspace version onto the root manifest. The `VERSION` file
|
|
589
|
+
// at the workspace root is the source of truth; synth only reads it.
|
|
590
|
+
this.package.addField("version", readWorkspaceVersion(this.outdir));
|
|
587
591
|
this.scope = scope;
|
|
588
592
|
this.extraWorkspaceMembers = options.extraWorkspaceMembers ?? [];
|
|
589
593
|
this.rootInstallOnly = options.rootInstallOnly !== false;
|
|
@@ -678,6 +682,10 @@ export class DBXToolsTypeScriptProject
|
|
|
678
682
|
".": "./index.ts",
|
|
679
683
|
"./package.json": "./package.json",
|
|
680
684
|
});
|
|
685
|
+
// Every package carries the single workspace version, copied from the root
|
|
686
|
+
// `VERSION` file (the source of truth). `this.root` is the workspace root for a
|
|
687
|
+
// discovered member and this project itself for a standalone compiling root.
|
|
688
|
+
this.package.addField("version", readWorkspaceVersion(this.root.outdir));
|
|
681
689
|
addPackageFiles(this, "index.ts", "src");
|
|
682
690
|
// `bun test` intercepts `node:test` (the suites keep using node:test) and
|
|
683
691
|
// runs it with bun's own fast runner. Args are FILTERS, not globs; a bare
|
package/src/project-predicate.ts
CHANGED
|
@@ -11,8 +11,8 @@ import { IConstruct } from "constructs";
|
|
|
11
11
|
import { Project } from "projen";
|
|
12
12
|
import { project } from "..";
|
|
13
13
|
import { toPosix } from "./packages.ts";
|
|
14
|
-
import type { DBXToolsProject } from "./project.ts";
|
|
15
14
|
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
15
|
+
import type { DBXToolsProject } from "./project.ts";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Guard: the construct is a projen {@link Project} - the base every builder here
|
package/src/project-py.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Component, type Project, javascript, python, vscode } from "projen";
|
|
|
4
4
|
import { GithubWorkflow } from "projen/lib/github";
|
|
5
5
|
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
6
6
|
import type { DBXToolsProject, DBXToolsProjectOptions } from "./project.ts";
|
|
7
|
+
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
7
8
|
|
|
8
9
|
/** Git location used by direct `#subdirectory=` package dependencies. */
|
|
9
10
|
export interface PythonRepositoryOptions {
|
|
@@ -19,6 +20,7 @@ export interface PythonPackageOptions extends DBXToolsProjectOptions {
|
|
|
19
20
|
readonly module: string;
|
|
20
21
|
readonly description: string;
|
|
21
22
|
readonly dependencies?: readonly string[];
|
|
23
|
+
readonly scripts?: Readonly<Record<string, string>>;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Options for one projen-native Python workspace member. */
|
|
@@ -27,6 +29,8 @@ export interface DBXToolsPythonProjectOptions extends DBXToolsProjectOptions {
|
|
|
27
29
|
readonly package: PythonPackageOptions;
|
|
28
30
|
readonly repository: Required<PythonRepositoryOptions>;
|
|
29
31
|
readonly requiresPython: string;
|
|
32
|
+
/** Workspace version copied onto this package's `pyproject.toml`. */
|
|
33
|
+
readonly version: string;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** Python release workflow configuration. */
|
|
@@ -94,7 +98,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
94
98
|
moduleName: pkg.module,
|
|
95
99
|
authorName: "",
|
|
96
100
|
authorEmail: "",
|
|
97
|
-
version:
|
|
101
|
+
version: options.version,
|
|
98
102
|
description: pkg.description,
|
|
99
103
|
github: false,
|
|
100
104
|
sample: false,
|
|
@@ -111,7 +115,7 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
111
115
|
uvOptions: {
|
|
112
116
|
project: {
|
|
113
117
|
name: pkg.name,
|
|
114
|
-
version:
|
|
118
|
+
version: options.version,
|
|
115
119
|
description: pkg.description,
|
|
116
120
|
readme: "README.md",
|
|
117
121
|
requiresPython: options.requiresPython,
|
|
@@ -140,6 +144,9 @@ export class DBXToolsPythonProject extends python.PythonProject implements DBXTo
|
|
|
140
144
|
this.uv = this.packagingManager;
|
|
141
145
|
this.uv.file.addDeletionOverride("project.authors");
|
|
142
146
|
this.uv.file.addDeletionOverride("dependency-groups");
|
|
147
|
+
if (pkg.scripts) {
|
|
148
|
+
this.uv.file.addOverride("project.scripts", pkg.scripts);
|
|
149
|
+
}
|
|
143
150
|
this.uv.file.readonly = true;
|
|
144
151
|
|
|
145
152
|
for (const path of [".gitattributes", ".gitignore"]) {
|
|
@@ -159,6 +166,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
159
166
|
readonly packages: readonly DBXToolsPythonProject[];
|
|
160
167
|
readonly repository: Required<PythonRepositoryOptions>;
|
|
161
168
|
readonly requiresPython: string;
|
|
169
|
+
readonly version: string;
|
|
162
170
|
readonly file: python.PyprojectTomlFile;
|
|
163
171
|
|
|
164
172
|
constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
|
|
@@ -169,6 +177,9 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
169
177
|
root: options.repository.root ?? "packages/py",
|
|
170
178
|
};
|
|
171
179
|
this.requiresPython = options.requiresPython ?? ">=3.10";
|
|
180
|
+
// The single workspace version, copied from the root `VERSION` file so Python
|
|
181
|
+
// members carry the same number as their JS siblings.
|
|
182
|
+
this.version = readWorkspaceVersion(project.outdir);
|
|
172
183
|
this.file = this.emitWorkspace(project, options);
|
|
173
184
|
this.packages = options.packages.map(
|
|
174
185
|
(pkg) =>
|
|
@@ -177,6 +188,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
177
188
|
package: pkg,
|
|
178
189
|
repository: this.repository,
|
|
179
190
|
requiresPython: this.requiresPython,
|
|
191
|
+
version: this.version,
|
|
180
192
|
}),
|
|
181
193
|
);
|
|
182
194
|
for (const pkg of this.packages) {
|
|
@@ -216,7 +228,7 @@ export class DBXToolsPythonWorkspace extends Component {
|
|
|
216
228
|
const file = new python.PyprojectTomlFile(project, {
|
|
217
229
|
project: {
|
|
218
230
|
name: options.workspaceName ?? `${string.toSlug(project.name)}-python-workspace`,
|
|
219
|
-
version:
|
|
231
|
+
version: this.version,
|
|
220
232
|
requiresPython: this.requiresPython,
|
|
221
233
|
dependencies: [],
|
|
222
234
|
},
|
package/src/project.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { object, type OneOrMany } from "@dbx-tools/shared-core";
|
|
|
7
7
|
import { type IConstruct } from "constructs";
|
|
8
8
|
import { Project, type ProjectOptions } from "projen";
|
|
9
9
|
import * as mixin from "./mixin.ts";
|
|
10
|
-
import * as projectPredicate from "./project-predicate.ts";
|
|
11
10
|
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
11
|
+
import * as projectPredicate from "./project-predicate.ts";
|
|
12
12
|
|
|
13
13
|
export * from "./project-js.ts";
|
|
14
14
|
export * from "./project-py.ts";
|
package/src/release.ts
CHANGED
|
@@ -269,9 +269,9 @@ export class DBXToolsRelease extends Component {
|
|
|
269
269
|
*
|
|
270
270
|
* `directory` (e.g. `projen/`) is a WORKSPACE MEMBER whose `@dbx-tools/*` deps
|
|
271
271
|
* are `workspace:*`. `bun publish` resolves those to whatever version its
|
|
272
|
-
* SIBLINGS carry (via the lockfile), so before publishing we
|
|
273
|
-
* the package AND its in-scope siblings, then refresh the lockfile -
|
|
274
|
-
*
|
|
272
|
+
* SIBLINGS carry (via the lockfile), so before publishing we re-affirm the
|
|
273
|
+
* version on the package AND its in-scope siblings, then refresh the lockfile -
|
|
274
|
+
* otherwise a stale resolved version could reach the published engine. The
|
|
275
275
|
* `Install` step already ran `bun install` from the repo root (the member
|
|
276
276
|
* subdir walks up to it), so the workspace is linked. The manifests are
|
|
277
277
|
* projen-readonly, hence the `chmod`. A manual `workflow_dispatch` run has no
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the workspace version.
|
|
3
|
+
*
|
|
4
|
+
* The repo-root `VERSION` file holds one plain `x.y.z` string that every
|
|
5
|
+
* generated manifest copies at synth: the root and `projen/` package.json, every
|
|
6
|
+
* JS member, every Python `pyproject.toml`, the generated openapi packages, and
|
|
7
|
+
* the example apps. Synth only READS this file (defaulting to {@link
|
|
8
|
+
* DEFAULT_VERSION} when it is absent on a fresh tree); it never rewrites it, so an
|
|
9
|
+
* ordinary `bunx projen` cannot move a package version up or down.
|
|
10
|
+
*
|
|
11
|
+
* Only two callers change the number: `bump` (which increments it) and the
|
|
12
|
+
* one-time bootstrap of a workspace that has no `VERSION` yet. Both resolve the
|
|
13
|
+
* base from the remote git tags first ({@link resolveRemoteVersion}) so a release
|
|
14
|
+
* cut elsewhere is respected, and fall back to the local file (or {@link
|
|
15
|
+
* DEFAULT_VERSION}) when the remote is unreachable or has no tags. The remote is
|
|
16
|
+
* consulted ONLY on those two paths, never on every synth/compile/commit.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { exec } from "@dbx-tools/core";
|
|
21
|
+
|
|
22
|
+
/** Name of the repo-root file holding the workspace version. */
|
|
23
|
+
export const VERSION_FILE = "VERSION";
|
|
24
|
+
|
|
25
|
+
/** Version a fresh workspace starts at when no `VERSION` file and no remote tag exist. */
|
|
26
|
+
export const DEFAULT_VERSION = "0.0.1";
|
|
27
|
+
|
|
28
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
29
|
+
|
|
30
|
+
/** A parsed `[major, minor, patch]` tuple. */
|
|
31
|
+
export type Semver = [number, number, number];
|
|
32
|
+
|
|
33
|
+
/** Parse `x.y.z` (ignoring any leading `v`/prefix), or `undefined` when it does not match. */
|
|
34
|
+
export function parseSemver(raw: string): Semver | undefined {
|
|
35
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw.trim());
|
|
36
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ordering comparator: negative when `a < b`, positive when `a > b`, zero when equal. */
|
|
40
|
+
export function compareSemver(a: Semver, b: Semver): number {
|
|
41
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Absolute path to the `VERSION` file for a workspace root. */
|
|
45
|
+
export function versionPath(root: string): string {
|
|
46
|
+
return join(root, VERSION_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the workspace version from `<root>/VERSION`. Returns {@link DEFAULT_VERSION}
|
|
51
|
+
* when the file is absent (a fresh consumer tree). A file that EXISTS but does not
|
|
52
|
+
* hold a valid `x.y.z` fails loudly rather than being silently "fixed" to a
|
|
53
|
+
* different number during synth.
|
|
54
|
+
*/
|
|
55
|
+
export function readWorkspaceVersion(root: string): string {
|
|
56
|
+
const path = versionPath(root);
|
|
57
|
+
if (!existsSync(path)) return DEFAULT_VERSION;
|
|
58
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
59
|
+
if (!SEMVER.test(raw)) {
|
|
60
|
+
throw new Error(`${VERSION_FILE} must contain an x.y.z version, got ${JSON.stringify(raw)}`);
|
|
61
|
+
}
|
|
62
|
+
return raw;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Write the workspace version to `<root>/VERSION`. Only `bump` and bootstrap call this. */
|
|
66
|
+
export function writeWorkspaceVersion(root: string, version: string): void {
|
|
67
|
+
if (!SEMVER.test(version)) {
|
|
68
|
+
throw new Error(`workspace version must be x.y.z, got ${JSON.stringify(version)}`);
|
|
69
|
+
}
|
|
70
|
+
writeFileSync(versionPath(root), `${version}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Run git in `cwd`, capturing stdout and swallowing failure (offline, no repo). */
|
|
74
|
+
function gitCapture(cwd: string, args: string[]): string {
|
|
75
|
+
try {
|
|
76
|
+
const res = exec.spawnSync("git", args, {
|
|
77
|
+
cwd,
|
|
78
|
+
stdout: "capture",
|
|
79
|
+
stderr: "ignore",
|
|
80
|
+
stdin: "ignore",
|
|
81
|
+
check: false,
|
|
82
|
+
});
|
|
83
|
+
return res.stdout?.trim() ?? "";
|
|
84
|
+
} catch {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Highest tag matching `<prefix><semver>` in the local tag list, or `undefined`. */
|
|
90
|
+
export function latestTagVersion(cwd: string, prefix: string): Semver | undefined {
|
|
91
|
+
const out = gitCapture(cwd, [
|
|
92
|
+
"-c",
|
|
93
|
+
"versionsort.suffix=-",
|
|
94
|
+
"tag",
|
|
95
|
+
"--sort=-version:refname",
|
|
96
|
+
"--list",
|
|
97
|
+
`${prefix}*`,
|
|
98
|
+
]);
|
|
99
|
+
for (const tag of out.split("\n")) {
|
|
100
|
+
const v = parseSemver(tag.replace(prefix, ""));
|
|
101
|
+
if (v) return v;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Highest published version across every tag prefix, or `undefined` when the
|
|
108
|
+
* remote is unreachable or no matching tag exists. Fetches tags first (best
|
|
109
|
+
* effort) so a release made elsewhere is respected; a fetch failure just means
|
|
110
|
+
* the local tag list is used, and callers fall back to the `VERSION` file.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveRemoteVersion(
|
|
113
|
+
cwd: string,
|
|
114
|
+
prefixes: readonly string[],
|
|
115
|
+
{ fetch = true }: { fetch?: boolean } = {},
|
|
116
|
+
): string | undefined {
|
|
117
|
+
if (fetch) gitCapture(cwd, ["fetch", "--tags", "--quiet"]);
|
|
118
|
+
let best: Semver | undefined;
|
|
119
|
+
for (const prefix of prefixes) {
|
|
120
|
+
const v = latestTagVersion(cwd, prefix);
|
|
121
|
+
if (v && (!best || compareSemver(v, best) > 0)) best = v;
|
|
122
|
+
}
|
|
123
|
+
return best ? best.join(".") : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The base version a `bump` increments from: the highest remote tag if any exists
|
|
128
|
+
* (a local file that is ahead does NOT win), else the local `VERSION` file, else
|
|
129
|
+
* {@link DEFAULT_VERSION}.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveBaseVersion(
|
|
132
|
+
root: string,
|
|
133
|
+
prefixes: readonly string[],
|
|
134
|
+
options: { fetch?: boolean } = {},
|
|
135
|
+
): { version: string; source: "remote" | "local" } {
|
|
136
|
+
const remote = resolveRemoteVersion(root, prefixes, options);
|
|
137
|
+
if (remote) return { version: remote, source: "remote" };
|
|
138
|
+
return { version: readWorkspaceVersion(root), source: "local" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create the `VERSION` file when it does not yet exist, seeding it from the remote
|
|
143
|
+
* tags (or {@link DEFAULT_VERSION} when the remote is unreachable / has no tag). An
|
|
144
|
+
* existing file is left untouched - only `bump` moves an established version, so
|
|
145
|
+
* bootstrap never upgrades or downgrades one. Returns the current version.
|
|
146
|
+
*/
|
|
147
|
+
export function ensureWorkspaceVersion(
|
|
148
|
+
root: string,
|
|
149
|
+
{ prefixes = ["v"], fetch = true }: { prefixes?: readonly string[]; fetch?: boolean } = {},
|
|
150
|
+
): string {
|
|
151
|
+
if (!existsSync(versionPath(root))) {
|
|
152
|
+
writeWorkspaceVersion(root, resolveRemoteVersion(root, prefixes, { fetch }) ?? DEFAULT_VERSION);
|
|
153
|
+
}
|
|
154
|
+
return readWorkspaceVersion(root);
|
|
155
|
+
}
|
package/tasks/bump.ts
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/**
|
|
3
|
-
* `projen bump` -
|
|
4
|
-
*
|
|
5
|
-
* workflow.
|
|
3
|
+
* `projen bump` - compute the next release version, write it to the workspace
|
|
4
|
+
* `VERSION` file, synth so every manifest copies it, then (by default) commit,
|
|
5
|
+
* tag, and push. Pushing the tag is what triggers the release workflow.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* The base version is the HIGHEST published git tag across `<prefix>` and every
|
|
8
|
+
* `--sibling` prefix (fetched from the remote so a release cut elsewhere wins),
|
|
9
|
+
* falling back to the local `VERSION` file when the remote is unreachable or has
|
|
10
|
+
* no matching tag. A local file that is ahead does NOT override an existing
|
|
11
|
+
* remote tag. The base is then incremented by `--level` (patch | minor | major;
|
|
12
|
+
* default patch). The remote is consulted only here and on VERSION bootstrap,
|
|
13
|
+
* never on an ordinary synth.
|
|
13
14
|
*
|
|
14
15
|
* `--sibling <dir>:<tagPrefix>` (repeatable) releases an in-repo project that
|
|
15
16
|
* publishes on its OWN tag namespace (e.g. `projen/`, tagged `projen-v*`) at the
|
|
16
|
-
* SAME version as the root, in the same run: its
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* SAME version as the root, in the same run: its `<tagPrefix><version>` tag is
|
|
18
|
+
* cut and pushed (triggering its own workflow), and it is included in the
|
|
19
|
+
* local-registry publish. Taking the base version from every prefix at once is
|
|
20
|
+
* what keeps the two in lockstep: the engine sat at 0.1.24 while the packages
|
|
21
|
+
* reached 0.3.41 precisely because each namespace only ever looked at its own
|
|
22
|
+
* tags. Both draw the fallback from the one root `VERSION` file, so the engine
|
|
23
|
+
* and the packages share a single source of truth.
|
|
22
24
|
*
|
|
23
25
|
* Flags (all default ON; negate with the `--no-` form, per commander):
|
|
24
|
-
* --synth / --no-synth
|
|
25
|
-
* --version / --no-version write the bumped version into
|
|
26
|
+
* --synth / --no-synth synth after writing VERSION so manifests copy it
|
|
27
|
+
* --version / --no-version write the bumped version into `VERSION`
|
|
26
28
|
* --commit / --no-commit commit the release (staged with `git add -A`)
|
|
27
29
|
* --tag / --no-tag create the `<prefix><version>` git tag
|
|
28
30
|
* --push / --no-push push the CURRENT branch + tag to origin
|
|
@@ -34,7 +36,14 @@
|
|
|
34
36
|
* verdaccio) right after the tag push. `--local-pypi <value>` does the same for
|
|
35
37
|
* Python packages through a writable devpi index. Values for both:
|
|
36
38
|
* - `auto` (default): publish only when `npm config get registry` is a
|
|
37
|
-
* loopback host, or
|
|
39
|
+
* loopback host, or when ANY active Python index — the primary
|
|
40
|
+
* `index-url` OR any `extra-index-url`, across uv and pip — is a loopback
|
|
41
|
+
* devpi URL. Scanning the extras is what lets the corp proxy stay the
|
|
42
|
+
* primary index while a local devpi added as an extra is the detected
|
|
43
|
+
* publish target. The deploy endpoint for that index is taken from the
|
|
44
|
+
* GLOBAL uv config — an explicit `publish-url` on the matching `[[index]]`
|
|
45
|
+
* (or `UV_PUBLISH_URL`) — falling back to deriving it from the `+simple`
|
|
46
|
+
* URL shape when no such setting exists.
|
|
38
47
|
* - `false`: never publish locally.
|
|
39
48
|
* - a URL: always publish to that registry.
|
|
40
49
|
*/
|
|
@@ -44,7 +53,15 @@ import { fileURLToPath } from "node:url";
|
|
|
44
53
|
import { exec, project } from "@dbx-tools/core";
|
|
45
54
|
import { log, net } from "@dbx-tools/shared-core";
|
|
46
55
|
import { Command, Option } from "commander";
|
|
47
|
-
import {
|
|
56
|
+
import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
|
|
57
|
+
import {
|
|
58
|
+
type Semver,
|
|
59
|
+
compareSemver,
|
|
60
|
+
latestTagVersion,
|
|
61
|
+
parseSemver,
|
|
62
|
+
resolveBaseVersion,
|
|
63
|
+
writeWorkspaceVersion,
|
|
64
|
+
} from "../src/workspace-version.ts";
|
|
48
65
|
|
|
49
66
|
const logger = log.logger("projen:bump");
|
|
50
67
|
const LEVELS = ["patch", "minor", "major"] as const;
|
|
@@ -70,17 +87,7 @@ function parseSibling(value: string, previous: Sibling[]): Sibling[] {
|
|
|
70
87
|
return [...previous, { dir: value.slice(0, at), prefix: value.slice(at + 1) }];
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
function parseSemver(raw: string): [number, number, number] | undefined {
|
|
75
|
-
const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw.trim());
|
|
76
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function compareSemver(a: [number, number, number], b: [number, number, number]): number {
|
|
80
|
-
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function increment(v: [number, number, number], level: Level): [number, number, number] {
|
|
90
|
+
function increment(v: Semver, level: Level): Semver {
|
|
84
91
|
if (level === "major") return [v[0] + 1, 0, 0];
|
|
85
92
|
if (level === "minor") return [v[0], v[1] + 1, 0];
|
|
86
93
|
return [v[0], v[1], v[2] + 1];
|
|
@@ -97,29 +104,6 @@ function git(args: string[], capture = false): string {
|
|
|
97
104
|
return res.stdout?.trim() ?? "";
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
/** Highest tag matching `<prefix><semver>`, or undefined. Call {@link fetchTags} first. */
|
|
101
|
-
function latestTagVersion(prefix: string): [number, number, number] | undefined {
|
|
102
|
-
const out = git(
|
|
103
|
-
["-c", "versionsort.suffix=-", "tag", "--sort=-version:refname", "--list", `${prefix}*`],
|
|
104
|
-
true,
|
|
105
|
-
);
|
|
106
|
-
for (const tag of out.split("\n")) {
|
|
107
|
-
const v = parseSemver(tag.replace(prefix, ""));
|
|
108
|
-
if (v) return v;
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Pull remote tags once, so a release made elsewhere is respected. */
|
|
114
|
-
function fetchTags(): void {
|
|
115
|
-
git(["fetch", "--tags", "--quiet"], true);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function readPackageVersion(pkgPath: string): [number, number, number] {
|
|
119
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: string };
|
|
120
|
-
return parseSemver(pkg.version ?? "") ?? [0, 0, 0];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
107
|
/**
|
|
124
108
|
* Write ONLY the `version` field into a manifest projen owns (read-only, so
|
|
125
109
|
* bracketed by a chmod that restores the mode). Used for the ROOT and the
|
|
@@ -190,7 +174,7 @@ program
|
|
|
190
174
|
)
|
|
191
175
|
.option(
|
|
192
176
|
"--local-pypi <value>",
|
|
193
|
-
"publish Python packages locally: 'auto' (
|
|
177
|
+
"publish Python packages locally: 'auto' (any active index-url or extra-index-url that is a loopback devpi +simple), 'false', or a devpi URL",
|
|
194
178
|
"auto",
|
|
195
179
|
)
|
|
196
180
|
.option("--python-root <path>", "Python workspace package root", "packages/py")
|
|
@@ -217,50 +201,55 @@ program
|
|
|
217
201
|
if (!existsSync(s.pkgPath)) throw new Error(`--sibling ${s.dir}: no package.json there`);
|
|
218
202
|
}
|
|
219
203
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
exec.spawnSync("bun", [".projenrc.ts"], {
|
|
225
|
-
cwd: process.cwd(),
|
|
226
|
-
stdout: "inherit",
|
|
227
|
-
stderr: "inherit",
|
|
228
|
-
stdin: "ignore",
|
|
229
|
-
check: true,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
204
|
+
// The `VERSION` file is the workspace source of truth and lives at the repo
|
|
205
|
+
// root, even when this task runs from a subdirectory (`cd projen && bun run
|
|
206
|
+
// bump`), so the engine and packages always share one number.
|
|
207
|
+
const root = project.root() ?? process.cwd();
|
|
232
208
|
|
|
233
|
-
// Base =
|
|
234
|
-
//
|
|
235
|
-
|
|
209
|
+
// Base = the highest published tag across EVERY namespace being released
|
|
210
|
+
// (fetched from the remote so a release cut elsewhere wins), falling back to
|
|
211
|
+
// the local `VERSION` file when the remote is unreachable or has no tag. A
|
|
212
|
+
// local file that happens to be ahead does NOT override an existing remote.
|
|
236
213
|
const prefixes = [opts.prefix, ...siblings.map((s) => s.prefix)];
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
.filter((t): t is { prefix: string; version: [number, number, number] } => !!t.version);
|
|
240
|
-
const base = tagged.reduce(
|
|
241
|
-
(highest, t) => (compareSemver(t.version, highest) > 0 ? t.version : highest),
|
|
242
|
-
readPackageVersion(pkgPath),
|
|
243
|
-
);
|
|
214
|
+
const baseInfo = resolveBaseVersion(root, prefixes);
|
|
215
|
+
const base = parseSemver(baseInfo.version) ?? [0, 0, 1];
|
|
244
216
|
const next = increment(base, opts.level);
|
|
245
217
|
const version = next.join(".");
|
|
246
218
|
const tags = prefixes.map((prefix) => `${prefix}${version}`);
|
|
247
219
|
logger.info(
|
|
248
220
|
`bump ${base.join(".")} -> ${version} (${opts.level}); tags ${tags.join(", ")}` +
|
|
249
|
-
`${
|
|
221
|
+
`${baseInfo.source === "remote" ? "" : " [no remote tag; used local VERSION]"}`,
|
|
250
222
|
);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
223
|
+
// Note any tag namespace that trailed the base so a lockstep catch-up is visible.
|
|
224
|
+
for (const prefix of prefixes) {
|
|
225
|
+
const tagged = latestTagVersion(root, prefix);
|
|
226
|
+
if (tagged && compareSemver(tagged, base) < 0) {
|
|
227
|
+
logger.info(`${prefix}* was behind at ${tagged.join(".")}, catching it up`);
|
|
254
228
|
}
|
|
255
229
|
}
|
|
256
230
|
|
|
257
231
|
const push = opts.push && opts.publish;
|
|
258
232
|
|
|
233
|
+
// Write the source of truth first, then synth so every generated manifest
|
|
234
|
+
// COPIES it - synth never invents, resets, or drifts a version on its own.
|
|
259
235
|
if (opts.version) {
|
|
236
|
+
writeWorkspaceVersion(root, version);
|
|
237
|
+
// Keep the writable root/sibling manifests coherent even when synth is
|
|
238
|
+
// skipped (`--no-synth`); synth would otherwise set the same value.
|
|
260
239
|
writeManifestVersion(pkgPath, version);
|
|
261
240
|
for (const s of siblings) writeManifestVersion(s.pkgPath, version);
|
|
262
|
-
|
|
263
|
-
|
|
241
|
+
logger.info(`wrote version ${version} to ${root}/VERSION`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (opts.synth) {
|
|
245
|
+
logger.info("synthesizing (projen)");
|
|
246
|
+
exec.spawnSync("bun", [".projenrc.ts"], {
|
|
247
|
+
cwd: process.cwd(),
|
|
248
|
+
stdout: "inherit",
|
|
249
|
+
stderr: "inherit",
|
|
250
|
+
stdin: "ignore",
|
|
251
|
+
check: true,
|
|
252
|
+
});
|
|
264
253
|
}
|
|
265
254
|
|
|
266
255
|
if (opts.commit) {
|
|
@@ -297,16 +286,15 @@ program
|
|
|
297
286
|
}
|
|
298
287
|
if (publishToLocalRegistry) {
|
|
299
288
|
logger.info(`publishing ${version} to local registry ${localRegistry}`);
|
|
300
|
-
// Mirror the CI `release` workflow via the shared publish task: it
|
|
301
|
-
// the release version on every workspace member (`bun pm pkg
|
|
302
|
-
//
|
|
303
|
-
// `bun publish`es each non-private one - and
|
|
304
|
-
// `workspace:`/`catalog:` protocols in the packed
|
|
305
|
-
// to
|
|
306
|
-
// at exit, so this leaves the worktree matching the
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// `NPM_CONFIG_PROVENANCE` is unset.
|
|
289
|
+
// Mirror the CI `release` workflow via the shared publish task: it
|
|
290
|
+
// re-affirms the release version on every workspace member (`bun pm pkg
|
|
291
|
+
// set`; the manifests already carry the shared `VERSION`, projen-owned, so
|
|
292
|
+
// it unlocks each briefly), then `bun publish`es each non-private one - and
|
|
293
|
+
// bun natively strips the `workspace:`/`catalog:` protocols in the packed
|
|
294
|
+
// tarball, resolving each to that version. `publish.ts` restores every
|
|
295
|
+
// manifest it touched at exit, so this leaves the worktree matching the
|
|
296
|
+
// release commit that was just pushed. Provenance is off for a local
|
|
297
|
+
// registry (no OIDC), so `NPM_CONFIG_PROVENANCE` is unset.
|
|
310
298
|
// `publish.ts` is this task's SIBLING in the engine's `tasks/` dir; resolve
|
|
311
299
|
// it off `import.meta.url` (works whether the engine is source-linked in-repo
|
|
312
300
|
// or installed under node_modules) rather than a repo-relative `tasks/...`
|
|
@@ -322,16 +310,21 @@ program
|
|
|
322
310
|
logger.success(`published ${version} to ${localRegistry}`);
|
|
323
311
|
}
|
|
324
312
|
|
|
325
|
-
|
|
326
|
-
|
|
313
|
+
// Scan EVERY active index (primary index-url + every extra-index-url,
|
|
314
|
+
// across uv and pip): auto-mode publishes to the first that is a loopback
|
|
315
|
+
// devpi +simple, so the corp proxy stays primary and a local devpi added
|
|
316
|
+
// as an extra index is the detected publish target.
|
|
317
|
+
const activeIndexes = activePythonIndexes();
|
|
318
|
+
const localPypi = resolveLocalPypi(opts.localPypi, activeIndexes);
|
|
327
319
|
const pythonRoot = resolve(opts.pythonRoot);
|
|
328
320
|
if (
|
|
329
321
|
opts.localPypi.toLowerCase() === "auto" &&
|
|
330
|
-
|
|
331
|
-
net.isLoopbackHost(new URL(activeIndex)) &&
|
|
322
|
+
activeIndexes.some((index) => net.isLoopbackHost(index)) &&
|
|
332
323
|
!localPypi
|
|
333
324
|
) {
|
|
334
|
-
logger.info(
|
|
325
|
+
logger.info(
|
|
326
|
+
`skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
|
|
327
|
+
);
|
|
335
328
|
}
|
|
336
329
|
if (opts.version === false && localPypi) {
|
|
337
330
|
logger.info("skipped local Python publish (--no-version left packages unstamped)");
|
|
@@ -360,16 +353,6 @@ program
|
|
|
360
353
|
);
|
|
361
354
|
logger.success(`published Python ${version} to ${localPypi.publishUrl}`);
|
|
362
355
|
}
|
|
363
|
-
|
|
364
|
-
// Publishing can run package lifecycle hooks, including a standalone
|
|
365
|
-
// project's own projen synth, which rewrites its generated manifest back
|
|
366
|
-
// to 0.0.0. Re-assert the release version last so root and every sibling
|
|
367
|
-
// manifest finish the bump in lockstep.
|
|
368
|
-
if (opts.version) {
|
|
369
|
-
writeManifestVersion(pkgPath, version);
|
|
370
|
-
for (const s of siblings) writeManifestVersion(s.pkgPath, version);
|
|
371
|
-
logger.info(`synchronized release manifests at ${version}`);
|
|
372
|
-
}
|
|
373
356
|
},
|
|
374
357
|
);
|
|
375
358
|
|
package/tasks/publish-python.ts
CHANGED
|
@@ -22,11 +22,24 @@ interface PythonProjectFile {
|
|
|
22
22
|
readonly source: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface StampPythonProjectsOptions {
|
|
26
|
+
readonly rewriteDependencies?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RestorePythonProjects {
|
|
30
|
+
(): void;
|
|
31
|
+
readonly paths: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
function escapeRegExp(value: string): string {
|
|
26
35
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
export function stampPythonProjects(
|
|
38
|
+
export function stampPythonProjects(
|
|
39
|
+
root: string,
|
|
40
|
+
version: string,
|
|
41
|
+
options: StampPythonProjectsOptions = {},
|
|
42
|
+
): RestorePythonProjects {
|
|
30
43
|
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
31
44
|
.filter((entry) => entry.isDirectory())
|
|
32
45
|
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
@@ -48,21 +61,25 @@ export function stampPythonProjects(root: string, version: string): () => void {
|
|
|
48
61
|
|
|
49
62
|
try {
|
|
50
63
|
for (const project of projects) {
|
|
51
|
-
|
|
52
|
-
if (
|
|
64
|
+
const versionPattern = /^version = "[^"]+"$/m;
|
|
65
|
+
if (!versionPattern.test(project.source)) {
|
|
53
66
|
throw new Error(`Expected one project version in ${project.path}`);
|
|
54
67
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
let stamped = project.source.replace(versionPattern, `version = "${version}"`);
|
|
69
|
+
if (options.rewriteDependencies ?? true) {
|
|
70
|
+
for (const sibling of projects) {
|
|
71
|
+
stamped = stamped.replace(
|
|
72
|
+
new RegExp(
|
|
73
|
+
`${escapeRegExp(sibling.name)} @ git\\+[^" ]+#subdirectory=[^" ]+/${escapeRegExp(sibling.directory)}`,
|
|
74
|
+
"g",
|
|
75
|
+
),
|
|
76
|
+
`${sibling.name}==${version}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
chmodSync(project.path, project.mode | 0o200);
|
|
65
81
|
writeFileSync(project.path, stamped);
|
|
82
|
+
chmodSync(project.path, project.mode);
|
|
66
83
|
}
|
|
67
84
|
} catch (error) {
|
|
68
85
|
for (const project of projects) {
|
|
@@ -73,13 +90,17 @@ export function stampPythonProjects(root: string, version: string): () => void {
|
|
|
73
90
|
throw error;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
|
|
93
|
+
const restore = () => {
|
|
77
94
|
for (const project of projects) {
|
|
78
95
|
chmodSync(project.path, project.mode | 0o200);
|
|
79
96
|
writeFileSync(project.path, project.source);
|
|
80
97
|
chmodSync(project.path, project.mode);
|
|
81
98
|
}
|
|
82
99
|
};
|
|
100
|
+
Object.defineProperty(restore, "paths", {
|
|
101
|
+
value: projects.map((project) => project.path),
|
|
102
|
+
});
|
|
103
|
+
return restore as RestorePythonProjects;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
export function publishPythonProjects(options: {
|
|
@@ -91,7 +112,7 @@ export function publishPythonProjects(options: {
|
|
|
91
112
|
}): void {
|
|
92
113
|
const root = resolve(options.root);
|
|
93
114
|
const output = mkdtempSync(join(tmpdir(), "dbx-tools-python-publish-"));
|
|
94
|
-
const
|
|
115
|
+
const stamp = stampPythonProjects(root, options.version);
|
|
95
116
|
try {
|
|
96
117
|
exec.spawnSync("uv", ["build", "--all-packages", "--out-dir", output], {
|
|
97
118
|
cwd: process.cwd(),
|
|
@@ -123,7 +144,7 @@ export function publishPythonProjects(options: {
|
|
|
123
144
|
},
|
|
124
145
|
);
|
|
125
146
|
} finally {
|
|
126
|
-
|
|
147
|
+
stamp();
|
|
127
148
|
rmSync(output, { recursive: true, force: true });
|
|
128
149
|
}
|
|
129
150
|
}
|
package/tasks/publish.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* packed manifest shows `"@scope/x": "<version>"` and the real catalog range,
|
|
18
18
|
* while the on-disk manifest keeps the protocols.) Setting each member's
|
|
19
19
|
* version first is the only prerequisite, so a sibling resolves the release
|
|
20
|
-
* version
|
|
20
|
+
* version; the disk manifest already carries the workspace `VERSION`, and
|
|
21
|
+
* this makes doubly sure it matches the value being published;
|
|
21
22
|
* - **`publishConfig` substitution** (compiled `lib/` entry points) is done
|
|
22
23
|
* HERE, by {@link applyPublishConfig}, NOT by bun: unlike pnpm/npm, `bun
|
|
23
24
|
* publish`/`bun pm pack` do NOT fold `publishConfig`'s `main`/`types`/`bin`/
|
|
@@ -42,10 +43,13 @@
|
|
|
42
43
|
* since its whole job is to stamp the workspace for a `bun publish` that runs
|
|
43
44
|
* afterwards from another directory.
|
|
44
45
|
*
|
|
45
|
-
* The disk manifests
|
|
46
|
-
* this unlocks each only long enough to
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* The disk manifests carry the workspace `VERSION` (projen owns them, read-only);
|
|
47
|
+
* this unlocks each only long enough to fold in the `publishConfig` entry points
|
|
48
|
+
* (and re-affirm the version) + publish, then RESTORES every one it touched
|
|
49
|
+
* byte-for-byte (and re-locks the mode) on the way out - see
|
|
50
|
+
* {@link restoreManifests}. Restore returns each manifest to its committed
|
|
51
|
+
* content, which already equals the release version, so the worktree is never
|
|
52
|
+
* left regressed.
|
|
49
53
|
*/
|
|
50
54
|
import { chmodSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
51
55
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -215,8 +219,9 @@ const members = workspaceMembers(root)
|
|
|
215
219
|
.filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
|
|
216
220
|
|
|
217
221
|
// Set the version on EVERY member first (native `bun pm pkg set`), so a sibling
|
|
218
|
-
// published later has its `workspace:*` dep resolved to the release version
|
|
219
|
-
//
|
|
222
|
+
// published later has its `workspace:*` dep resolved to the release version by
|
|
223
|
+
// `bun publish`'s own protocol rewriting. The disk manifests already carry the
|
|
224
|
+
// workspace `VERSION`; this re-affirms it against the value being published.
|
|
220
225
|
logger.info(`setting ${version} across ${members.length} members`);
|
|
221
226
|
for (const dir of members) {
|
|
222
227
|
unlockManifest(join(dir, "package.json"));
|
|
@@ -227,7 +232,8 @@ for (const dir of members) {
|
|
|
227
232
|
// just set. `bun publish`/`pm pack` reads the workspace version from the LOCKFILE,
|
|
228
233
|
// not the live manifest, and a plain `bun install` (even `--force`) does NOT
|
|
229
234
|
// re-resolve it after only a version-field change - deleting the lockfile first
|
|
230
|
-
// does. Without this
|
|
235
|
+
// does. Without this a `workspace:*` dep could publish against a stale resolved
|
|
236
|
+
// version instead of the one just set.
|
|
231
237
|
const lockfile = join(root, "bun.lock");
|
|
232
238
|
if (existsSync(lockfile)) rmSync(lockfile);
|
|
233
239
|
logger.info("refreshing lockfile so workspace deps resolve to the release version");
|
package/tasks/python-registry.ts
CHANGED
|
@@ -22,7 +22,72 @@ export function parseUvDefaultIndex(source: string): string | undefined {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Read EVERY index URL from uv's TOML config — the default (primary) index
|
|
27
|
+
* first, then any extra `[[index]]` entries in file order. uv's `[[index]]`
|
|
28
|
+
* table with `default = true` is the primary index; every other `[[index]]`
|
|
29
|
+
* is an extra index also consulted at resolve time, so a local devpi added as
|
|
30
|
+
* a non-default block is discoverable here.
|
|
31
|
+
*/
|
|
32
|
+
export function parseUvIndexes(source: string): string[] {
|
|
33
|
+
const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
|
|
34
|
+
let primary: string | undefined;
|
|
35
|
+
const extras: string[] = [];
|
|
36
|
+
for (const block of blocks) {
|
|
37
|
+
if (!/^\[\[index\]\]\s*$/m.test(block)) continue;
|
|
38
|
+
const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
|
|
39
|
+
if (!url) continue;
|
|
40
|
+
if (/^\s*default\s*=\s*true\s*$/m.test(block)) primary = url;
|
|
41
|
+
else extras.push(url);
|
|
42
|
+
}
|
|
43
|
+
return primary ? [primary, ...extras] : extras;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read every `[[index]]` block's `url` -> `publish-url` mapping from uv's TOML
|
|
48
|
+
* config. uv lets an index declare an explicit upload endpoint via `publish-url`
|
|
49
|
+
* (the writable index URL, distinct from the `+simple` read URL); this is the
|
|
50
|
+
* global setting that names the local deploy target, so auto-detection prefers
|
|
51
|
+
* it over deriving the publish URL from the `+simple` URL shape.
|
|
52
|
+
*/
|
|
53
|
+
export function parseUvPublishUrls(source: string): Map<string, string> {
|
|
54
|
+
const map = new Map<string, string>();
|
|
55
|
+
const blocks = source.split(/(?=^\[\[index\]\]\s*$)/m);
|
|
56
|
+
for (const block of blocks) {
|
|
57
|
+
if (!/^\[\[index\]\]\s*$/m.test(block)) continue;
|
|
58
|
+
const url = /^\s*url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
|
|
59
|
+
const publishUrl = /^\s*publish-url\s*=\s*["']([^"']+)["']\s*$/m.exec(block)?.[1];
|
|
60
|
+
if (url && publishUrl) map.set(url, publishUrl);
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The explicit `publish-url` configured (in the global uv config or
|
|
67
|
+
* `UV_PUBLISH_URL`) for a given `+simple` index URL, or `undefined` when none is
|
|
68
|
+
* set. Lets auto-detection use the deploy endpoint the user declared globally
|
|
69
|
+
* rather than inferring it from the index URL.
|
|
70
|
+
*/
|
|
71
|
+
export function configuredPublishUrl(
|
|
72
|
+
indexUrl: string,
|
|
73
|
+
uvConfigPath: string = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml"),
|
|
74
|
+
): string | undefined {
|
|
75
|
+
const fromEnv = process.env.UV_PUBLISH_URL?.trim();
|
|
76
|
+
if (fromEnv) return fromEnv;
|
|
77
|
+
if (!existsSync(uvConfigPath)) return undefined;
|
|
78
|
+
return parseUvPublishUrls(readFileSync(uvConfigPath, "utf8")).get(indexUrl);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert a devpi Simple API URL into a local publish target.
|
|
83
|
+
*
|
|
84
|
+
* The publish URL is taken from the GLOBAL uv config first — an explicit
|
|
85
|
+
* `publish-url` on the matching `[[index]]` (or `UV_PUBLISH_URL`), i.e. the
|
|
86
|
+
* deploy endpoint the user declared — and only DERIVED from the `+simple` URL
|
|
87
|
+
* shape when no such setting exists. Returns `undefined` for a non-loopback
|
|
88
|
+
* host or a URL that is neither a `+simple` index nor has a configured
|
|
89
|
+
* `publish-url`.
|
|
90
|
+
*/
|
|
26
91
|
export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
27
92
|
let url: URL;
|
|
28
93
|
try {
|
|
@@ -32,6 +97,16 @@ export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
|
32
97
|
}
|
|
33
98
|
if (!net.isLoopbackHost(url)) return undefined;
|
|
34
99
|
|
|
100
|
+
// A globally-configured publish-url wins: it names the deploy target directly,
|
|
101
|
+
// so we honor it even if the index URL isn't the conventional `+simple` shape.
|
|
102
|
+
const configured = configuredPublishUrl(url.href);
|
|
103
|
+
if (configured) {
|
|
104
|
+
return {
|
|
105
|
+
indexUrl: url.href,
|
|
106
|
+
publishUrl: configured.endsWith("/") ? configured : `${configured}/`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
35
110
|
const path = url.pathname.replace(/\/+$/, "");
|
|
36
111
|
if (!path.endsWith("/+simple")) return undefined;
|
|
37
112
|
url.pathname = `${path.slice(0, -"/+simple".length)}/`;
|
|
@@ -43,38 +118,94 @@ export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
|
43
118
|
};
|
|
44
119
|
}
|
|
45
120
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
}
|
|
121
|
+
/** Split a whitespace-separated index list (the pip/uv env-var form). */
|
|
122
|
+
function splitIndexList(value: string | undefined): string[] {
|
|
123
|
+
const trimmed = value?.trim();
|
|
124
|
+
return trimmed ? trimmed.split(/\s+/) : [];
|
|
125
|
+
}
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
/** Read a `pip config get <key>`, treating pip's literal "undefined" as unset. */
|
|
128
|
+
function pipConfig(key: string): string | undefined {
|
|
129
|
+
const res = exec.spawnSync("python", ["-m", "pip", "config", "get", key], {
|
|
60
130
|
cwd: process.cwd(),
|
|
61
131
|
stdout: "capture",
|
|
62
132
|
stderr: "ignore",
|
|
63
133
|
stdin: "ignore",
|
|
64
134
|
check: false,
|
|
65
135
|
});
|
|
66
|
-
|
|
136
|
+
const out = res.stdout?.trim();
|
|
137
|
+
return out && out !== "undefined" ? out : undefined;
|
|
67
138
|
}
|
|
68
139
|
|
|
69
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* Every Python package index in effect — the primary index FIRST, then extra
|
|
142
|
+
* indexes — deduplicated, across uv (preferred, because Python builds use uv)
|
|
143
|
+
* and pip. Both the primary `index-url` and every `extra-index-url` are read so
|
|
144
|
+
* a local devpi configured as an *extra* index (leaving the corp proxy as the
|
|
145
|
+
* primary) is still detected.
|
|
146
|
+
*/
|
|
147
|
+
export function activePythonIndexes(): string[] {
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
const add = (value: string | undefined): void => {
|
|
151
|
+
const url = value?.trim();
|
|
152
|
+
if (url && !seen.has(url)) {
|
|
153
|
+
seen.add(url);
|
|
154
|
+
out.push(url);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// uv: env vars first (primary, then extras), then uv.toml's index blocks.
|
|
159
|
+
add(process.env.UV_DEFAULT_INDEX);
|
|
160
|
+
add(process.env.UV_INDEX_URL);
|
|
161
|
+
for (const url of splitIndexList(process.env.UV_INDEX)) add(url);
|
|
162
|
+
for (const url of splitIndexList(process.env.UV_EXTRA_INDEX_URL)) add(url);
|
|
163
|
+
const uvConfig = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml");
|
|
164
|
+
if (existsSync(uvConfig)) {
|
|
165
|
+
for (const url of parseUvIndexes(readFileSync(uvConfig, "utf8"))) add(url);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// pip: env vars (primary + extras), then `pip config` (index-url + extra-index-url).
|
|
169
|
+
add(process.env.PIP_INDEX_URL);
|
|
170
|
+
for (const url of splitIndexList(process.env.PIP_EXTRA_INDEX_URL)) add(url);
|
|
171
|
+
add(pipConfig("global.index-url"));
|
|
172
|
+
for (const url of splitIndexList(pipConfig("global.extra-index-url"))) add(url);
|
|
173
|
+
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The primary (first) active Python index, or `undefined` when none is set. */
|
|
178
|
+
export function activePythonIndex(): string | undefined {
|
|
179
|
+
return activePythonIndexes()[0];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve `auto`, `false`, or an explicit devpi index/publish URL.
|
|
184
|
+
*
|
|
185
|
+
* - `false` (or empty): skip local publishing.
|
|
186
|
+
* - a URL: publish there (derive the writable index from a `+simple` URL, else
|
|
187
|
+
* treat the value itself as the writable index).
|
|
188
|
+
* - `auto`: scan every active index — primary `index-url` AND every
|
|
189
|
+
* `extra-index-url`, across uv and pip — and publish to the FIRST that is a
|
|
190
|
+
* loopback devpi `+simple` index. This is what lets the corp proxy stay the
|
|
191
|
+
* primary index while a local devpi added as an extra is the publish target.
|
|
192
|
+
*
|
|
193
|
+
* `indexes` accepts an array (the normal case) or a single string (kept for the
|
|
194
|
+
* existing single-index callers/tests); it defaults to {@link activePythonIndexes}.
|
|
195
|
+
*/
|
|
70
196
|
export function resolveLocalPypi(
|
|
71
197
|
value: string,
|
|
72
|
-
|
|
198
|
+
indexes: readonly string[] | string | undefined = activePythonIndexes(),
|
|
73
199
|
): LocalPythonRegistry | undefined {
|
|
74
200
|
const trimmed = value.trim();
|
|
75
201
|
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
76
202
|
if (trimmed.toLowerCase() === "auto") {
|
|
77
|
-
|
|
203
|
+
const list = typeof indexes === "string" ? [indexes] : (indexes ?? []);
|
|
204
|
+
for (const index of list) {
|
|
205
|
+
const registry = devpiRegistry(index);
|
|
206
|
+
if (registry) return registry;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
78
209
|
}
|
|
79
210
|
|
|
80
211
|
const derived = devpiRegistry(trimmed);
|