@dbx-tools/projen 0.9.4 → 0.9.6
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 +64 -16
- package/index.ts +7 -5
- package/package.json +9 -6
- package/src/_rust-project.ts +356 -0
- package/src/_rust-release-workflow.ts +901 -0
- package/src/_rust-release.ts +605 -0
- package/src/_task-command.ts +89 -0
- package/src/barrels.ts +9 -7
- package/src/engine-root.ts +5 -5
- package/src/openapi.ts +1 -1
- package/src/packages.ts +9 -9
- package/src/pnpm-workspace.ts +1 -1
- package/src/project-js.ts +19 -25
- package/src/project-py.ts +6 -6
- package/src/project-rs.ts +100 -1098
- package/src/projen-version.ts +9 -0
- package/src/publish.ts +3 -4
- package/src/release-catalog.ts +1 -1
- package/src/release.ts +11 -12
- package/src/scaffold.ts +1 -1
- package/src/uniffi.ts +7 -0
- package/src/watch.ts +2 -2
- package/src/workspace-version.ts +20 -15
- package/tasks/barrels.ts +4 -4
- package/tasks/build-rust-release.ts +68 -0
- package/tasks/bump.ts +3 -2
- package/tasks/clean.ts +3 -3
- package/tasks/local-publish.ts +21 -3
- package/tasks/openapi.ts +2 -2
- package/tasks/publish-npm.ts +12 -6
- package/tasks/publish-python.ts +2 -2
- package/tasks/publish-uniffi-local.ts +11 -15
- package/tasks/publish.ts +88 -88
- package/tasks/python-registry.ts +1 -1
- package/tasks/release-bootstrap.ts +2 -2
- package/tasks/release-plan.ts +1 -1
- package/tasks/release-please.ts +1 -1
- package/tasks/release-pr.ts +60 -115
- package/tasks/release-request.ts +8 -29
- package/tasks/release-summary-units.ts +2 -9
- package/tasks/release-summary.ts +35 -24
- package/tasks/release-version.ts +44 -0
- package/tasks/rust-release.d.ts +41 -0
- package/tasks/rust-release.mjs +1717 -0
- package/tasks/uniffi-python.js +1 -1
- package/tasks/uniffi.ts +7 -1
- package/tasks/version-check.ts +6 -5
package/tasks/publish.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
/**
|
|
3
|
-
* `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]`
|
|
3
|
+
* `bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]`
|
|
4
4
|
* - verify every workspace member and the Bun lock carry the release version,
|
|
5
|
-
* then publish each package owned by the standard Node release
|
|
5
|
+
* then pack and publish each package owned by the standard Node release.
|
|
6
6
|
*
|
|
7
7
|
* Bun has no `pnpm -r publish`, so this loop is the recursive-publish stand-in.
|
|
8
8
|
* It leans on native bun for everything bun already does:
|
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
* its `#!/usr/bin/env node` shebang, node chokes on the `.ts`
|
|
27
27
|
* (ERR_UNKNOWN_FILE_EXTENSION). We merge `publishConfig` onto the top-level
|
|
28
28
|
* manifest before packing so the tarball advertises the compiled `lib/` tree;
|
|
29
|
-
* - **compiled output** is emitted once, before
|
|
29
|
+
* - **compiled output** is emitted once, before packing, by one root-level
|
|
30
30
|
* filtered `bun run` that fans out to every publishable member in parallel.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `prepack`. Packages retain
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* Every package is then packed once with lifecycle scripts disabled. The
|
|
32
|
+
* exact validated archive is passed to `bun publish`, so upload never
|
|
33
|
+
* repacks or repeats a member's `prepack`. Packages retain `prepack` for
|
|
34
|
+
* standalone publishes.
|
|
35
|
+
* - **release recovery** compares each packed archive's integrity and
|
|
36
|
+
* repository identity with registry metadata. A matching immutable version
|
|
37
|
+
* is skipped, while any mismatch fails the retry.
|
|
37
38
|
*
|
|
38
39
|
* `--dry-run` forwards to `bun publish`: it packs + validates
|
|
39
40
|
* but uploads nothing, so the `release` workflow is testable end-to-end via a
|
|
@@ -63,8 +64,7 @@ import {
|
|
|
63
64
|
} from "node:fs";
|
|
64
65
|
import { tmpdir } from "node:os";
|
|
65
66
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
66
|
-
import {
|
|
67
|
-
import { log } from "@dbx-tools/shared-core";
|
|
67
|
+
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
68
68
|
import ts from "typescript";
|
|
69
69
|
import { parse } from "yaml";
|
|
70
70
|
import {
|
|
@@ -73,9 +73,11 @@ import {
|
|
|
73
73
|
publishedNpmRelease,
|
|
74
74
|
readNpmArchiveIdentity,
|
|
75
75
|
} from "./publish-npm.ts";
|
|
76
|
+
import { runTaskCommand, runTaskCommandAsync } from "../src/_task-command.ts";
|
|
77
|
+
import { toPosix } from "../src/packages.ts";
|
|
76
78
|
import type { ReleasePlan } from "../src/release-plan.ts";
|
|
77
79
|
|
|
78
|
-
const logger = log.logger("
|
|
80
|
+
const logger = log.logger("projen:publish");
|
|
79
81
|
|
|
80
82
|
/** A manifest's on-disk bytes + mode, captured before this task edits it. */
|
|
81
83
|
interface ManifestBackup {
|
|
@@ -115,7 +117,7 @@ function manifestsMatchVersions(
|
|
|
115
117
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
116
118
|
version?: string;
|
|
117
119
|
};
|
|
118
|
-
const path = dir.slice(resolve(root).length + 1)
|
|
120
|
+
const path = toPosix(dir.slice(resolve(root).length + 1));
|
|
119
121
|
return pkg.version === expected.get(path);
|
|
120
122
|
});
|
|
121
123
|
}
|
|
@@ -134,10 +136,7 @@ function lockfileMatchesManifestVersions(root: string, members: readonly string[
|
|
|
134
136
|
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
135
137
|
version?: string;
|
|
136
138
|
};
|
|
137
|
-
const relative = dir
|
|
138
|
-
.slice(resolve(root).length + 1)
|
|
139
|
-
.split("\\")
|
|
140
|
-
.join("/");
|
|
139
|
+
const relative = toPosix(dir.slice(resolve(root).length + 1));
|
|
141
140
|
return lock.workspaces?.[relative]?.version === manifest.version;
|
|
142
141
|
});
|
|
143
142
|
} catch {
|
|
@@ -145,51 +144,6 @@ function lockfileMatchesManifestVersions(root: string, members: readonly string[
|
|
|
145
144
|
}
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
/** Spawn `command` in `cwd` with `PATH` overridden, failing the task on non-zero. */
|
|
149
|
-
function run(cwd: string, command: string, args: string[], path: string): void {
|
|
150
|
-
const executable = command === "bun" && process.versions.bun ? process.execPath : command;
|
|
151
|
-
exec.spawnSync(executable, args, {
|
|
152
|
-
cwd,
|
|
153
|
-
stdout: "inherit",
|
|
154
|
-
stderr: "inherit",
|
|
155
|
-
stdin: "ignore",
|
|
156
|
-
check: true,
|
|
157
|
-
env: { ...process.env, PATH: path },
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Asynchronous counterpart used for bounded parallel stamping and publishing. */
|
|
162
|
-
async function runAsync(cwd: string, command: string, args: string[], path: string): Promise<void> {
|
|
163
|
-
const executable = command === "bun" && process.versions.bun ? process.execPath : command;
|
|
164
|
-
await exec.spawn(executable, args, {
|
|
165
|
-
cwd,
|
|
166
|
-
stdout: "inherit",
|
|
167
|
-
stderr: "inherit",
|
|
168
|
-
stdin: "ignore",
|
|
169
|
-
check: true,
|
|
170
|
-
env: { ...process.env, PATH: path },
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Run independent jobs with a small fixed worker pool. */
|
|
175
|
-
async function runConcurrent<T>(
|
|
176
|
-
values: readonly T[],
|
|
177
|
-
concurrency: number,
|
|
178
|
-
worker: (value: T) => Promise<void>,
|
|
179
|
-
): Promise<void> {
|
|
180
|
-
let next = 0;
|
|
181
|
-
const results = await Promise.allSettled(
|
|
182
|
-
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
183
|
-
while (next < values.length) {
|
|
184
|
-
const value = values[next++];
|
|
185
|
-
await worker(value);
|
|
186
|
-
}
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
const failure = results.find((result) => result.status === "rejected");
|
|
190
|
-
if (failure?.status === "rejected") throw failure.reason;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
147
|
/**
|
|
194
148
|
* Make a projen-readonly manifest writable so the publishConfig projection can
|
|
195
149
|
* edit it, recording its original bytes and mode for {@link restoreManifests}
|
|
@@ -228,6 +182,14 @@ function restoreManifests(): void {
|
|
|
228
182
|
/** Entry-point fields projen writes as `.ts` source in-repo and rewrites to `lib/` for publish. */
|
|
229
183
|
const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
|
|
230
184
|
|
|
185
|
+
/** Compiled files referenced by a publishConfig entry-point tree. */
|
|
186
|
+
function compiledPublishTargets(value: unknown): string[] {
|
|
187
|
+
if (typeof value === "string") return value.startsWith("./lib/") ? [value] : [];
|
|
188
|
+
if (Array.isArray(value)) return value.flatMap(compiledPublishTargets);
|
|
189
|
+
if (!value || typeof value !== "object") return [];
|
|
190
|
+
return Object.values(value).flatMap(compiledPublishTargets);
|
|
191
|
+
}
|
|
192
|
+
|
|
231
193
|
/**
|
|
232
194
|
* Fold a package's `publishConfig` entry-point fields onto the top-level manifest,
|
|
233
195
|
* the way pnpm/npm do at pack time but `bun publish` does NOT (see the module
|
|
@@ -272,13 +234,14 @@ const plan =
|
|
|
272
234
|
: undefined;
|
|
273
235
|
if (!version && !plan) {
|
|
274
236
|
logger.error(
|
|
275
|
-
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run]",
|
|
237
|
+
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run] [--skip-compile]",
|
|
276
238
|
);
|
|
277
239
|
process.exit(1);
|
|
278
240
|
}
|
|
279
241
|
const registryIdx = rest.indexOf("--registry");
|
|
280
242
|
const registry = registryIdx >= 0 ? rest[registryIdx + 1] : undefined;
|
|
281
243
|
const dryRun = rest.includes("--dry-run");
|
|
244
|
+
const skipCompile = rest.includes("--skip-compile");
|
|
282
245
|
const concurrencyIdx = rest.indexOf("--concurrency");
|
|
283
246
|
const parsedConcurrency = Number(concurrencyIdx >= 0 ? rest[concurrencyIdx + 1] : 4);
|
|
284
247
|
if (!Number.isInteger(parsedConcurrency) || parsedConcurrency < 1) {
|
|
@@ -302,15 +265,10 @@ const allMembers = workspaceMembers(root)
|
|
|
302
265
|
const expectedVersions = new Map(
|
|
303
266
|
plan
|
|
304
267
|
? plan.nodePackages.map((pkg) => [pkg.path, pkg.version] as const)
|
|
305
|
-
: allMembers.map((dir) => [
|
|
306
|
-
dir.slice(resolve(root).length + 1).replaceAll("\\", "/"),
|
|
307
|
-
version!,
|
|
308
|
-
]),
|
|
268
|
+
: allMembers.map((dir) => [toPosix(dir.slice(resolve(root).length + 1)), version!]),
|
|
309
269
|
);
|
|
310
270
|
const members = plan
|
|
311
|
-
? allMembers.filter((dir) =>
|
|
312
|
-
expectedVersions.has(dir.slice(resolve(root).length + 1).replaceAll("\\", "/")),
|
|
313
|
-
)
|
|
271
|
+
? allMembers.filter((dir) => expectedVersions.has(toPosix(dir.slice(resolve(root).length + 1))))
|
|
314
272
|
: allMembers;
|
|
315
273
|
|
|
316
274
|
if (!manifestsMatchVersions(root, members, expectedVersions)) {
|
|
@@ -328,12 +286,10 @@ if (lockfileMatchesManifestVersions(root, allMembers)) {
|
|
|
328
286
|
} else {
|
|
329
287
|
if (existsSync(lockfile)) rmSync(lockfile);
|
|
330
288
|
logger.info("refreshing lockfile so workspace deps resolve to the release version");
|
|
331
|
-
|
|
289
|
+
runTaskCommand(root, "bun", ["install"], { env: { ...process.env, PATH: path } });
|
|
332
290
|
}
|
|
333
291
|
|
|
334
292
|
const publishArgs = [
|
|
335
|
-
"--access",
|
|
336
|
-
"public",
|
|
337
293
|
"--ignore-scripts",
|
|
338
294
|
...(registry ? ["--registry", registry] : []),
|
|
339
295
|
...(dryRun ? ["--dry-run"] : []),
|
|
@@ -343,6 +299,8 @@ const publishable: Array<{
|
|
|
343
299
|
name: string;
|
|
344
300
|
version: string;
|
|
345
301
|
compile: boolean;
|
|
302
|
+
compiledTargets: string[];
|
|
303
|
+
access?: "public" | "restricted";
|
|
346
304
|
}> = [];
|
|
347
305
|
for (const dir of members) {
|
|
348
306
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
@@ -350,7 +308,7 @@ for (const dir of members) {
|
|
|
350
308
|
version?: string;
|
|
351
309
|
private?: boolean;
|
|
352
310
|
dbxToolsConfig?: { uniffi?: boolean };
|
|
353
|
-
|
|
311
|
+
publishConfig?: { access?: unknown };
|
|
354
312
|
};
|
|
355
313
|
if (pkg.private) {
|
|
356
314
|
logger.info(`skip private ${pkg.name ?? dirname(dir)}`);
|
|
@@ -361,18 +319,46 @@ for (const dir of members) {
|
|
|
361
319
|
continue;
|
|
362
320
|
}
|
|
363
321
|
if (!pkg.version) throw new Error(`Missing package version for ${pkg.name ?? dirname(dir)}`);
|
|
322
|
+
const access = pkg.publishConfig?.access;
|
|
323
|
+
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`${pkg.name ?? dirname(dir)} has invalid publishConfig.access ${String(access)}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const compiledTargets = [...new Set(compiledPublishTargets(pkg.publishConfig))];
|
|
364
329
|
publishable.push({
|
|
365
330
|
dir,
|
|
366
331
|
name: pkg.name ?? dirname(dir),
|
|
367
332
|
version: pkg.version,
|
|
368
|
-
compile:
|
|
333
|
+
compile: compiledTargets.length > 0,
|
|
334
|
+
compiledTargets,
|
|
335
|
+
...(access ? { access } : {}),
|
|
369
336
|
});
|
|
370
337
|
}
|
|
371
338
|
|
|
372
339
|
const compiled = publishable.filter((pkg) => pkg.compile);
|
|
373
340
|
if (compiled.length > 0) {
|
|
374
|
-
|
|
375
|
-
|
|
341
|
+
if (skipCompile) {
|
|
342
|
+
const missing = compiled.flatMap((pkg) =>
|
|
343
|
+
pkg.compiledTargets
|
|
344
|
+
.filter((target) => !existsSync(resolve(pkg.dir, target)))
|
|
345
|
+
.map((target) => `${pkg.name}:${target}`),
|
|
346
|
+
);
|
|
347
|
+
if (missing.length > 0) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
`--skip-compile requires validated compiled output; missing ${missing.join(", ")}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
logger.info(`reusing validated compiled output for ${compiled.length} publishable packages`);
|
|
353
|
+
} else {
|
|
354
|
+
logger.info(`compiling ${compiled.length} publishable packages from the workspace root`);
|
|
355
|
+
runTaskCommand(
|
|
356
|
+
root,
|
|
357
|
+
"bun",
|
|
358
|
+
["run", ...compiled.flatMap((pkg) => ["--filter", pkg.name]), "compile"],
|
|
359
|
+
{ env: { ...process.env, PATH: path } },
|
|
360
|
+
);
|
|
361
|
+
}
|
|
376
362
|
}
|
|
377
363
|
|
|
378
364
|
// Compile against workspace source exports first. Switching manifests to their
|
|
@@ -387,9 +373,10 @@ for (const { dir } of publishable) {
|
|
|
387
373
|
logger.info(
|
|
388
374
|
`${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
|
|
389
375
|
);
|
|
390
|
-
await
|
|
391
|
-
|
|
392
|
-
|
|
376
|
+
await asyncUtils.mapConcurrent(
|
|
377
|
+
publishable,
|
|
378
|
+
async ({ dir, name, version: packageVersion, access }) => {
|
|
379
|
+
const packed = mkdtempSync(join(tmpdir(), "projen-npm-release-"));
|
|
393
380
|
try {
|
|
394
381
|
const archive = packNpmPackage(dir, packed, path);
|
|
395
382
|
const local = readNpmArchiveIdentity(archive);
|
|
@@ -398,16 +385,29 @@ await runConcurrent(publishable, concurrency, async ({ dir, name, version: packa
|
|
|
398
385
|
`Packed npm identity ${local.name}@${local.version} does not match ${name}@${packageVersion}`,
|
|
399
386
|
);
|
|
400
387
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
388
|
+
if (local.access !== access) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Packed npm access ${String(local.access)} does not match ${String(access)} for ${name}`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (!dryRun) {
|
|
394
|
+
const published = await publishedNpmRelease(local.name, local.version, registry);
|
|
395
|
+
if (npmReleaseMatches(local, published)) {
|
|
396
|
+
logger.info(`skip published ${name} @ ${packageVersion}`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
405
399
|
}
|
|
400
|
+
logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${packageVersion}`);
|
|
401
|
+
await runTaskCommandAsync(
|
|
402
|
+
dir,
|
|
403
|
+
"bun",
|
|
404
|
+
["publish", ...(access ? ["--access", access] : []), ...publishArgs, archive],
|
|
405
|
+
{ env: { ...process.env, PATH: path } },
|
|
406
|
+
);
|
|
406
407
|
} finally {
|
|
407
408
|
rmSync(packed, { recursive: true, force: true });
|
|
408
409
|
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
});
|
|
410
|
+
},
|
|
411
|
+
{ concurrency, errorMode: "settle" },
|
|
412
|
+
);
|
|
413
413
|
logger.success(`${dryRun ? "dry-run: packed" : "published"} ${publishable.length} packages`);
|
package/tasks/python-registry.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
5
5
|
import { net } from "@dbx-tools/shared-core";
|
|
6
6
|
|
|
7
7
|
export interface LocalPythonRegistry {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
7
7
|
import { json, log } from "@dbx-tools/shared-core";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
@@ -53,7 +53,7 @@ if (import.meta.main) {
|
|
|
53
53
|
new Command()
|
|
54
54
|
.option("--root <path>", "repository root")
|
|
55
55
|
.action((options: { root?: string }) => {
|
|
56
|
-
bootstrapReleaseUnits(resolve(options.root ??
|
|
56
|
+
bootstrapReleaseUnits(resolve(options.root ?? projectUtils.root() ?? process.cwd()));
|
|
57
57
|
})
|
|
58
58
|
.parse();
|
|
59
59
|
}
|
package/tasks/release-plan.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
7
|
import { json, log } from "@dbx-tools/shared-core";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
package/tasks/release-please.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { appendFileSync, readFileSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
7
|
import { log } from "@dbx-tools/shared-core";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { GitHub } from "release-please";
|
package/tasks/release-pr.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import
|
|
14
|
-
import
|
|
13
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
14
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
15
|
+
import { asyncUtils, json, log, object } from "@dbx-tools/shared-core";
|
|
15
16
|
import { Command } from "commander";
|
|
16
17
|
import { publishLocalRelease } from "./local-publish.ts";
|
|
17
18
|
import { generateReleaseSummary } from "./release-summary.ts";
|
|
@@ -23,6 +24,16 @@ import {
|
|
|
23
24
|
type ReleaseOs,
|
|
24
25
|
type VersionLevel,
|
|
25
26
|
} from "../src/_release-platform.ts";
|
|
27
|
+
import {
|
|
28
|
+
captureTaskCommand,
|
|
29
|
+
runTaskCommand,
|
|
30
|
+
runTaskCommandAsync,
|
|
31
|
+
taskCommandSucceeds,
|
|
32
|
+
} from "../src/_task-command.ts";
|
|
33
|
+
import {
|
|
34
|
+
RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
35
|
+
type ReleaseSummaryProviderName,
|
|
36
|
+
} from "../src/release-dispatch.ts";
|
|
26
37
|
import {
|
|
27
38
|
githubAccountSupportsWorkflowChanges,
|
|
28
39
|
githubAuthenticatedAccounts,
|
|
@@ -32,10 +43,6 @@ import {
|
|
|
32
43
|
githubTokenArguments,
|
|
33
44
|
githubTokenEnvironmentName,
|
|
34
45
|
} from "../src/release-github.ts";
|
|
35
|
-
import {
|
|
36
|
-
RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
37
|
-
type ReleaseSummaryProviderName,
|
|
38
|
-
} from "../src/release-dispatch.ts";
|
|
39
46
|
import {
|
|
40
47
|
readWorkspaceVersion,
|
|
41
48
|
resolveBaseVersion,
|
|
@@ -49,14 +56,9 @@ function git(
|
|
|
49
56
|
args: string[],
|
|
50
57
|
{ capture = false, check = true }: { capture?: boolean; check?: boolean } = {},
|
|
51
58
|
): string {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
stderr: capture ? "ignore" : "inherit",
|
|
56
|
-
stdin: "ignore",
|
|
57
|
-
check,
|
|
58
|
-
});
|
|
59
|
-
return result.stdout?.trim() ?? "";
|
|
59
|
+
if (capture) return captureTaskCommand(root, "git", args, { check });
|
|
60
|
+
runTaskCommand(root, "git", args);
|
|
61
|
+
return "";
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function pushCurrentBranch(root: string, branch: string): void {
|
|
@@ -68,84 +70,7 @@ function pushCurrentBranch(root: string, branch: string): void {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function gitSucceeds(root: string, args: string[]): boolean {
|
|
71
|
-
return (
|
|
72
|
-
exec.spawnSync("git", args, {
|
|
73
|
-
cwd: root,
|
|
74
|
-
stdout: "ignore",
|
|
75
|
-
stderr: "ignore",
|
|
76
|
-
stdin: "ignore",
|
|
77
|
-
check: false,
|
|
78
|
-
}).exitCode === 0
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function commandSucceeds(
|
|
83
|
-
root: string,
|
|
84
|
-
command: string,
|
|
85
|
-
args: string[],
|
|
86
|
-
env?: NodeJS.ProcessEnv,
|
|
87
|
-
): boolean {
|
|
88
|
-
return (
|
|
89
|
-
exec.spawnSync(command, args, {
|
|
90
|
-
cwd: root,
|
|
91
|
-
env,
|
|
92
|
-
stdout: "ignore",
|
|
93
|
-
stderr: "ignore",
|
|
94
|
-
stdin: "ignore",
|
|
95
|
-
check: false,
|
|
96
|
-
}).exitCode === 0
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function run(root: string, command: string, args: string[], env?: NodeJS.ProcessEnv): void {
|
|
101
|
-
exec.spawnSync(command, args, {
|
|
102
|
-
cwd: root,
|
|
103
|
-
env,
|
|
104
|
-
stdout: "inherit",
|
|
105
|
-
stderr: "inherit",
|
|
106
|
-
stdin: "ignore",
|
|
107
|
-
check: true,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Run one release command with inherited output and a hard timeout. */
|
|
112
|
-
async function runWithTimeout(
|
|
113
|
-
root: string,
|
|
114
|
-
command: string,
|
|
115
|
-
args: string[],
|
|
116
|
-
env: NodeJS.ProcessEnv,
|
|
117
|
-
timeoutMs: number,
|
|
118
|
-
): Promise<void> {
|
|
119
|
-
await exec.spawn(command, args, {
|
|
120
|
-
cwd: root,
|
|
121
|
-
env,
|
|
122
|
-
stdout: "inherit",
|
|
123
|
-
stderr: "inherit",
|
|
124
|
-
stdin: "ignore",
|
|
125
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
126
|
-
check: true,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Capture one non-interactive command without throwing on a nonzero exit. */
|
|
131
|
-
function captureCommand(
|
|
132
|
-
root: string,
|
|
133
|
-
command: string,
|
|
134
|
-
args: string[],
|
|
135
|
-
env: NodeJS.ProcessEnv,
|
|
136
|
-
): string {
|
|
137
|
-
return (
|
|
138
|
-
exec
|
|
139
|
-
.spawnSync(command, args, {
|
|
140
|
-
cwd: root,
|
|
141
|
-
env,
|
|
142
|
-
stdout: "capture",
|
|
143
|
-
stderr: "ignore",
|
|
144
|
-
stdin: "ignore",
|
|
145
|
-
check: false,
|
|
146
|
-
})
|
|
147
|
-
.stdout?.trim() ?? ""
|
|
148
|
-
);
|
|
73
|
+
return taskCommandSucceeds(root, "git", args);
|
|
149
74
|
}
|
|
150
75
|
|
|
151
76
|
/** Wait for required checks and return the merged pull request commit. */
|
|
@@ -155,16 +80,15 @@ async function waitForPullRequestMerge(
|
|
|
155
80
|
env: NodeJS.ProcessEnv,
|
|
156
81
|
timeoutMs: number,
|
|
157
82
|
): Promise<string> {
|
|
158
|
-
await
|
|
83
|
+
await runTaskCommandAsync(
|
|
159
84
|
root,
|
|
160
85
|
"gh",
|
|
161
86
|
["pr", "checks", releaseBranch, "--watch", "--fail-fast", "--required", "--interval", "10"],
|
|
162
|
-
env,
|
|
163
|
-
timeoutMs,
|
|
87
|
+
{ env, signal: AbortSignal.timeout(timeoutMs) },
|
|
164
88
|
);
|
|
165
89
|
const deadline = Date.now() + timeoutMs;
|
|
166
90
|
while (Date.now() < deadline) {
|
|
167
|
-
const state =
|
|
91
|
+
const state = captureTaskCommand(
|
|
168
92
|
root,
|
|
169
93
|
"gh",
|
|
170
94
|
[
|
|
@@ -176,12 +100,12 @@ async function waitForPullRequestMerge(
|
|
|
176
100
|
"--jq",
|
|
177
101
|
'[.state, (.mergeCommit.oid // "")] | @tsv',
|
|
178
102
|
],
|
|
179
|
-
env,
|
|
103
|
+
{ env },
|
|
180
104
|
);
|
|
181
105
|
const [status, mergeSha] = state.split("\t");
|
|
182
106
|
if (status === "MERGED" && mergeSha) return mergeSha;
|
|
183
107
|
if (status === "CLOSED") throw new Error(`Release pull request closed without merging`);
|
|
184
|
-
await
|
|
108
|
+
await asyncUtils.sleep(2_000);
|
|
185
109
|
}
|
|
186
110
|
throw new Error(`Release pull request did not merge within ${timeoutMs}ms`);
|
|
187
111
|
}
|
|
@@ -197,7 +121,7 @@ async function waitForReleaseWorkflow(
|
|
|
197
121
|
const deadline = Date.now() + timeoutMs;
|
|
198
122
|
let runId = "";
|
|
199
123
|
while (Date.now() < deadline) {
|
|
200
|
-
runId =
|
|
124
|
+
runId = captureTaskCommand(
|
|
201
125
|
root,
|
|
202
126
|
"gh",
|
|
203
127
|
[
|
|
@@ -218,13 +142,16 @@ async function waitForReleaseWorkflow(
|
|
|
218
142
|
"--jq",
|
|
219
143
|
".[0].databaseId // empty",
|
|
220
144
|
],
|
|
221
|
-
env,
|
|
145
|
+
{ env },
|
|
222
146
|
);
|
|
223
147
|
if (runId) break;
|
|
224
|
-
await
|
|
148
|
+
await asyncUtils.sleep(5_000);
|
|
225
149
|
}
|
|
226
150
|
if (!runId) throw new Error(`Release workflow did not start within ${timeoutMs}ms`);
|
|
227
|
-
await
|
|
151
|
+
await runTaskCommandAsync(root, "gh", ["run", "watch", runId, "--exit-status"], {
|
|
152
|
+
env,
|
|
153
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
154
|
+
});
|
|
228
155
|
}
|
|
229
156
|
|
|
230
157
|
function githubAccount(root: string): {
|
|
@@ -234,7 +161,7 @@ function githubAccount(root: string): {
|
|
|
234
161
|
login: string;
|
|
235
162
|
token: string;
|
|
236
163
|
} {
|
|
237
|
-
const repository =
|
|
164
|
+
const repository = projectUtils.repositoryUrl(root);
|
|
238
165
|
if (!repository) throw new Error("Release preparation requires a GitHub repository");
|
|
239
166
|
const identity = githubRepositoryIdentity(repository);
|
|
240
167
|
const status = exec
|
|
@@ -375,7 +302,7 @@ program
|
|
|
375
302
|
wait: boolean;
|
|
376
303
|
waitTimeoutMinutes: string;
|
|
377
304
|
}) => {
|
|
378
|
-
const root =
|
|
305
|
+
const root = projectUtils.root() ?? process.cwd();
|
|
379
306
|
const waitTimeoutMinutes = object.toNumber(opts.waitTimeoutMinutes, {
|
|
380
307
|
separators: false,
|
|
381
308
|
percent: false,
|
|
@@ -397,6 +324,15 @@ program
|
|
|
397
324
|
includeComponentTags: false,
|
|
398
325
|
}).version;
|
|
399
326
|
const next = resolveNextVersion(root, [opts.prefix], opts.level, { fetch: false });
|
|
327
|
+
const releaseVersionScript = fileURLToPath(new URL("./release-version.ts", import.meta.url));
|
|
328
|
+
runTaskCommand(root, process.execPath, [
|
|
329
|
+
releaseVersionScript,
|
|
330
|
+
"--version",
|
|
331
|
+
next.version,
|
|
332
|
+
"--prefix",
|
|
333
|
+
opts.prefix,
|
|
334
|
+
"--assert-next",
|
|
335
|
+
]);
|
|
400
336
|
const releaseTag = `${opts.prefix}${next.version}`;
|
|
401
337
|
const releaseBranch = `release/${releaseTag}`;
|
|
402
338
|
const releaseRoot = join(root, ".worktrees", releaseTag);
|
|
@@ -454,11 +390,11 @@ program
|
|
|
454
390
|
}
|
|
455
391
|
logger.info(`resuming ${releaseBranch} in ${releaseRoot}`);
|
|
456
392
|
}
|
|
457
|
-
|
|
393
|
+
runTaskCommand(releaseRoot, process.execPath, ["install"]);
|
|
458
394
|
|
|
459
395
|
const bumpScript = fileURLToPath(new URL("./bump.ts", import.meta.url));
|
|
460
396
|
const versionCheckScript = fileURLToPath(new URL("./version-check.ts", import.meta.url));
|
|
461
|
-
|
|
397
|
+
runTaskCommand(releaseRoot, process.execPath, [
|
|
462
398
|
bumpScript,
|
|
463
399
|
"--level",
|
|
464
400
|
opts.level,
|
|
@@ -471,19 +407,19 @@ program
|
|
|
471
407
|
throw new Error(`Release preparation did not produce ${next.version}`);
|
|
472
408
|
}
|
|
473
409
|
|
|
474
|
-
|
|
410
|
+
runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
|
|
475
411
|
if (opts.validate) {
|
|
476
412
|
for (const task of opts.validateTask) {
|
|
477
|
-
|
|
413
|
+
runTaskCommand(releaseRoot, process.execPath, ["run", task]);
|
|
478
414
|
}
|
|
479
415
|
if (existsSync(join(releaseRoot, "Cargo.toml"))) {
|
|
480
|
-
|
|
416
|
+
runTaskCommand(releaseRoot, "cargo", [
|
|
481
417
|
"test",
|
|
482
418
|
"--workspace",
|
|
483
419
|
...(existsSync(join(releaseRoot, "Cargo.lock")) ? ["--locked"] : []),
|
|
484
420
|
]);
|
|
485
421
|
}
|
|
486
|
-
|
|
422
|
+
runTaskCommand(releaseRoot, process.execPath, ["run", "compile"]);
|
|
487
423
|
} else {
|
|
488
424
|
logger.warn("release validation skipped by --no-validate");
|
|
489
425
|
}
|
|
@@ -495,11 +431,12 @@ program
|
|
|
495
431
|
localPypi: opts.localPypi,
|
|
496
432
|
pythonRoot: opts.pythonRoot,
|
|
497
433
|
localCargo: opts.localCargo,
|
|
434
|
+
reuseValidatedNodeCompile: opts.validate,
|
|
498
435
|
});
|
|
499
436
|
} else {
|
|
500
437
|
logger.info("local publication skipped by --no-local-publish");
|
|
501
438
|
}
|
|
502
|
-
|
|
439
|
+
runTaskCommand(releaseRoot, process.execPath, [versionCheckScript]);
|
|
503
440
|
const releaseSummary = opts.releaseSummary
|
|
504
441
|
? await generateReleaseSummary({
|
|
505
442
|
root: releaseRoot,
|
|
@@ -537,8 +474,14 @@ program
|
|
|
537
474
|
[githubTokenEnvironmentName(account.hostname)]: account.token,
|
|
538
475
|
};
|
|
539
476
|
const ensurePullRequest = (): void => {
|
|
540
|
-
if (
|
|
541
|
-
|
|
477
|
+
if (
|
|
478
|
+
taskCommandSucceeds(root, "gh", ["pr", "view", releaseBranch], {
|
|
479
|
+
env: githubEnvironment,
|
|
480
|
+
})
|
|
481
|
+
) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
runTaskCommand(
|
|
542
485
|
root,
|
|
543
486
|
"gh",
|
|
544
487
|
[
|
|
@@ -553,12 +496,14 @@ program
|
|
|
553
496
|
"--body",
|
|
554
497
|
body,
|
|
555
498
|
],
|
|
556
|
-
githubEnvironment,
|
|
499
|
+
{ env: githubEnvironment },
|
|
557
500
|
);
|
|
558
501
|
};
|
|
559
502
|
ensurePullRequest();
|
|
560
503
|
if (opts.approve) {
|
|
561
|
-
|
|
504
|
+
runTaskCommand(root, "gh", ["pr", "merge", releaseBranch, "--auto", "--merge"], {
|
|
505
|
+
env: githubEnvironment,
|
|
506
|
+
});
|
|
562
507
|
}
|
|
563
508
|
git(root, ["worktree", "remove", "--force", releaseRoot]);
|
|
564
509
|
git(root, ["branch", "--delete", "--force", releaseBranch]);
|