@dbx-tools/projen 0.9.18 → 0.9.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/._LICENSE +0 -0
- package/._README.md +0 -0
- package/._index.ts +0 -0
- package/._package.json +0 -0
- package/README.md +72 -88
- package/index.ts +2 -7
- package/package.json +9 -5
- package/src/.__lazy-require.ts +0 -0
- package/src/.__release-platform.ts +0 -0
- package/src/.__rust-project.ts +0 -0
- package/src/.__rust-release-workflow.ts +0 -0
- package/src/.__task-command.ts +0 -0
- package/src/._barrels.ts +0 -0
- package/src/._bun-app.ts +0 -0
- package/src/._bun-workflow.ts +0 -0
- package/src/._clean.ts +0 -0
- package/src/._codegen.ts +0 -0
- package/src/._dbx-tools-config.ts +0 -0
- package/src/._engine-root.ts +0 -0
- package/src/._generated.ts +0 -0
- package/src/._mixin.ts +0 -0
- package/src/._module-exports.ts +0 -0
- package/src/._openapi.ts +0 -0
- package/src/._packages.ts +0 -0
- package/src/._pnpm-workspace.ts +0 -0
- package/src/._project-js.ts +0 -0
- package/src/._project-predicate.ts +0 -0
- package/src/._project-py.ts +0 -0
- package/src/._project-rs.ts +0 -0
- package/src/._project.ts +0 -0
- package/src/._projen-version.ts +0 -0
- package/src/._publish.ts +0 -0
- package/src/._release-catalog.ts +0 -0
- package/src/._release-dispatch.ts +0 -0
- package/src/._release-github.ts +0 -0
- package/src/._release.ts +0 -0
- package/src/._scaffold.ts +0 -0
- package/src/._tags.ts +0 -0
- package/src/._tsconfig.ts +0 -0
- package/src/._uniffi.ts +0 -0
- package/src/._vscode.ts +0 -0
- package/src/._watch.ts +0 -0
- package/src/._workspace-lock.ts +0 -0
- package/src/._workspace-version.ts +0 -0
- package/src/_rust-release-workflow.ts +18 -582
- package/src/_task-command.ts +75 -0
- package/src/project-js.ts +2 -42
- package/src/project-py.ts +60 -162
- package/src/project-rs.ts +4 -100
- package/src/release-catalog.ts +37 -203
- package/src/release.ts +96 -775
- package/src/scaffold.ts +2 -6
- package/src/workspace-version.ts +9 -121
- package/tasks/._barrels.ts +0 -0
- package/tasks/._bump.ts +0 -0
- package/tasks/._clean.ts +0 -0
- package/tasks/._compile-workspace.ts +0 -0
- package/tasks/._local-publish.ts +0 -0
- package/tasks/._openapi.ts +0 -0
- package/tasks/._projenrc.ts +0 -0
- package/tasks/._publish-npm.ts +0 -0
- package/tasks/._publish-python.ts +0 -0
- package/tasks/._publish-uniffi-local.ts +0 -0
- package/tasks/._publish.ts +0 -0
- package/tasks/._python-registry.ts +0 -0
- package/tasks/._release-assets.ts +0 -0
- package/tasks/._release-candidate.ts +0 -0
- package/tasks/._release-manifest.ts +0 -0
- package/tasks/._release-pr.ts +0 -0
- package/tasks/._release-summary.ts +0 -0
- package/tasks/._release-version.ts +0 -0
- package/tasks/._rust.ts +0 -0
- package/tasks/._sync.ts +0 -0
- package/tasks/._uniffi-python.d.ts +0 -0
- package/tasks/._uniffi-python.js +0 -0
- package/tasks/._uniffi-release.mjs +0 -0
- package/tasks/._uniffi.ts +0 -0
- package/tasks/._version-check.ts +0 -0
- package/tasks/bump.ts +17 -34
- package/tasks/local-publish.ts +124 -117
- package/tasks/publish-npm.ts +125 -41
- package/tasks/publish-python.ts +79 -106
- package/tasks/publish-uniffi-local.ts +129 -84
- package/tasks/publish.ts +44 -139
- package/tasks/python-registry.ts +2 -9
- package/tasks/release-assets.ts +56 -65
- package/tasks/release-candidate.ts +416 -0
- package/tasks/release-manifest.ts +273 -0
- package/tasks/release-pr.ts +312 -369
- package/tasks/release-summary.ts +8 -10
- package/tasks/release-version.ts +23 -2
- package/tasks/rust.ts +8 -13
- package/tasks/uniffi-python.d.ts +2 -3
- package/tasks/uniffi-python.js +7 -20
- package/tasks/uniffi-release.mjs +19 -13
- package/tasks/uniffi.ts +15 -24
- package/tasks/version-check.ts +24 -56
- package/src/release-plan.ts +0 -269
- package/src/release-please.ts +0 -242
- package/tasks/release-bootstrap.ts +0 -59
- package/tasks/release-plan.ts +0 -89
- package/tasks/release-please.ts +0 -162
- package/tasks/release-request.ts +0 -166
- package/tasks/release-summary-units.ts +0 -75
- package/tasks/stamp-python.ts +0 -13
package/tasks/release-assets.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { platform } from "node:os";
|
|
16
16
|
import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
17
|
-
import * as exec from "@dbx-tools/core/exec";
|
|
18
17
|
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
19
18
|
import { json, log } from "@dbx-tools/shared-core";
|
|
20
19
|
import { Command } from "commander";
|
|
@@ -23,6 +22,11 @@ import type {
|
|
|
23
22
|
RustReleaseConfiguration,
|
|
24
23
|
RustReleaseTargetPlan,
|
|
25
24
|
} from "../src/_rust-release-workflow.ts";
|
|
25
|
+
import {
|
|
26
|
+
captureTaskCommand,
|
|
27
|
+
runLoggedTaskCommand,
|
|
28
|
+
taskCommandSucceeds,
|
|
29
|
+
} from "../src/_task-command.ts";
|
|
26
30
|
|
|
27
31
|
const logger = log.logger("projen:release-assets");
|
|
28
32
|
const emoji = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
|
|
@@ -39,55 +43,8 @@ interface ReleaseAsset {
|
|
|
39
43
|
readonly size: number;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
command: string,
|
|
45
|
-
args: readonly string[],
|
|
46
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
47
|
-
): void {
|
|
48
|
-
const result = exec.spawnSync(command, [...args], {
|
|
49
|
-
cwd: root,
|
|
50
|
-
env,
|
|
51
|
-
stdin: "ignore",
|
|
52
|
-
stdout: "capture",
|
|
53
|
-
stderr: "capture",
|
|
54
|
-
check: false,
|
|
55
|
-
});
|
|
56
|
-
for (const line of `${result.stdout}\n${result.stderr}`.split(/\r?\n/)) {
|
|
57
|
-
if (line) logger.info(line.replace(emoji, ""));
|
|
58
|
-
}
|
|
59
|
-
if (result.exitCode !== 0) {
|
|
60
|
-
throw new Error(`${command} exited with ${result.exitCode}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function capture(
|
|
65
|
-
root: string,
|
|
66
|
-
command: string,
|
|
67
|
-
args: readonly string[],
|
|
68
|
-
env: NodeJS.ProcessEnv,
|
|
69
|
-
): string {
|
|
70
|
-
return exec.spawnSync(command, [...args], {
|
|
71
|
-
cwd: root,
|
|
72
|
-
env,
|
|
73
|
-
stdin: "ignore",
|
|
74
|
-
stdout: "capture",
|
|
75
|
-
stderr: "inherit",
|
|
76
|
-
check: true,
|
|
77
|
-
}).stdout;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function commandSucceeds(root: string, command: string, args: readonly string[]): boolean {
|
|
81
|
-
return (
|
|
82
|
-
exec.spawnSync(command, [...args], {
|
|
83
|
-
cwd: root,
|
|
84
|
-
env: { ...process.env, PATH: releasePath() },
|
|
85
|
-
stdin: "ignore",
|
|
86
|
-
stdout: "ignore",
|
|
87
|
-
stderr: "ignore",
|
|
88
|
-
check: false,
|
|
89
|
-
}).exitCode === 0
|
|
90
|
-
);
|
|
46
|
+
function logReleaseOutput(line: string): void {
|
|
47
|
+
logger.info(line.replace(emoji, ""));
|
|
91
48
|
}
|
|
92
49
|
|
|
93
50
|
function releasePath(): string {
|
|
@@ -110,7 +67,12 @@ export function missingLocalReleaseTools(root: string): string[] {
|
|
|
110
67
|
["zip", "zip", ["-v"]],
|
|
111
68
|
];
|
|
112
69
|
return checks
|
|
113
|
-
.filter(
|
|
70
|
+
.filter(
|
|
71
|
+
([, command, args]) =>
|
|
72
|
+
!taskCommandSucceeds(root, command, args, {
|
|
73
|
+
env: { ...process.env, PATH: releasePath() },
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
114
76
|
.map(([name]) => name);
|
|
115
77
|
}
|
|
116
78
|
|
|
@@ -205,7 +167,11 @@ function targetBuildEnvironment(
|
|
|
205
167
|
|
|
206
168
|
function cargoTargetRoot(root: string, env: NodeJS.ProcessEnv): string {
|
|
207
169
|
const metadata = json.parseRecord(
|
|
208
|
-
|
|
170
|
+
captureTaskCommand(root, "cargo", ["metadata", "--format-version", "1", "--no-deps"], {
|
|
171
|
+
env,
|
|
172
|
+
check: true,
|
|
173
|
+
stderr: "inherit",
|
|
174
|
+
}),
|
|
209
175
|
);
|
|
210
176
|
if (typeof metadata?.target_directory !== "string") {
|
|
211
177
|
throw new Error("Cargo metadata returned no target directory");
|
|
@@ -214,7 +180,13 @@ function cargoTargetRoot(root: string, env: NodeJS.ProcessEnv): string {
|
|
|
214
180
|
}
|
|
215
181
|
|
|
216
182
|
function rustHostTarget(root: string, env: NodeJS.ProcessEnv): string {
|
|
217
|
-
const host = /^host:\s+(.+)$/m.exec(
|
|
183
|
+
const host = /^host:\s+(.+)$/m.exec(
|
|
184
|
+
captureTaskCommand(root, "rustc", ["-vV"], {
|
|
185
|
+
env,
|
|
186
|
+
check: true,
|
|
187
|
+
stderr: "inherit",
|
|
188
|
+
}),
|
|
189
|
+
)?.[1];
|
|
218
190
|
if (!host) throw new Error("rustc did not report a host target");
|
|
219
191
|
return host;
|
|
220
192
|
}
|
|
@@ -226,7 +198,7 @@ function buildBindingGenerators(
|
|
|
226
198
|
env: NodeJS.ProcessEnv,
|
|
227
199
|
): void {
|
|
228
200
|
if (configuration.bindings.length === 0) return;
|
|
229
|
-
|
|
201
|
+
runLoggedTaskCommand(
|
|
230
202
|
root,
|
|
231
203
|
"cargo",
|
|
232
204
|
[
|
|
@@ -241,7 +213,7 @@ function buildBindingGenerators(
|
|
|
241
213
|
"--target",
|
|
242
214
|
hostTarget,
|
|
243
215
|
],
|
|
244
|
-
env,
|
|
216
|
+
{ env, onLine: logReleaseOutput },
|
|
245
217
|
);
|
|
246
218
|
}
|
|
247
219
|
|
|
@@ -255,7 +227,7 @@ function generatePythonBindings(
|
|
|
255
227
|
for (const binding of configuration.bindings) {
|
|
256
228
|
if (!binding.python) continue;
|
|
257
229
|
const output = join(root, "dist/local-bindings", binding.crate);
|
|
258
|
-
|
|
230
|
+
runLoggedTaskCommand(
|
|
259
231
|
root,
|
|
260
232
|
"node",
|
|
261
233
|
[
|
|
@@ -270,7 +242,7 @@ function generatePythonBindings(
|
|
|
270
242
|
"--output",
|
|
271
243
|
output,
|
|
272
244
|
],
|
|
273
|
-
env,
|
|
245
|
+
{ env, onLine: logReleaseOutput },
|
|
274
246
|
);
|
|
275
247
|
generated.set(binding.crate, join(output, `${binding.crate.replaceAll("-", "_")}.py`));
|
|
276
248
|
}
|
|
@@ -286,7 +258,7 @@ function packageBindings(
|
|
|
286
258
|
env: NodeJS.ProcessEnv,
|
|
287
259
|
): void {
|
|
288
260
|
for (const binding of configuration.bindings) {
|
|
289
|
-
|
|
261
|
+
runLoggedTaskCommand(
|
|
290
262
|
root,
|
|
291
263
|
"node",
|
|
292
264
|
[
|
|
@@ -325,7 +297,7 @@ function packageBindings(
|
|
|
325
297
|
? ["--python-bindings", pythonBindings.get(binding.crate)!]
|
|
326
298
|
: []),
|
|
327
299
|
],
|
|
328
|
-
env,
|
|
300
|
+
{ env, onLine: logReleaseOutput },
|
|
329
301
|
);
|
|
330
302
|
}
|
|
331
303
|
}
|
|
@@ -352,9 +324,15 @@ function packageBinaries(
|
|
|
352
324
|
const output = join(root, "dist/release", binary.crate, target.node, "binary");
|
|
353
325
|
const asset = join(output, releaseBinaryAssetName(binary.binary, target.node, target.os));
|
|
354
326
|
if (target.os === "win32") {
|
|
355
|
-
|
|
327
|
+
runLoggedTaskCommand(root, "zip", ["-j", asset, source], {
|
|
328
|
+
env,
|
|
329
|
+
onLine: logReleaseOutput,
|
|
330
|
+
});
|
|
356
331
|
} else {
|
|
357
|
-
|
|
332
|
+
runLoggedTaskCommand(root, "tar", ["-C", dirname(source), "-czf", asset, basename(source)], {
|
|
333
|
+
env,
|
|
334
|
+
onLine: logReleaseOutput,
|
|
335
|
+
});
|
|
358
336
|
}
|
|
359
337
|
}
|
|
360
338
|
}
|
|
@@ -418,8 +396,15 @@ function uploadAssets(
|
|
|
418
396
|
assets: readonly ReleaseAsset[],
|
|
419
397
|
manifest: string,
|
|
420
398
|
): void {
|
|
421
|
-
|
|
422
|
-
|
|
399
|
+
runLoggedTaskCommand(
|
|
400
|
+
root,
|
|
401
|
+
"gh",
|
|
402
|
+
["release", "upload", tag, ...assets.map((asset) => asset.path), "--clobber"],
|
|
403
|
+
{ onLine: logReleaseOutput },
|
|
404
|
+
);
|
|
405
|
+
runLoggedTaskCommand(root, "gh", ["release", "upload", tag, manifest, "--clobber"], {
|
|
406
|
+
onLine: logReleaseOutput,
|
|
407
|
+
});
|
|
423
408
|
}
|
|
424
409
|
|
|
425
410
|
/** Build all configured targets sequentially and optionally upload them. */
|
|
@@ -443,11 +428,17 @@ export function buildReleaseAssets(options: {
|
|
|
443
428
|
buildBindingGenerators(root, configuration, hostTarget, env);
|
|
444
429
|
const pythonBindings = generatePythonBindings(root, configuration, hostTarget, env);
|
|
445
430
|
for (const target of configuration.targets) {
|
|
446
|
-
|
|
431
|
+
runLoggedTaskCommand(root, "rustup", ["target", "add", target.cargo], {
|
|
432
|
+
env,
|
|
433
|
+
onLine: logReleaseOutput,
|
|
434
|
+
});
|
|
447
435
|
const invocation = localTargetCommand(configuration, target);
|
|
448
436
|
const buildEnv = targetBuildEnvironment(root, target, env);
|
|
449
437
|
logger.info("building Rust release target", { target: target.cargo });
|
|
450
|
-
|
|
438
|
+
runLoggedTaskCommand(root, invocation.command, invocation.args, {
|
|
439
|
+
env: buildEnv,
|
|
440
|
+
onLine: logReleaseOutput,
|
|
441
|
+
});
|
|
451
442
|
packageBindings(root, configuration, target, options.version, pythonBindings, env);
|
|
452
443
|
packageBinaries(root, configuration, target, targetRoot, env);
|
|
453
444
|
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Build and optionally upload the complete locally approved release candidate. */
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
8
|
+
import { json, log } from "@dbx-tools/shared-core";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { readNpmArchiveIdentity } from "./publish-npm.ts";
|
|
11
|
+
import { buildPythonProjects } from "./publish-python.ts";
|
|
12
|
+
import { buildReleaseAssets } from "./release-assets.ts";
|
|
13
|
+
import {
|
|
14
|
+
type ReleaseArtifactInput,
|
|
15
|
+
type ReleaseArtifactRole,
|
|
16
|
+
verifyReleaseManifest,
|
|
17
|
+
writeReleaseManifest,
|
|
18
|
+
} from "./release-manifest.ts";
|
|
19
|
+
import type { RustReleaseConfiguration } from "../src/_rust-release-workflow.ts";
|
|
20
|
+
import { captureTaskCommand, runTaskCommand, taskCommandSucceeds } from "../src/_task-command.ts";
|
|
21
|
+
|
|
22
|
+
const logger = log.logger("projen:release-candidate");
|
|
23
|
+
|
|
24
|
+
function files(directory: string): string[] {
|
|
25
|
+
if (!existsSync(directory)) return [];
|
|
26
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
27
|
+
const path = join(directory, entry.name);
|
|
28
|
+
return entry.isDirectory() ? files(path) : [path];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pythonPackageName(path: string, version: string): string {
|
|
33
|
+
const name = basename(path);
|
|
34
|
+
const marker = `-${version}`;
|
|
35
|
+
const index = name.indexOf(marker);
|
|
36
|
+
if (index <= 0) throw new Error(`Cannot resolve Python package name from ${name}`);
|
|
37
|
+
return name.slice(0, index).replace(/[_.]+/g, "-").toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function role(path: string): ReleaseArtifactRole {
|
|
41
|
+
const normalized = path.split("\\").join("/");
|
|
42
|
+
if (normalized.includes("/npm-facade/")) return "facade";
|
|
43
|
+
if (normalized.includes("/npm-workspace/") || normalized.includes("/python-workspace/")) {
|
|
44
|
+
return "workspace";
|
|
45
|
+
}
|
|
46
|
+
return "native";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function releaseArtifacts(root: string, version: string): ReleaseArtifactInput[] {
|
|
50
|
+
const releaseRoot = join(root, "dist/release");
|
|
51
|
+
return files(releaseRoot)
|
|
52
|
+
.filter((path) => !path.includes(`${join(releaseRoot, "upload")}/`))
|
|
53
|
+
.flatMap((path): ReleaseArtifactInput[] => {
|
|
54
|
+
const normalized = path.split("\\").join("/");
|
|
55
|
+
if (
|
|
56
|
+
![
|
|
57
|
+
"/binary/",
|
|
58
|
+
"/npm/",
|
|
59
|
+
"/npm-facade/",
|
|
60
|
+
"/npm-workspace/",
|
|
61
|
+
"/python/",
|
|
62
|
+
"/python-workspace/",
|
|
63
|
+
].some((directory) => normalized.includes(directory))
|
|
64
|
+
) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
if (path.endsWith(".tgz")) {
|
|
68
|
+
const identity = readNpmArchiveIdentity(path);
|
|
69
|
+
if (identity.version !== version) {
|
|
70
|
+
throw new Error(`${identity.name} carries ${identity.version}, expected ${version}`);
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
kind: "npm",
|
|
75
|
+
packageName: identity.name,
|
|
76
|
+
packageVersion: identity.version,
|
|
77
|
+
path,
|
|
78
|
+
role: role(path),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
path.endsWith(".whl") ||
|
|
84
|
+
(path.endsWith(".tar.gz") &&
|
|
85
|
+
(normalized.includes("/python/") || normalized.includes("/python-workspace/")))
|
|
86
|
+
) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
kind: "pypi",
|
|
90
|
+
packageName: pythonPackageName(path, version),
|
|
91
|
+
packageVersion: version,
|
|
92
|
+
path,
|
|
93
|
+
role: role(path),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
if (path.endsWith(".zip") || path.endsWith(".tar.gz")) {
|
|
98
|
+
return [{ kind: "binary", path }];
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function pruneDraftAssets(
|
|
105
|
+
root: string,
|
|
106
|
+
tag: string,
|
|
107
|
+
desired: ReadonlySet<string>,
|
|
108
|
+
env: NodeJS.ProcessEnv,
|
|
109
|
+
): void {
|
|
110
|
+
const current = captureTaskCommand(
|
|
111
|
+
root,
|
|
112
|
+
"gh",
|
|
113
|
+
["release", "view", tag, "--json", "assets", "--jq", ".assets[].name"],
|
|
114
|
+
{ env, check: true, stderr: "inherit" },
|
|
115
|
+
)
|
|
116
|
+
.split(/\r?\n/)
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
for (const name of current) {
|
|
119
|
+
if (!desired.has(name)) {
|
|
120
|
+
runTaskCommand(root, "gh", ["release", "delete-asset", tag, name, "--yes"], { env });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function rustReleaseConfiguration(root: string): RustReleaseConfiguration | undefined {
|
|
126
|
+
const path = join(root, ".projen/rust-release.json");
|
|
127
|
+
return existsSync(path)
|
|
128
|
+
? (json.parse(readFileSync(path, "utf8")) as RustReleaseConfiguration)
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildFacades(root: string, version: string): void {
|
|
133
|
+
const configuration = rustReleaseConfiguration(root);
|
|
134
|
+
if (!configuration) return;
|
|
135
|
+
for (const binding of configuration.bindings) {
|
|
136
|
+
if (!binding.node || !binding.nodePackage) continue;
|
|
137
|
+
runTaskCommand(root, "node", [
|
|
138
|
+
".projen/uniffi-release.mjs",
|
|
139
|
+
"facade",
|
|
140
|
+
"--node",
|
|
141
|
+
binding.node,
|
|
142
|
+
"--node-package",
|
|
143
|
+
binding.nodePackage,
|
|
144
|
+
"--node-triple",
|
|
145
|
+
"linux-x64-gnu",
|
|
146
|
+
"--version",
|
|
147
|
+
version,
|
|
148
|
+
"--output",
|
|
149
|
+
`dist/release/${binding.crate}/facade`,
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureDraftRelease(options: {
|
|
155
|
+
readonly root: string;
|
|
156
|
+
readonly sha: string;
|
|
157
|
+
readonly tag: string;
|
|
158
|
+
readonly notesFile?: string;
|
|
159
|
+
readonly env: NodeJS.ProcessEnv;
|
|
160
|
+
}): void {
|
|
161
|
+
const existingTag = captureTaskCommand(
|
|
162
|
+
options.root,
|
|
163
|
+
"git",
|
|
164
|
+
["rev-parse", "--verify", `${options.tag}^{commit}`],
|
|
165
|
+
{ env: options.env },
|
|
166
|
+
);
|
|
167
|
+
if (existingTag) {
|
|
168
|
+
if (existingTag !== options.sha) {
|
|
169
|
+
throw new Error(`Release tag ${options.tag} does not point to ${options.sha}`);
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
captureTaskCommand(options.root, "git", ["cat-file", "-t", options.tag], {
|
|
173
|
+
env: options.env,
|
|
174
|
+
check: true,
|
|
175
|
+
stderr: "inherit",
|
|
176
|
+
}) !== "tag"
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(`Release tag ${options.tag} must be annotated`);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
runTaskCommand(
|
|
182
|
+
options.root,
|
|
183
|
+
"git",
|
|
184
|
+
["tag", "-a", options.tag, options.sha, "-m", options.tag],
|
|
185
|
+
{ env: options.env },
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const remoteTag = taskCommandSucceeds(
|
|
189
|
+
options.root,
|
|
190
|
+
"git",
|
|
191
|
+
["ls-remote", "--exit-code", "--tags", "origin", `refs/tags/${options.tag}`],
|
|
192
|
+
{ env: options.env },
|
|
193
|
+
);
|
|
194
|
+
if (remoteTag) {
|
|
195
|
+
runTaskCommand(
|
|
196
|
+
options.root,
|
|
197
|
+
"git",
|
|
198
|
+
["fetch", "--force", "origin", `+refs/tags/${options.tag}:refs/tags/${options.tag}`],
|
|
199
|
+
{ env: options.env },
|
|
200
|
+
);
|
|
201
|
+
if (
|
|
202
|
+
captureTaskCommand(options.root, "git", ["rev-parse", `${options.tag}^{commit}`], {
|
|
203
|
+
env: options.env,
|
|
204
|
+
check: true,
|
|
205
|
+
stderr: "inherit",
|
|
206
|
+
}) !== options.sha
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(`Remote release tag ${options.tag} does not point to ${options.sha}`);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
captureTaskCommand(options.root, "git", ["cat-file", "-t", options.tag], {
|
|
212
|
+
env: options.env,
|
|
213
|
+
check: true,
|
|
214
|
+
stderr: "inherit",
|
|
215
|
+
}) !== "tag"
|
|
216
|
+
) {
|
|
217
|
+
throw new Error(`Remote release tag ${options.tag} must be annotated`);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
runTaskCommand(options.root, "git", ["push", "origin", `refs/tags/${options.tag}`], {
|
|
221
|
+
env: options.env,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const release = captureTaskCommand(
|
|
226
|
+
options.root,
|
|
227
|
+
"gh",
|
|
228
|
+
["release", "view", options.tag, "--json", "isDraft"],
|
|
229
|
+
{ env: options.env },
|
|
230
|
+
);
|
|
231
|
+
if (release) {
|
|
232
|
+
const state = JSON.parse(release) as { isDraft?: boolean };
|
|
233
|
+
if (state.isDraft !== true) {
|
|
234
|
+
throw new Error(`GitHub Release ${options.tag} is already published`);
|
|
235
|
+
}
|
|
236
|
+
if (options.notesFile) {
|
|
237
|
+
runTaskCommand(
|
|
238
|
+
options.root,
|
|
239
|
+
"gh",
|
|
240
|
+
["release", "edit", options.tag, "--title", options.tag, "--notes-file", options.notesFile],
|
|
241
|
+
{ env: options.env },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const notes =
|
|
247
|
+
options.notesFile && existsSync(options.notesFile) ? ["--notes-file", options.notesFile] : [];
|
|
248
|
+
runTaskCommand(
|
|
249
|
+
options.root,
|
|
250
|
+
"gh",
|
|
251
|
+
["release", "create", options.tag, "--draft", "--verify-tag", "--title", options.tag, ...notes],
|
|
252
|
+
{ env: options.env },
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Build the exact release candidate and optionally attach it to a draft release. */
|
|
257
|
+
export function buildReleaseCandidate(options: {
|
|
258
|
+
readonly root: string;
|
|
259
|
+
readonly pythonRoot: string;
|
|
260
|
+
readonly sha: string;
|
|
261
|
+
readonly tag: string;
|
|
262
|
+
readonly version: string;
|
|
263
|
+
readonly notesFile?: string;
|
|
264
|
+
readonly upload?: boolean;
|
|
265
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
266
|
+
}): void {
|
|
267
|
+
const root = resolve(options.root);
|
|
268
|
+
const env = options.env ?? process.env;
|
|
269
|
+
if (
|
|
270
|
+
captureTaskCommand(root, "git", ["rev-parse", "HEAD"], {
|
|
271
|
+
env,
|
|
272
|
+
check: true,
|
|
273
|
+
stderr: "inherit",
|
|
274
|
+
}) !== options.sha
|
|
275
|
+
) {
|
|
276
|
+
throw new Error(`Release candidate must be built from ${options.sha}`);
|
|
277
|
+
}
|
|
278
|
+
if (readFileSync(join(root, "VERSION"), "utf8").trim() !== options.version) {
|
|
279
|
+
throw new Error(`VERSION does not match ${options.version}`);
|
|
280
|
+
}
|
|
281
|
+
const status = captureTaskCommand(
|
|
282
|
+
root,
|
|
283
|
+
"git",
|
|
284
|
+
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
285
|
+
{ env, check: true, stderr: "inherit" },
|
|
286
|
+
);
|
|
287
|
+
if (status) throw new Error("Release candidate source contains tracked changes");
|
|
288
|
+
|
|
289
|
+
rmSync(join(root, "dist/release"), { recursive: true, force: true });
|
|
290
|
+
if ((rustReleaseConfiguration(root)?.targets.length ?? 0) > 0) {
|
|
291
|
+
buildReleaseAssets({ root, version: options.version });
|
|
292
|
+
buildFacades(root, options.version);
|
|
293
|
+
}
|
|
294
|
+
const publishScript = fileURLToPath(new URL("./publish.ts", import.meta.url));
|
|
295
|
+
runTaskCommand(root, process.execPath, [
|
|
296
|
+
publishScript,
|
|
297
|
+
options.version,
|
|
298
|
+
"--output",
|
|
299
|
+
"dist/release/npm-workspace",
|
|
300
|
+
]);
|
|
301
|
+
const pythonRoot = resolve(root, options.pythonRoot);
|
|
302
|
+
if (existsSync(pythonRoot)) {
|
|
303
|
+
buildPythonProjects({
|
|
304
|
+
allowEmpty: true,
|
|
305
|
+
excludeUniFFI: true,
|
|
306
|
+
output: join(root, "dist/release/python-workspace"),
|
|
307
|
+
root: pythonRoot,
|
|
308
|
+
version: options.version,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const uploadDirectory = join(root, "dist/release/upload");
|
|
312
|
+
const manifest = writeReleaseManifest({
|
|
313
|
+
artifacts: releaseArtifacts(root, options.version),
|
|
314
|
+
directory: uploadDirectory,
|
|
315
|
+
gitSha: options.sha,
|
|
316
|
+
tag: options.tag,
|
|
317
|
+
version: options.version,
|
|
318
|
+
});
|
|
319
|
+
logger.success("built release candidate", {
|
|
320
|
+
artifacts: manifest.artifacts.length,
|
|
321
|
+
sha: options.sha,
|
|
322
|
+
tag: options.tag,
|
|
323
|
+
});
|
|
324
|
+
if (!options.upload) return;
|
|
325
|
+
uploadReleaseCandidate(options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Attach an already-built and verified candidate to its draft GitHub Release. */
|
|
329
|
+
export function uploadReleaseCandidate(options: {
|
|
330
|
+
readonly root: string;
|
|
331
|
+
readonly sha: string;
|
|
332
|
+
readonly tag: string;
|
|
333
|
+
readonly version: string;
|
|
334
|
+
readonly notesFile?: string;
|
|
335
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
336
|
+
}): void {
|
|
337
|
+
const root = resolve(options.root);
|
|
338
|
+
const env = options.env ?? process.env;
|
|
339
|
+
const uploadDirectory = join(root, "dist/release/upload");
|
|
340
|
+
verifyReleaseManifest({
|
|
341
|
+
directory: uploadDirectory,
|
|
342
|
+
gitSha: options.sha,
|
|
343
|
+
tag: options.tag,
|
|
344
|
+
version: options.version,
|
|
345
|
+
});
|
|
346
|
+
const notesFile = join(root, "dist/release/release-notes.md");
|
|
347
|
+
const summary =
|
|
348
|
+
options.notesFile && existsSync(resolve(root, options.notesFile))
|
|
349
|
+
? readFileSync(resolve(root, options.notesFile), "utf8").trim()
|
|
350
|
+
: "";
|
|
351
|
+
writeFileSync(
|
|
352
|
+
notesFile,
|
|
353
|
+
[
|
|
354
|
+
`Release commit: \`${options.sha}\``,
|
|
355
|
+
"",
|
|
356
|
+
"Candidate manifest: `release-manifest.json`",
|
|
357
|
+
"",
|
|
358
|
+
"Candidate checksums: `SHA256SUMS`",
|
|
359
|
+
...(summary ? ["", summary] : []),
|
|
360
|
+
"",
|
|
361
|
+
].join("\n"),
|
|
362
|
+
);
|
|
363
|
+
ensureDraftRelease({
|
|
364
|
+
root,
|
|
365
|
+
sha: options.sha,
|
|
366
|
+
tag: options.tag,
|
|
367
|
+
notesFile,
|
|
368
|
+
env,
|
|
369
|
+
});
|
|
370
|
+
const uploadFiles = files(uploadDirectory).sort((left, right) => {
|
|
371
|
+
const priority = (path: string): number =>
|
|
372
|
+
basename(path) === "release-manifest.json" ? 2 : basename(path) === "SHA256SUMS" ? 1 : 0;
|
|
373
|
+
return priority(left) - priority(right) || left.localeCompare(right);
|
|
374
|
+
});
|
|
375
|
+
pruneDraftAssets(root, options.tag, new Set(uploadFiles.map((path) => basename(path))), env);
|
|
376
|
+
runTaskCommand(root, "gh", ["release", "upload", options.tag, ...uploadFiles, "--clobber"], {
|
|
377
|
+
env,
|
|
378
|
+
});
|
|
379
|
+
logger.success(`uploaded draft release candidate ${options.tag}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (import.meta.main) {
|
|
383
|
+
new Command()
|
|
384
|
+
.requiredOption("--version <version>", "release version")
|
|
385
|
+
.requiredOption("--tag <tag>", "annotated release tag")
|
|
386
|
+
.requiredOption("--sha <sha>", "exact release commit")
|
|
387
|
+
.option("--root <path>", "repository root")
|
|
388
|
+
.option("--python-root <path>", "Python package root", "packages/py")
|
|
389
|
+
.option("--notes-file <path>", "draft release notes file")
|
|
390
|
+
.option("--upload", "create or update the draft GitHub Release after building")
|
|
391
|
+
.option("--upload-existing", "upload the existing verified candidate without rebuilding")
|
|
392
|
+
.action(
|
|
393
|
+
(options: {
|
|
394
|
+
version: string;
|
|
395
|
+
tag: string;
|
|
396
|
+
sha: string;
|
|
397
|
+
root?: string;
|
|
398
|
+
pythonRoot: string;
|
|
399
|
+
notesFile?: string;
|
|
400
|
+
upload?: boolean;
|
|
401
|
+
uploadExisting?: boolean;
|
|
402
|
+
}) => {
|
|
403
|
+
const candidateOptions = {
|
|
404
|
+
root: options.root ?? projectUtils.root() ?? process.cwd(),
|
|
405
|
+
pythonRoot: options.pythonRoot,
|
|
406
|
+
sha: options.sha,
|
|
407
|
+
tag: options.tag,
|
|
408
|
+
version: options.version,
|
|
409
|
+
...(options.notesFile ? { notesFile: options.notesFile } : {}),
|
|
410
|
+
};
|
|
411
|
+
if (options.uploadExisting) uploadReleaseCandidate(candidateOptions);
|
|
412
|
+
else buildReleaseCandidate({ ...candidateOptions, upload: options.upload });
|
|
413
|
+
},
|
|
414
|
+
)
|
|
415
|
+
.parse();
|
|
416
|
+
}
|