@dbx-tools/projen 0.6.195 → 0.6.196
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 +43 -31
- package/index.ts +2 -2
- package/package.json +4 -4
- package/src/pnpm-workspace.ts +4 -2
- package/src/project-js.ts +43 -12
- package/src/project-rs.ts +17 -6
- package/src/release.ts +35 -4
- package/src/workspace-version.ts +57 -7
- package/tasks/bump.ts +19 -439
- package/tasks/clean.ts +1 -1
- package/tasks/local-publish.ts +166 -0
- package/tasks/publish-uniffi-local.ts +30 -8
- package/tasks/publish.ts +22 -59
- package/tasks/release-pr.ts +259 -0
- package/tasks/version-check.ts +85 -0
package/tasks/bump.ts
CHANGED
|
@@ -1,70 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* annotated version tag before publishing.
|
|
7
|
-
*
|
|
8
|
-
* The base version is the HIGHEST published git tag across `<prefix>` and every
|
|
9
|
-
* `--sibling` prefix (fetched from the remote so a release cut elsewhere wins),
|
|
10
|
-
* falling back to the local `VERSION` file when the remote is unreachable or has
|
|
11
|
-
* no matching tag. A local file that is ahead does NOT override an existing
|
|
12
|
-
* remote tag. The base is then incremented by `--level` (patch | minor | major;
|
|
13
|
-
* default patch). The remote is consulted only here and on VERSION bootstrap,
|
|
14
|
-
* never on an ordinary synth.
|
|
15
|
-
*
|
|
16
|
-
* `--sibling <dir>:<tagPrefix>` (repeatable) releases an in-repo project that
|
|
17
|
-
* publishes on its OWN tag namespace (e.g. `projen/`, tagged `projen-v*`) at the
|
|
18
|
-
* SAME version as the root, in the same run: its `<tagPrefix><version>` tag is
|
|
19
|
-
* cut and pushed (triggering its own workflow), and it is included in the
|
|
20
|
-
* local-registry publish. Taking the base version from every prefix at once is
|
|
21
|
-
* what keeps the two in lockstep: the engine sat at 0.1.24 while the packages
|
|
22
|
-
* reached 0.3.41 precisely because each namespace only ever looked at its own
|
|
23
|
-
* tags. Both draw the fallback from the one root `VERSION` file, so the engine
|
|
24
|
-
* and the packages share a single source of truth.
|
|
25
|
-
*
|
|
26
|
-
* Flags default ON unless noted; negate with the `--no-` form, per commander:
|
|
27
|
-
* --synth / --no-synth synth after writing VERSION so manifests copy it
|
|
28
|
-
* --version / --no-version write the bumped version into `VERSION`
|
|
29
|
-
* --commit / --no-commit commit the release (staged with `git add -A`)
|
|
30
|
-
* --tag create the `<prefix><version>` git tag locally (default OFF)
|
|
31
|
-
* --push / --no-push push the CURRENT branch to origin
|
|
32
|
-
*
|
|
33
|
-
* `--publish` / `--no-publish` is an alias for `--push`. A push publishes only
|
|
34
|
-
* when the current branch is the configured release branch. The tag prefix
|
|
35
|
-
* comes from `--prefix` (default `v`).
|
|
36
|
-
*
|
|
37
|
-
* `--local-registry <value>` publishes npm packages to a LOCAL registry (e.g.
|
|
38
|
-
* verdaccio) right after the branch push. `--local-pypi <value>` does the same for
|
|
39
|
-
* Python packages through a writable devpi index. Values for both:
|
|
40
|
-
* - `auto` (default): publish only when `npm config get registry` is a
|
|
41
|
-
* loopback host, or when ANY active Python index - the primary
|
|
42
|
-
* `index-url` OR any `extra-index-url`, across uv and pip - is a loopback
|
|
43
|
-
* devpi URL. Scanning the extras is what lets the corp proxy stay the
|
|
44
|
-
* primary index while a local devpi added as an extra is the detected
|
|
45
|
-
* publish target. The deploy endpoint for that index is taken from the
|
|
46
|
-
* GLOBAL uv config - an explicit `publish-url` on the matching `[[index]]`
|
|
47
|
-
* (or `UV_PUBLISH_URL`) - falling back to deriving it from the `+simple`
|
|
48
|
-
* URL shape when no such setting exists.
|
|
49
|
-
* - `false`: never publish locally.
|
|
50
|
-
* - a URL: always publish to that registry.
|
|
3
|
+
* Compute the next workspace version, write VERSION, and synthesize every
|
|
4
|
+
* generated version surface. This task has no git, registry, or publication
|
|
5
|
+
* side effects; release preparation owns those operations.
|
|
51
6
|
*/
|
|
52
|
-
import { chmodSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
53
|
-
import { homedir } from "node:os";
|
|
54
|
-
import { join, resolve } from "node:path";
|
|
55
|
-
import { fileURLToPath } from "node:url";
|
|
56
7
|
import { exec, project } from "@dbx-tools/core";
|
|
57
|
-
import { log
|
|
8
|
+
import { log } from "@dbx-tools/shared-core";
|
|
58
9
|
import { Command, Option } from "commander";
|
|
59
|
-
import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
|
|
60
10
|
import { releasePlatformFilter } from "../src/_release-platform.ts";
|
|
61
|
-
import { readDbxToolsConfig, repoRoot } from "../src/packages.ts";
|
|
62
11
|
import {
|
|
63
|
-
type
|
|
64
|
-
|
|
65
|
-
latestTagVersion,
|
|
66
|
-
parseSemver,
|
|
67
|
-
resolveBaseVersion,
|
|
12
|
+
type VersionLevel,
|
|
13
|
+
resolveNextVersion,
|
|
68
14
|
writeWorkspaceVersion,
|
|
69
15
|
} from "../src/workspace-version.ts";
|
|
70
16
|
|
|
@@ -72,129 +18,20 @@ const logger = log.logger("projen:bump");
|
|
|
72
18
|
const LEVELS = ["patch", "minor", "major"] as const;
|
|
73
19
|
const RELEASE_OSES = ["darwin", "linux", "win32"] as const;
|
|
74
20
|
const RELEASE_ARCHES = ["arm64", "x64"] as const;
|
|
75
|
-
type Level = (typeof LEVELS)[number];
|
|
76
21
|
type ReleaseOs = (typeof RELEASE_OSES)[number];
|
|
77
22
|
type ReleaseArch = (typeof RELEASE_ARCHES)[number];
|
|
78
23
|
|
|
79
|
-
/** A standalone in-repo project released alongside the root, on its own tag prefix. */
|
|
80
|
-
interface Sibling {
|
|
81
|
-
/** Repo-relative directory, e.g. `projen`. */
|
|
82
|
-
readonly dir: string;
|
|
83
|
-
/** Git tag prefix, disjoint from the root's, e.g. `projen-v`. */
|
|
84
|
-
readonly prefix: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Commander collector for the repeatable `--sibling <dir>:<tagPrefix>`. Split on
|
|
89
|
-
* the LAST colon so a directory containing one still parses.
|
|
90
|
-
*/
|
|
91
|
-
function parseSibling(value: string, previous: Sibling[]): Sibling[] {
|
|
92
|
-
const at = value.lastIndexOf(":");
|
|
93
|
-
if (at <= 0 || at === value.length - 1) {
|
|
94
|
-
throw new Error(`--sibling expects <dir>:<tagPrefix>, got "${value}"`);
|
|
95
|
-
}
|
|
96
|
-
return [...previous, { dir: value.slice(0, at), prefix: value.slice(at + 1) }];
|
|
97
|
-
}
|
|
98
|
-
|
|
99
24
|
function collectValue<T extends string>(value: T, previous: T[]): T[] {
|
|
100
25
|
return [...previous, value];
|
|
101
26
|
}
|
|
102
27
|
|
|
103
|
-
function increment(v: Semver, level: Level): Semver {
|
|
104
|
-
if (level === "major") return [v[0] + 1, 0, 0];
|
|
105
|
-
if (level === "minor") return [v[0], v[1] + 1, 0];
|
|
106
|
-
return [v[0], v[1], v[2] + 1];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function git(args: string[], capture = false): string {
|
|
110
|
-
const res = exec.spawnSync("git", args, {
|
|
111
|
-
cwd: process.cwd(),
|
|
112
|
-
stdout: capture ? "capture" : "inherit",
|
|
113
|
-
stderr: capture ? "ignore" : "inherit",
|
|
114
|
-
stdin: "ignore",
|
|
115
|
-
check: !capture,
|
|
116
|
-
});
|
|
117
|
-
return res.stdout?.trim() ?? "";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function assertReleaseTagsPointToHead(tags: readonly string[]): void {
|
|
121
|
-
const head = git(["rev-parse", "HEAD"], true);
|
|
122
|
-
for (const tag of tags) {
|
|
123
|
-
const target = git(["rev-parse", `${tag}^{commit}`], true);
|
|
124
|
-
if (!head || target !== head) {
|
|
125
|
-
throw new Error(`Release tag ${tag} does not point to the pushed HEAD commit`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Write ONLY the `version` field into a manifest projen owns (read-only, so
|
|
132
|
-
* bracketed by a chmod that restores the mode). Used for the ROOT and the
|
|
133
|
-
* standalone `projen/` sibling so the committed release marks the version.
|
|
134
|
-
*
|
|
135
|
-
* It deliberately does NOT rewrite `@scope/*` dep ranges. `projen/` is a workspace
|
|
136
|
-
* member with `workspace:*` sibling deps; baking a `^version` for the just-bumped
|
|
137
|
-
* (not-yet-published) version into the COMMITTED manifest breaks the release
|
|
138
|
-
* workflow's initial `bun install`, which checks out that commit before anything
|
|
139
|
-
* is published. Sibling versions are resolved transiently at publish time instead:
|
|
140
|
-
* The bump stamps every workspace member before committing so independently
|
|
141
|
-
* synthesized members and the Bun lock resolve the same release version.
|
|
142
|
-
*/
|
|
143
|
-
function writeManifestVersion(pkgPath: string, version: string): void {
|
|
144
|
-
const { mode } = statSync(pkgPath);
|
|
145
|
-
chmodSync(pkgPath, mode | 0o200);
|
|
146
|
-
try {
|
|
147
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
|
148
|
-
pkg.version = version;
|
|
149
|
-
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
150
|
-
} finally {
|
|
151
|
-
chmodSync(pkgPath, mode);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Resolve the `--local-registry` value to a registry URL to publish to, or
|
|
157
|
-
* `undefined` to skip. `false` skips; a URL is used as-is; `auto` uses the
|
|
158
|
-
* active npm registry only when it is a loopback host (a local verdaccio etc.).
|
|
159
|
-
*/
|
|
160
|
-
function resolveLocalRegistry(value: string): string | undefined {
|
|
161
|
-
const trimmed = value.trim();
|
|
162
|
-
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
163
|
-
if (trimmed.toLowerCase() === "auto") {
|
|
164
|
-
const registry = project.npmRegistry();
|
|
165
|
-
return registry && net.isLoopbackHost(registry) ? registry.href : undefined;
|
|
166
|
-
}
|
|
167
|
-
return trimmed;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function localCargoRegistry(): string | undefined {
|
|
171
|
-
if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
|
|
172
|
-
const config = join(homedir(), ".cargo", "config.toml");
|
|
173
|
-
if (!existsSync(config)) return undefined;
|
|
174
|
-
const source = readFileSync(config, "utf8");
|
|
175
|
-
const sections = [
|
|
176
|
-
...source.matchAll(/^\[registries\.([^\]]+)\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/gm),
|
|
177
|
-
];
|
|
178
|
-
for (const section of sections) {
|
|
179
|
-
const index = section[2]?.match(/^\s*index\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
180
|
-
if (index && net.isLoopbackHost(index.replace(/^sparse\+/, ""))) return section[1];
|
|
181
|
-
}
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
28
|
const program = new Command();
|
|
186
29
|
program
|
|
187
|
-
.description("
|
|
30
|
+
.description("Increment VERSION and synchronize generated workspace versions")
|
|
188
31
|
.addOption(
|
|
189
32
|
new Option("-l, --level <level>", "semver increment").choices([...LEVELS]).default("patch"),
|
|
190
33
|
)
|
|
191
|
-
.option("--prefix <prefix>", "git tag prefix", "v")
|
|
192
|
-
.option(
|
|
193
|
-
"--sibling <dir:prefix>",
|
|
194
|
-
"standalone in-repo project (not a workspace member) to release at the same version, repeatable",
|
|
195
|
-
parseSibling,
|
|
196
|
-
[] as Sibling[],
|
|
197
|
-
)
|
|
34
|
+
.option("--prefix <prefix>", "git tag prefix used to resolve the published base", "v")
|
|
198
35
|
.addOption(
|
|
199
36
|
new Option("--os <os>", "release operating system, repeatable; crossed with every --arch")
|
|
200
37
|
.choices([...RELEASE_OSES])
|
|
@@ -207,98 +44,28 @@ program
|
|
|
207
44
|
.argParser((value, previous: ReleaseArch[]) => collectValue(value as ReleaseArch, previous))
|
|
208
45
|
.default([] as ReleaseArch[]),
|
|
209
46
|
)
|
|
210
|
-
|
|
211
|
-
// `true` and is turned off by `--no-synth` / `--no-version` / ... (the
|
|
212
|
-
// positive `--synth` etc. also work and are no-ops on the default).
|
|
213
|
-
.option("--no-synth", "do not run `projen` (synth) before bumping")
|
|
214
|
-
.option("--no-version", "do not write the bumped version into package.json")
|
|
215
|
-
.option("--no-commit", "do not commit the version change")
|
|
216
|
-
.option("--tag", "create the release tag locally instead of leaving it to the workflow")
|
|
217
|
-
.option("--no-push", "do not push the current branch to origin")
|
|
218
|
-
// `--publish` is a friendlier alias for `--push`.
|
|
219
|
-
.option("--no-publish", "alias for --no-push")
|
|
220
|
-
.option(
|
|
221
|
-
"--local-registry <value>",
|
|
222
|
-
"publish locally after the branch push: 'auto' (only a loopback npm registry), 'false', or a registry URL",
|
|
223
|
-
"auto",
|
|
224
|
-
)
|
|
225
|
-
.option(
|
|
226
|
-
"--local-pypi <value>",
|
|
227
|
-
"publish Python packages locally: 'auto' (any active index-url or extra-index-url that is a loopback devpi +simple), 'false', or a devpi URL",
|
|
228
|
-
"auto",
|
|
229
|
-
)
|
|
230
|
-
.option("--python-root <path>", "Python workspace package root", "packages/py")
|
|
47
|
+
.option("--no-synth", "write VERSION without synchronizing generated files")
|
|
231
48
|
.action(
|
|
232
|
-
|
|
233
|
-
level:
|
|
49
|
+
(opts: {
|
|
50
|
+
level: VersionLevel;
|
|
234
51
|
prefix: string;
|
|
235
|
-
sibling: Sibling[];
|
|
236
52
|
os: ReleaseOs[];
|
|
237
53
|
arch: ReleaseArch[];
|
|
238
54
|
synth: boolean;
|
|
239
|
-
version: boolean;
|
|
240
|
-
commit: boolean;
|
|
241
|
-
tag: boolean;
|
|
242
|
-
push: boolean;
|
|
243
|
-
publish: boolean;
|
|
244
|
-
localPypi: string;
|
|
245
|
-
localRegistry: string;
|
|
246
|
-
pythonRoot: string;
|
|
247
55
|
}) => {
|
|
248
|
-
const pkgPath = resolve(process.cwd(), "package.json");
|
|
249
|
-
if (!existsSync(pkgPath)) throw new Error(`no package.json in ${process.cwd()}`);
|
|
250
|
-
const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
|
|
251
|
-
|
|
252
|
-
const siblings = opts.sibling.map((s) => ({ ...s, pkgPath: resolve(s.dir, "package.json") }));
|
|
253
|
-
for (const s of siblings) {
|
|
254
|
-
if (!existsSync(s.pkgPath)) throw new Error(`--sibling ${s.dir}: no package.json there`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// The `VERSION` file is the workspace source of truth and lives at the repo
|
|
258
|
-
// root, even when this task runs from a subdirectory (`cd projen && bun run
|
|
259
|
-
// bump`), so the engine and packages always share one number.
|
|
260
56
|
const root = project.root() ?? process.cwd();
|
|
57
|
+
const next = resolveNextVersion(root, [opts.prefix], opts.level);
|
|
58
|
+
const releasePlatforms = releasePlatformFilter(opts.os, opts.arch);
|
|
261
59
|
|
|
262
|
-
// Base = the highest published tag across EVERY namespace being released
|
|
263
|
-
// (fetched from the remote so a release cut elsewhere wins), falling back to
|
|
264
|
-
// the local `VERSION` file when the remote is unreachable or has no tag. A
|
|
265
|
-
// local file that happens to be ahead does NOT override an existing remote.
|
|
266
|
-
const prefixes = [opts.prefix, ...siblings.map((s) => s.prefix)];
|
|
267
|
-
const baseInfo = resolveBaseVersion(root, prefixes);
|
|
268
|
-
const base = parseSemver(baseInfo.version) ?? [0, 0, 1];
|
|
269
|
-
const next = increment(base, opts.level);
|
|
270
|
-
const version = next.join(".");
|
|
271
|
-
const tags = prefixes.map((prefix) => `${prefix}${version}`);
|
|
272
60
|
logger.info(
|
|
273
|
-
`bump ${base
|
|
274
|
-
`${
|
|
61
|
+
`bump ${next.base} -> ${next.version} (${opts.level})` +
|
|
62
|
+
`${next.source === "remote" ? "" : " [no remote tag; used local VERSION]"}`,
|
|
275
63
|
);
|
|
276
|
-
|
|
277
|
-
for (const prefix of prefixes) {
|
|
278
|
-
const tagged = latestTagVersion(root, prefix);
|
|
279
|
-
if (tagged && compareSemver(tagged, base) < 0) {
|
|
280
|
-
logger.info(`${prefix}* was behind at ${tagged.join(".")}, catching it up`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const push = opts.push && opts.publish;
|
|
285
|
-
const createTags = opts.tag || siblings.length > 0;
|
|
286
|
-
|
|
287
|
-
// Write the source of truth first, then synth so every generated manifest
|
|
288
|
-
// COPIES it - synth never invents, resets, or drifts a version on its own.
|
|
289
|
-
if (opts.version) {
|
|
290
|
-
writeWorkspaceVersion(root, version);
|
|
291
|
-
// Keep the writable root/sibling manifests coherent even when synth is
|
|
292
|
-
// skipped (`--no-synth`); synth would otherwise set the same value.
|
|
293
|
-
writeManifestVersion(pkgPath, version);
|
|
294
|
-
for (const s of siblings) writeManifestVersion(s.pkgPath, version);
|
|
295
|
-
logger.info(`wrote version ${version} to ${root}/VERSION`);
|
|
296
|
-
}
|
|
64
|
+
writeWorkspaceVersion(root, next.version);
|
|
297
65
|
|
|
298
66
|
if (opts.synth) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
cwd: process.cwd(),
|
|
67
|
+
exec.spawnSync(process.execPath, [".projenrc.ts"], {
|
|
68
|
+
cwd: root,
|
|
302
69
|
stdout: "inherit",
|
|
303
70
|
stderr: "inherit",
|
|
304
71
|
stdin: "ignore",
|
|
@@ -309,194 +76,7 @@ program
|
|
|
309
76
|
},
|
|
310
77
|
});
|
|
311
78
|
}
|
|
312
|
-
|
|
313
|
-
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
314
|
-
exec.spawnSync("bun", [publishScript, version, "--stamp-only"], {
|
|
315
|
-
cwd: process.cwd(),
|
|
316
|
-
stdout: "inherit",
|
|
317
|
-
stderr: "inherit",
|
|
318
|
-
stdin: "ignore",
|
|
319
|
-
check: true,
|
|
320
|
-
});
|
|
321
|
-
if (existsSync(join(root, "Cargo.toml"))) {
|
|
322
|
-
logger.info("refreshing Cargo.lock workspace versions");
|
|
323
|
-
exec.spawnSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], {
|
|
324
|
-
cwd: process.cwd(),
|
|
325
|
-
stdout: "ignore",
|
|
326
|
-
stderr: "inherit",
|
|
327
|
-
stdin: "ignore",
|
|
328
|
-
check: true,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (opts.commit) {
|
|
334
|
-
// Stage the whole tree so the release commit captures the version bump
|
|
335
|
-
// plus anything synth regenerated. Skip the commit when nothing changed.
|
|
336
|
-
git(["add", "-A"]);
|
|
337
|
-
const staged = git(["diff", "--cached", "--name-only"], true);
|
|
338
|
-
if (staged) {
|
|
339
|
-
git([
|
|
340
|
-
"commit",
|
|
341
|
-
"-m",
|
|
342
|
-
`chore(release): ${version}`,
|
|
343
|
-
"-m",
|
|
344
|
-
"Shipped with Kanna - https://kanna.sh",
|
|
345
|
-
"-m",
|
|
346
|
-
"Co-Authored-By: Kanna <noreply@kanna.sh>\nKanna-Agent: codex/databricks-gpt-5-6-sol",
|
|
347
|
-
]);
|
|
348
|
-
} else {
|
|
349
|
-
logger.info("nothing to commit");
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (createTags) {
|
|
354
|
-
for (const t of tags) git(["tag", "-a", t, "-m", t]);
|
|
355
|
-
logger.info(`tagged ${tags.join(", ")}`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (push) {
|
|
359
|
-
git(["push", "origin", "HEAD"]);
|
|
360
|
-
if (createTags) {
|
|
361
|
-
assertReleaseTagsPointToHead(tags);
|
|
362
|
-
git(["push", "--no-verify", "origin", ...tags]);
|
|
363
|
-
}
|
|
364
|
-
logger.success(`pushed ${createTags ? tags.join(", ") : "HEAD"} to origin`);
|
|
365
|
-
} else {
|
|
366
|
-
logger.info("skipped push (--no-push / --no-publish)");
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Local registry publication starts after the branch push so the GitHub
|
|
370
|
-
// release workflow can own public publication independently.
|
|
371
|
-
const localRegistry = resolveLocalRegistry(opts.localRegistry);
|
|
372
|
-
const publishToLocalRegistry = opts.version && localRegistry;
|
|
373
|
-
if (opts.version === false && localRegistry) {
|
|
374
|
-
logger.info("skipped local publish (--no-version left package.json unbumped)");
|
|
375
|
-
}
|
|
376
|
-
// Scan EVERY active index (primary index-url + every extra-index-url,
|
|
377
|
-
// across uv and pip): auto-mode publishes to the first that is a loopback
|
|
378
|
-
// devpi +simple, so the corp proxy stays primary and a local devpi added
|
|
379
|
-
// as an extra index is the detected publish target.
|
|
380
|
-
const activeIndexes = activePythonIndexes();
|
|
381
|
-
const localPypi = resolveLocalPypi(opts.localPypi, activeIndexes);
|
|
382
|
-
const pythonRoot = resolve(opts.pythonRoot);
|
|
383
|
-
if (
|
|
384
|
-
opts.localPypi.toLowerCase() === "auto" &&
|
|
385
|
-
activeIndexes.some((index) => net.isLoopbackHost(index)) &&
|
|
386
|
-
!localPypi
|
|
387
|
-
) {
|
|
388
|
-
logger.info(
|
|
389
|
-
`skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
if (opts.version === false && localPypi) {
|
|
393
|
-
logger.info("skipped local Python publish (--no-version left packages unstamped)");
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// npm and Python touch disjoint package trees and registries. Start both
|
|
397
|
-
// mirrors together so local publishing takes the slower duration rather
|
|
398
|
-
// than the sum of both. Each child still owns its internal build/upload
|
|
399
|
-
// ordering and restores its temporary manifest edits on exit.
|
|
400
|
-
const localPublishes: Promise<unknown>[] = [];
|
|
401
|
-
if (publishToLocalRegistry) {
|
|
402
|
-
logger.info(`publishing ${version} to local registry ${localRegistry}`);
|
|
403
|
-
// The shared driver skips stamping/install when synth already made the
|
|
404
|
-
// workspace release-current, compiles publishable members once from the
|
|
405
|
-
// root, then publishes through a bounded pool. Temporary publishConfig
|
|
406
|
-
// edits are restored before this child exits.
|
|
407
|
-
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
408
|
-
localPublishes.push(
|
|
409
|
-
exec
|
|
410
|
-
.spawn("bun", [publishScript, version, "--registry", localRegistry], {
|
|
411
|
-
cwd: process.cwd(),
|
|
412
|
-
stdout: "inherit",
|
|
413
|
-
stderr: "inherit",
|
|
414
|
-
stdin: "ignore",
|
|
415
|
-
check: true,
|
|
416
|
-
})
|
|
417
|
-
.then(() => logger.success(`published ${version} to ${localRegistry}`)),
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
if (opts.version && localPypi && existsSync(pythonRoot)) {
|
|
421
|
-
logger.info(`publishing Python ${version} to local devpi ${localPypi.publishUrl}`);
|
|
422
|
-
const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
|
|
423
|
-
localPublishes.push(
|
|
424
|
-
exec
|
|
425
|
-
.spawn(
|
|
426
|
-
"bun",
|
|
427
|
-
[
|
|
428
|
-
publishPythonScript,
|
|
429
|
-
version,
|
|
430
|
-
"--root",
|
|
431
|
-
pythonRoot,
|
|
432
|
-
"--index-url",
|
|
433
|
-
localPypi.indexUrl,
|
|
434
|
-
"--publish-url",
|
|
435
|
-
localPypi.publishUrl,
|
|
436
|
-
],
|
|
437
|
-
{
|
|
438
|
-
cwd: process.cwd(),
|
|
439
|
-
stdout: "inherit",
|
|
440
|
-
stderr: "inherit",
|
|
441
|
-
stdin: "ignore",
|
|
442
|
-
check: true,
|
|
443
|
-
},
|
|
444
|
-
)
|
|
445
|
-
.then(() => logger.success(`published Python ${version} to ${localPypi.publishUrl}`)),
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
await Promise.all(localPublishes);
|
|
449
|
-
if (opts.version && (publishToLocalRegistry || localPypi)) {
|
|
450
|
-
const publishUniFFIScript = fileURLToPath(
|
|
451
|
-
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
452
|
-
);
|
|
453
|
-
await exec.spawn(
|
|
454
|
-
"bun",
|
|
455
|
-
[
|
|
456
|
-
publishUniFFIScript,
|
|
457
|
-
"--version",
|
|
458
|
-
version,
|
|
459
|
-
...(publishToLocalRegistry ? ["--registry", localRegistry] : []),
|
|
460
|
-
...(localPypi ? ["--pypi-publish-url", localPypi.publishUrl] : []),
|
|
461
|
-
],
|
|
462
|
-
{
|
|
463
|
-
cwd: process.cwd(),
|
|
464
|
-
stdout: "inherit",
|
|
465
|
-
stderr: "inherit",
|
|
466
|
-
stdin: "ignore",
|
|
467
|
-
check: true,
|
|
468
|
-
},
|
|
469
|
-
);
|
|
470
|
-
logger.success(`published host-native bindings for ${version}`);
|
|
471
|
-
}
|
|
472
|
-
const rust = readDbxToolsConfig(repoRoot)?.rust;
|
|
473
|
-
const hasRustCrates = Boolean(
|
|
474
|
-
rust &&
|
|
475
|
-
typeof rust === "object" &&
|
|
476
|
-
!Array.isArray(rust) &&
|
|
477
|
-
Array.isArray((rust as { crates?: unknown }).crates) &&
|
|
478
|
-
(rust as { crates: unknown[] }).crates.length,
|
|
479
|
-
);
|
|
480
|
-
if (opts.version && hasRustCrates) {
|
|
481
|
-
const cargoRegistry = localCargoRegistry();
|
|
482
|
-
if (cargoRegistry) {
|
|
483
|
-
const publishUniFFIScript = fileURLToPath(
|
|
484
|
-
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
485
|
-
);
|
|
486
|
-
await exec.spawn(
|
|
487
|
-
"bun",
|
|
488
|
-
[publishUniFFIScript, "--version", version, "--cargo-registry", cargoRegistry],
|
|
489
|
-
{
|
|
490
|
-
cwd: process.cwd(),
|
|
491
|
-
stdout: "inherit",
|
|
492
|
-
stderr: "inherit",
|
|
493
|
-
stdin: "ignore",
|
|
494
|
-
check: true,
|
|
495
|
-
},
|
|
496
|
-
);
|
|
497
|
-
logger.success(`published Rust ${version} to ${cargoRegistry}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
79
|
+
logger.success(`workspace version synchronized at ${next.version}`);
|
|
500
80
|
},
|
|
501
81
|
);
|
|
502
82
|
|
package/tasks/clean.ts
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Local registry preflight shared by release preparation. */
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { exec, project } from "@dbx-tools/core";
|
|
7
|
+
import { log, net } from "@dbx-tools/shared-core";
|
|
8
|
+
import { activePythonIndexes, resolveLocalPypi } from "./python-registry.ts";
|
|
9
|
+
import { readDbxToolsConfig } from "../src/packages.ts";
|
|
10
|
+
|
|
11
|
+
const logger = log.logger("projen:local-publish");
|
|
12
|
+
|
|
13
|
+
/** Options for publishing one release candidate to configured loopback registries. */
|
|
14
|
+
export interface LocalPublishOptions {
|
|
15
|
+
readonly root: string;
|
|
16
|
+
readonly version: string;
|
|
17
|
+
readonly localRegistry: string;
|
|
18
|
+
readonly localPypi: string;
|
|
19
|
+
readonly pythonRoot: string;
|
|
20
|
+
readonly localCargo: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveLocalRegistry(value: string): string | undefined {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (!trimmed || trimmed.toLowerCase() === "false") return undefined;
|
|
26
|
+
if (trimmed.toLowerCase() === "auto") {
|
|
27
|
+
const registry = project.npmRegistry();
|
|
28
|
+
return registry && net.isLoopbackHost(registry) ? registry.href : undefined;
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function localCargoRegistry(): string | undefined {
|
|
34
|
+
if (process.env.LOCAL_CARGO_REGISTRY) return process.env.LOCAL_CARGO_REGISTRY;
|
|
35
|
+
const config = join(homedir(), ".cargo", "config.toml");
|
|
36
|
+
if (!existsSync(config)) return undefined;
|
|
37
|
+
const source = readFileSync(config, "utf8");
|
|
38
|
+
const sections = [
|
|
39
|
+
...source.matchAll(/^\[registries\.([^\]]+)\]\s*\n([\s\S]*?)(?=^\[|(?![\s\S]))/gm),
|
|
40
|
+
];
|
|
41
|
+
for (const section of sections) {
|
|
42
|
+
const index = section[2]?.match(/^\s*index\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
43
|
+
if (index && net.isLoopbackHost(index.replace(/^sparse\+/, ""))) return section[1];
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Publish the exact release candidate to local npm, PyPI, and Cargo mirrors. */
|
|
49
|
+
export async function publishLocalRelease(options: LocalPublishOptions): Promise<void> {
|
|
50
|
+
const localRegistry = resolveLocalRegistry(options.localRegistry);
|
|
51
|
+
const activeIndexes = activePythonIndexes();
|
|
52
|
+
const localPypi = resolveLocalPypi(options.localPypi, activeIndexes);
|
|
53
|
+
const pythonRoot = resolve(options.root, options.pythonRoot);
|
|
54
|
+
const localPublishes: Promise<unknown>[] = [];
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
options.localPypi.toLowerCase() === "auto" &&
|
|
58
|
+
activeIndexes.some((index) => net.isLoopbackHost(index)) &&
|
|
59
|
+
!localPypi
|
|
60
|
+
) {
|
|
61
|
+
logger.info(
|
|
62
|
+
`skipped local Python publish: no active index (${activeIndexes.join(", ")}) is a devpi +simple index`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (localRegistry) {
|
|
67
|
+
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
68
|
+
logger.info(`publishing ${options.version} to local registry ${localRegistry}`);
|
|
69
|
+
localPublishes.push(
|
|
70
|
+
exec
|
|
71
|
+
.spawn(process.execPath, [publishScript, options.version, "--registry", localRegistry], {
|
|
72
|
+
cwd: options.root,
|
|
73
|
+
stdout: "inherit",
|
|
74
|
+
stderr: "inherit",
|
|
75
|
+
stdin: "ignore",
|
|
76
|
+
check: true,
|
|
77
|
+
})
|
|
78
|
+
.then(() => logger.success(`published ${options.version} to ${localRegistry}`)),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (localPypi && existsSync(pythonRoot)) {
|
|
83
|
+
const publishPythonScript = fileURLToPath(new URL("./publish-python.ts", import.meta.url));
|
|
84
|
+
logger.info(`publishing Python ${options.version} to local devpi ${localPypi.publishUrl}`);
|
|
85
|
+
localPublishes.push(
|
|
86
|
+
exec
|
|
87
|
+
.spawn(
|
|
88
|
+
process.execPath,
|
|
89
|
+
[
|
|
90
|
+
publishPythonScript,
|
|
91
|
+
options.version,
|
|
92
|
+
"--root",
|
|
93
|
+
pythonRoot,
|
|
94
|
+
"--index-url",
|
|
95
|
+
localPypi.indexUrl,
|
|
96
|
+
"--publish-url",
|
|
97
|
+
localPypi.publishUrl,
|
|
98
|
+
],
|
|
99
|
+
{
|
|
100
|
+
cwd: options.root,
|
|
101
|
+
stdout: "inherit",
|
|
102
|
+
stderr: "inherit",
|
|
103
|
+
stdin: "ignore",
|
|
104
|
+
check: true,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
.then(() =>
|
|
108
|
+
logger.success(`published Python ${options.version} to ${localPypi.publishUrl}`),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await Promise.all(localPublishes);
|
|
114
|
+
if (localRegistry || localPypi) {
|
|
115
|
+
const publishUniFFIScript = fileURLToPath(
|
|
116
|
+
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
117
|
+
);
|
|
118
|
+
await exec.spawn(
|
|
119
|
+
process.execPath,
|
|
120
|
+
[
|
|
121
|
+
publishUniFFIScript,
|
|
122
|
+
"--version",
|
|
123
|
+
options.version,
|
|
124
|
+
...(localRegistry ? ["--registry", localRegistry] : []),
|
|
125
|
+
...(localPypi ? ["--pypi-publish-url", localPypi.publishUrl] : []),
|
|
126
|
+
],
|
|
127
|
+
{
|
|
128
|
+
cwd: options.root,
|
|
129
|
+
stdout: "inherit",
|
|
130
|
+
stderr: "inherit",
|
|
131
|
+
stdin: "ignore",
|
|
132
|
+
check: true,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
logger.success(`published host-native bindings for ${options.version}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const rust = readDbxToolsConfig(options.root)?.rust;
|
|
139
|
+
const hasRustCrates = Boolean(
|
|
140
|
+
rust &&
|
|
141
|
+
typeof rust === "object" &&
|
|
142
|
+
!Array.isArray(rust) &&
|
|
143
|
+
Array.isArray((rust as { crates?: unknown }).crates) &&
|
|
144
|
+
(rust as { crates: unknown[] }).crates.length > 0,
|
|
145
|
+
);
|
|
146
|
+
if (options.localCargo && hasRustCrates) {
|
|
147
|
+
const cargoRegistry = localCargoRegistry();
|
|
148
|
+
if (cargoRegistry) {
|
|
149
|
+
const publishUniFFIScript = fileURLToPath(
|
|
150
|
+
new URL("./publish-uniffi-local.ts", import.meta.url),
|
|
151
|
+
);
|
|
152
|
+
await exec.spawn(
|
|
153
|
+
process.execPath,
|
|
154
|
+
[publishUniFFIScript, "--version", options.version, "--cargo-registry", cargoRegistry],
|
|
155
|
+
{
|
|
156
|
+
cwd: options.root,
|
|
157
|
+
stdout: "inherit",
|
|
158
|
+
stderr: "inherit",
|
|
159
|
+
stdin: "ignore",
|
|
160
|
+
check: true,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
logger.success(`published Rust ${options.version} to ${cargoRegistry}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|