@dbx-tools/projen 0.9.17 → 0.9.18
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 +29 -26
- package/index.ts +2 -2
- package/package.json +4 -5
- package/src/_release-platform.ts +10 -0
- package/src/_rust-project.ts +3 -0
- package/src/_rust-release-workflow.ts +158 -392
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +24 -5
- package/src/project-rs.ts +11 -18
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +32 -40
- package/src/release.ts +18 -5
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-plan.ts +1 -2
- package/tasks/release-pr.ts +122 -13
- package/tasks/uniffi-python.js +24 -12
- package/tasks/uniffi-release.mjs +35 -2
- package/src/_rust-release.ts +0 -633
- package/tasks/build-rust-release.ts +0 -68
- package/tasks/rust-release.d.ts +0 -45
- package/tasks/rust-release.mjs +0 -1726
package/tasks/release-pr.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
|
15
15
|
import { asyncUtils, json, log, object } from "@dbx-tools/shared-core";
|
|
16
16
|
import { Command } from "commander";
|
|
17
17
|
import { publishLocalRelease } from "./local-publish.ts";
|
|
18
|
+
import { hasLocalReleaseTargets, missingLocalReleaseTools } from "./release-assets.ts";
|
|
18
19
|
import { generateReleaseSummary } from "./release-summary.ts";
|
|
19
20
|
import {
|
|
20
21
|
releaseArchitectureOption,
|
|
@@ -38,7 +39,9 @@ import {
|
|
|
38
39
|
githubAccountSupportsWorkflowChanges,
|
|
39
40
|
githubAuthenticatedAccounts,
|
|
40
41
|
githubRepositoryApiPath,
|
|
42
|
+
githubRepositoryCanManage,
|
|
41
43
|
githubRepositoryIdentity,
|
|
44
|
+
githubRepositoryPermissions,
|
|
42
45
|
githubRepositorySpecifier,
|
|
43
46
|
githubTokenArguments,
|
|
44
47
|
githubTokenEnvironmentName,
|
|
@@ -52,6 +55,41 @@ import {
|
|
|
52
55
|
|
|
53
56
|
const logger = log.logger("projen:release");
|
|
54
57
|
|
|
58
|
+
export type ReleaseBuildMode = "auto" | "local" | "remote";
|
|
59
|
+
|
|
60
|
+
/** Resolve an explicit or environment-sensitive native release build mode. */
|
|
61
|
+
export function resolveReleaseBuildMode(options: {
|
|
62
|
+
readonly requested: ReleaseBuildMode;
|
|
63
|
+
readonly canManage: boolean;
|
|
64
|
+
readonly localTools: readonly string[];
|
|
65
|
+
readonly hasLocalTargets: boolean;
|
|
66
|
+
readonly approve: boolean;
|
|
67
|
+
readonly wait: boolean;
|
|
68
|
+
readonly hostPlatform?: NodeJS.Platform;
|
|
69
|
+
}): Exclude<ReleaseBuildMode, "auto"> {
|
|
70
|
+
const local =
|
|
71
|
+
options.canManage &&
|
|
72
|
+
options.hasLocalTargets &&
|
|
73
|
+
(options.hostPlatform ?? process.platform) === "darwin" &&
|
|
74
|
+
options.localTools.length === 0 &&
|
|
75
|
+
options.approve &&
|
|
76
|
+
options.wait;
|
|
77
|
+
if (options.requested === "auto") return local ? "local" : "remote";
|
|
78
|
+
if (options.requested === "remote") return "remote";
|
|
79
|
+
if (!options.canManage) throw new Error("--build local requires maintain or admin permission");
|
|
80
|
+
if (!options.hasLocalTargets) throw new Error("--build local requires Rust release targets");
|
|
81
|
+
if ((options.hostPlatform ?? process.platform) !== "darwin") {
|
|
82
|
+
throw new Error("--build local requires macOS");
|
|
83
|
+
}
|
|
84
|
+
if (options.localTools.length > 0) {
|
|
85
|
+
throw new Error(`--build local requires: ${options.localTools.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
if (!options.approve || !options.wait) {
|
|
88
|
+
throw new Error("--build local requires automatic merge and waiting");
|
|
89
|
+
}
|
|
90
|
+
return "local";
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
function git(
|
|
56
94
|
root: string,
|
|
57
95
|
args: string[],
|
|
@@ -151,12 +189,27 @@ async function waitForReleaseWorkflow(
|
|
|
151
189
|
});
|
|
152
190
|
}
|
|
153
191
|
|
|
192
|
+
async function waitForGitHubRelease(
|
|
193
|
+
root: string,
|
|
194
|
+
tag: string,
|
|
195
|
+
env: NodeJS.ProcessEnv,
|
|
196
|
+
timeoutMs: number,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const deadline = Date.now() + timeoutMs;
|
|
199
|
+
while (Date.now() < deadline) {
|
|
200
|
+
if (taskCommandSucceeds(root, "gh", ["release", "view", tag], { env })) return;
|
|
201
|
+
await asyncUtils.sleep(2_000);
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`GitHub Release ${tag} did not appear within ${timeoutMs}ms`);
|
|
204
|
+
}
|
|
205
|
+
|
|
154
206
|
function githubAccount(root: string): {
|
|
155
207
|
hostname: string;
|
|
156
208
|
owner: string;
|
|
157
209
|
repository: string;
|
|
158
210
|
login: string;
|
|
159
211
|
token: string;
|
|
212
|
+
canManage: boolean;
|
|
160
213
|
} {
|
|
161
214
|
const repository = projectUtils.repositoryUrl(root);
|
|
162
215
|
if (!repository) throw new Error("Release preparation requires a GitHub repository");
|
|
@@ -199,21 +252,20 @@ function githubAccount(root: string): {
|
|
|
199
252
|
});
|
|
200
253
|
const repositoryData =
|
|
201
254
|
repositoryResult.exitCode === 0 ? json.parseRecord(repositoryResult.stdout ?? "") : undefined;
|
|
202
|
-
const permissions =
|
|
203
|
-
|
|
204
|
-
: undefined;
|
|
205
|
-
if (
|
|
206
|
-
permissions?.push !== true &&
|
|
207
|
-
permissions?.maintain !== true &&
|
|
208
|
-
permissions?.admin !== true
|
|
209
|
-
) {
|
|
255
|
+
const permissions = githubRepositoryPermissions(repositoryData);
|
|
256
|
+
if (!permissions.push && !permissions.maintain && !permissions.admin) {
|
|
210
257
|
continue;
|
|
211
258
|
}
|
|
212
259
|
logger.info("selected GitHub CLI account", {
|
|
213
260
|
login: account.login,
|
|
214
261
|
repository: `${identity.owner}/${identity.repository}`,
|
|
215
262
|
});
|
|
216
|
-
return {
|
|
263
|
+
return {
|
|
264
|
+
...identity,
|
|
265
|
+
login: account.login,
|
|
266
|
+
token,
|
|
267
|
+
canManage: githubRepositoryCanManage(permissions),
|
|
268
|
+
};
|
|
217
269
|
}
|
|
218
270
|
const detected =
|
|
219
271
|
accounts
|
|
@@ -271,6 +323,7 @@ program
|
|
|
271
323
|
.option("--no-validate", "skip repository validation tasks, Rust tests, and TypeScript compile")
|
|
272
324
|
.option("--no-local-publish", "skip local npm, PyPI, and Cargo publication")
|
|
273
325
|
.option("--no-local-cargo", "skip local Cargo publication")
|
|
326
|
+
.option("--build <mode>", "native release build: auto, local, or remote", "auto")
|
|
274
327
|
.option("--no-approve", "open the release pull request without enabling automatic merge")
|
|
275
328
|
.option("--no-wait", "return after enabling automatic merge without watching publication")
|
|
276
329
|
.option(
|
|
@@ -295,6 +348,7 @@ program
|
|
|
295
348
|
validate: boolean;
|
|
296
349
|
localPublish: boolean;
|
|
297
350
|
localCargo: boolean;
|
|
351
|
+
build: ReleaseBuildMode;
|
|
298
352
|
approve: boolean;
|
|
299
353
|
wait: boolean;
|
|
300
354
|
waitTimeoutMinutes: string;
|
|
@@ -316,6 +370,26 @@ program
|
|
|
316
370
|
const currentBranch = git(root, ["branch", "--show-current"], { capture: true });
|
|
317
371
|
if (!currentBranch) throw new Error("Release preparation requires a local branch");
|
|
318
372
|
const account = githubAccount(root);
|
|
373
|
+
if (!["auto", "local", "remote"].includes(opts.build)) {
|
|
374
|
+
throw new Error("--build must be auto, local, or remote");
|
|
375
|
+
}
|
|
376
|
+
const inspectLocalTools =
|
|
377
|
+
opts.build === "local" || (opts.build === "auto" && account.canManage);
|
|
378
|
+
const localTools = inspectLocalTools ? missingLocalReleaseTools(root) : [];
|
|
379
|
+
const buildMode = resolveReleaseBuildMode({
|
|
380
|
+
requested: opts.build,
|
|
381
|
+
canManage: account.canManage,
|
|
382
|
+
localTools,
|
|
383
|
+
hasLocalTargets: hasLocalReleaseTargets(root),
|
|
384
|
+
approve: opts.approve,
|
|
385
|
+
wait: opts.wait,
|
|
386
|
+
});
|
|
387
|
+
logger.info("selected native release build", {
|
|
388
|
+
requested: opts.build,
|
|
389
|
+
resolved: buildMode,
|
|
390
|
+
manager: account.canManage,
|
|
391
|
+
...(localTools.length > 0 ? { missing: localTools } : {}),
|
|
392
|
+
});
|
|
319
393
|
git(root, ["fetch", "--tags", "origin", opts.base]);
|
|
320
394
|
const comparisonBase = resolveBaseVersion(root, [opts.prefix], {
|
|
321
395
|
fetch: false,
|
|
@@ -455,7 +529,13 @@ program
|
|
|
455
529
|
git(releaseRoot, ["add", "-A"]);
|
|
456
530
|
const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
|
|
457
531
|
if (staged) {
|
|
458
|
-
git(releaseRoot, [
|
|
532
|
+
git(releaseRoot, [
|
|
533
|
+
"commit",
|
|
534
|
+
"-m",
|
|
535
|
+
`chore(release): ${next.version}`,
|
|
536
|
+
"-m",
|
|
537
|
+
`Release-Build: ${buildMode}`,
|
|
538
|
+
]);
|
|
459
539
|
} else if (!worktreeExists) {
|
|
460
540
|
throw new Error("Release preparation produced no changes");
|
|
461
541
|
}
|
|
@@ -471,6 +551,8 @@ program
|
|
|
471
551
|
`Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
|
|
472
552
|
"",
|
|
473
553
|
"Merging this PR updates VERSION on main and starts the public release workflow.",
|
|
554
|
+
"",
|
|
555
|
+
`Native build: ${buildMode}.`,
|
|
474
556
|
...(releaseSummary ? ["", releaseSummary] : []),
|
|
475
557
|
].join("\n");
|
|
476
558
|
const githubEnvironment = {
|
|
@@ -511,8 +593,6 @@ program
|
|
|
511
593
|
env: githubEnvironment,
|
|
512
594
|
});
|
|
513
595
|
}
|
|
514
|
-
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
515
|
-
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
516
596
|
if (opts.approve && opts.wait) {
|
|
517
597
|
const mergeSha = await waitForPullRequestMerge(
|
|
518
598
|
root,
|
|
@@ -521,10 +601,39 @@ program
|
|
|
521
601
|
waitTimeoutMs,
|
|
522
602
|
);
|
|
523
603
|
logger.info("release pull request merged", { releaseBranch, mergeSha });
|
|
604
|
+
if (buildMode === "local") {
|
|
605
|
+
git(root, ["fetch", "origin", opts.base]);
|
|
606
|
+
git(releaseRoot, ["switch", "--detach", mergeSha]);
|
|
607
|
+
runTaskCommand(releaseRoot, process.execPath, ["install"]);
|
|
608
|
+
await waitForGitHubRelease(root, releaseTag, githubEnvironment, waitTimeoutMs);
|
|
609
|
+
const releaseAssetsScript = join(
|
|
610
|
+
releaseRoot,
|
|
611
|
+
"node_modules/@dbx-tools/projen/tasks/release-assets.ts",
|
|
612
|
+
);
|
|
613
|
+
runTaskCommand(
|
|
614
|
+
releaseRoot,
|
|
615
|
+
process.execPath,
|
|
616
|
+
[
|
|
617
|
+
releaseAssetsScript,
|
|
618
|
+
"--root",
|
|
619
|
+
releaseRoot,
|
|
620
|
+
"--version",
|
|
621
|
+
next.version,
|
|
622
|
+
"--tag",
|
|
623
|
+
releaseTag,
|
|
624
|
+
"--upload",
|
|
625
|
+
],
|
|
626
|
+
{ env: githubEnvironment },
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
630
|
+
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
524
631
|
await waitForReleaseWorkflow(root, opts.base, mergeSha, githubEnvironment, waitTimeoutMs);
|
|
525
632
|
logger.success(`published ${releaseTag} from ${mergeSha}`);
|
|
526
633
|
return;
|
|
527
634
|
}
|
|
635
|
+
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
636
|
+
git(root, ["branch", "--delete", "--force", releaseBranch]);
|
|
528
637
|
logger.success(
|
|
529
638
|
`${
|
|
530
639
|
opts.approve ? "enabled automatic merge for" : "opened"
|
|
@@ -534,4 +643,4 @@ program
|
|
|
534
643
|
},
|
|
535
644
|
);
|
|
536
645
|
|
|
537
|
-
await program.parseAsync();
|
|
646
|
+
if (import.meta.main) await program.parseAsync();
|
package/tasks/uniffi-python.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
mkdirSync,
|
|
6
6
|
mkdtempSync,
|
|
7
7
|
readFileSync,
|
|
8
|
+
readdirSync,
|
|
8
9
|
rmSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
} from "node:fs";
|
|
@@ -130,6 +131,7 @@ export const installPythonBindings = ({
|
|
|
130
131
|
module,
|
|
131
132
|
packageRoot,
|
|
132
133
|
platform,
|
|
134
|
+
generatedSource,
|
|
133
135
|
readonly = false,
|
|
134
136
|
run,
|
|
135
137
|
targetDirectory,
|
|
@@ -137,20 +139,25 @@ export const installPythonBindings = ({
|
|
|
137
139
|
const packageName = module.split(".").at(-1);
|
|
138
140
|
if (!packageName) throw new Error(`Invalid Python module ${module}`);
|
|
139
141
|
const packageDirectory = resolve(packageRoot, "src", ...module.split("."));
|
|
140
|
-
const generatedDirectory =
|
|
142
|
+
const generatedDirectory = generatedSource
|
|
143
|
+
? undefined
|
|
144
|
+
: mkdtempSync(join(tmpdir(), `${packageName}-python-`));
|
|
141
145
|
const generator = resolve(targetDirectory, pythonBindingGeneratorName(crate, platform));
|
|
142
|
-
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
143
146
|
|
|
144
147
|
try {
|
|
145
|
-
|
|
146
|
-
generator
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
if (!generatedSource) {
|
|
149
|
+
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
150
|
+
run(
|
|
151
|
+
generator,
|
|
152
|
+
pythonBindingGeneratorArgs({
|
|
153
|
+
crate,
|
|
154
|
+
library,
|
|
155
|
+
output: generatedDirectory,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const generated =
|
|
160
|
+
generatedSource ?? join(generatedDirectory, `${crate.replaceAll("-", "_")}.py`);
|
|
154
161
|
if (!existsSync(generated)) throw new Error(`Missing generated binding ${generated}`);
|
|
155
162
|
mkdirSync(packageDirectory, { recursive: true });
|
|
156
163
|
|
|
@@ -162,6 +169,11 @@ export const installPythonBindings = ({
|
|
|
162
169
|
setWritable(init);
|
|
163
170
|
writeFileSync(init, `${generatedHeader(crate)}from .bindings import *\n`);
|
|
164
171
|
|
|
172
|
+
for (const candidate of readdirSync(packageDirectory)) {
|
|
173
|
+
if (/\.(?:dll|dylib|so)$/.test(candidate)) {
|
|
174
|
+
rmSync(join(packageDirectory, candidate), { force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
165
177
|
const nativeLibrary = join(packageDirectory, basename(library));
|
|
166
178
|
setWritable(nativeLibrary);
|
|
167
179
|
cpSync(library, nativeLibrary);
|
|
@@ -174,6 +186,6 @@ export const installPythonBindings = ({
|
|
|
174
186
|
rmSync(join(packageDirectory, "_generated"), { recursive: true, force: true });
|
|
175
187
|
return { bindings, init, library: nativeLibrary };
|
|
176
188
|
} finally {
|
|
177
|
-
rmSync(generatedDirectory, { recursive: true, force: true });
|
|
189
|
+
if (generatedDirectory) rmSync(generatedDirectory, { recursive: true, force: true });
|
|
178
190
|
}
|
|
179
191
|
};
|
package/tasks/uniffi-release.mjs
CHANGED
|
@@ -17,7 +17,12 @@ import { fileURLToPath } from "node:url";
|
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
18
|
import { parseArgs } from "node:util";
|
|
19
19
|
import { spawnSync } from "node:child_process";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
installPythonBindings,
|
|
22
|
+
pythonBindingGeneratorArgs,
|
|
23
|
+
pythonBindingGeneratorName,
|
|
24
|
+
stampPythonProject,
|
|
25
|
+
} from "./uniffi-python.js";
|
|
21
26
|
|
|
22
27
|
const require = createRequire(import.meta.url);
|
|
23
28
|
const bundledToml = fileURLToPath(new URL("./smol-toml.cjs", import.meta.url));
|
|
@@ -34,7 +39,9 @@ const parsed = parseArgs({
|
|
|
34
39
|
"native-package": { type: "string" },
|
|
35
40
|
"python-package": { type: "string" },
|
|
36
41
|
"python-module": { type: "string" },
|
|
42
|
+
"python-bindings": { type: "string" },
|
|
37
43
|
"cargo-target": { type: "string" },
|
|
44
|
+
"generator-target": { type: "string" },
|
|
38
45
|
"node-triple": { type: "string" },
|
|
39
46
|
"python-tag": { type: "string" },
|
|
40
47
|
os: { type: "string" },
|
|
@@ -414,8 +421,10 @@ const packagePython = ({
|
|
|
414
421
|
pythonTag,
|
|
415
422
|
version,
|
|
416
423
|
cargoTarget,
|
|
424
|
+
generatorTarget,
|
|
417
425
|
os,
|
|
418
426
|
pythonModule,
|
|
427
|
+
pythonBindings,
|
|
419
428
|
}) => {
|
|
420
429
|
const pythonRoot = resolve(output, "python-root");
|
|
421
430
|
cpSync(resolve(root, pythonDirectory), pythonRoot, { recursive: true });
|
|
@@ -442,8 +451,9 @@ const packagePython = ({
|
|
|
442
451
|
module: pythonModule,
|
|
443
452
|
packageRoot: pythonRoot,
|
|
444
453
|
platform: os,
|
|
454
|
+
generatedSource: pythonBindings,
|
|
445
455
|
run,
|
|
446
|
-
targetDirectory: resolve(cargoTargetRoot, cargoTarget, "release"),
|
|
456
|
+
targetDirectory: resolve(cargoTargetRoot, generatorTarget ?? cargoTarget, "release"),
|
|
447
457
|
});
|
|
448
458
|
|
|
449
459
|
const wheelDirectory = resolve(output, "python");
|
|
@@ -463,6 +473,25 @@ const packagePython = ({
|
|
|
463
473
|
]);
|
|
464
474
|
};
|
|
465
475
|
|
|
476
|
+
const generatePython = () => {
|
|
477
|
+
const crate = required("crate");
|
|
478
|
+
const cargoTarget = required("cargo-target");
|
|
479
|
+
const os = required("os");
|
|
480
|
+
const output = resolve(root, required("output"));
|
|
481
|
+
const generator = resolve(
|
|
482
|
+
cargoTargetRoot,
|
|
483
|
+
cargoTarget,
|
|
484
|
+
"release",
|
|
485
|
+
pythonBindingGeneratorName(crate, os),
|
|
486
|
+
);
|
|
487
|
+
const library = libraryPath(crate, cargoTarget, os);
|
|
488
|
+
if (!existsSync(generator)) throw new Error(`Missing UniFFI generator ${generator}`);
|
|
489
|
+
if (!existsSync(library)) throw new Error(`Missing native library ${library}`);
|
|
490
|
+
rmSync(output, { recursive: true, force: true });
|
|
491
|
+
mkdirSync(output, { recursive: true });
|
|
492
|
+
run(generator, pythonBindingGeneratorArgs({ crate, library, output }));
|
|
493
|
+
};
|
|
494
|
+
|
|
466
495
|
const build = () => {
|
|
467
496
|
const crate = required("crate");
|
|
468
497
|
const cargoTarget = required("cargo-target");
|
|
@@ -517,14 +546,18 @@ const build = () => {
|
|
|
517
546
|
pythonTag,
|
|
518
547
|
version,
|
|
519
548
|
cargoTarget,
|
|
549
|
+
generatorTarget: parsed.values["generator-target"],
|
|
520
550
|
os,
|
|
521
551
|
pythonModule: required("python-module"),
|
|
552
|
+
pythonBindings: parsed.values["python-bindings"],
|
|
522
553
|
});
|
|
523
554
|
}
|
|
524
555
|
};
|
|
525
556
|
|
|
526
557
|
if (parsed.positionals[0] === "build") {
|
|
527
558
|
build();
|
|
559
|
+
} else if (parsed.positionals[0] === "generate-python") {
|
|
560
|
+
generatePython();
|
|
528
561
|
} else if (parsed.positionals[0] === "facade") {
|
|
529
562
|
packageNodeFacade({
|
|
530
563
|
output: resolve(root, required("output")),
|