@dbx-tools/projen 0.6.228 → 0.6.230
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 +54 -85
- package/index.ts +9 -1
- package/package.json +8 -5
- package/src/openapi.ts +9 -1
- package/src/project-js.ts +65 -10
- package/src/project-py.ts +133 -4
- package/src/project-rs.ts +392 -25
- package/src/release-catalog.ts +832 -0
- package/src/release-dispatch.ts +3 -2
- package/src/release-plan.ts +236 -0
- package/src/release-please.ts +242 -0
- package/src/release.ts +385 -0
- package/tasks/publish-npm.ts +4 -4
- package/tasks/publish-python.ts +56 -14
- package/tasks/publish.ts +60 -30
- package/tasks/release-bootstrap.ts +59 -0
- package/tasks/release-plan.ts +89 -0
- package/tasks/release-please.ts +163 -0
- package/tasks/release-summary-units.ts +44 -0
- package/tasks/release-summary.ts +33 -7
- package/tasks/uniffi-python.d.ts +5 -0
- package/tasks/uniffi-python.js +21 -2
- package/tasks/uniffi-release.mjs +8 -3
- package/tasks/version-check.ts +43 -15
package/tasks/publish.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
publishedNpmRelease,
|
|
74
74
|
readNpmArchiveIdentity,
|
|
75
75
|
} from "./publish-npm.ts";
|
|
76
|
+
import type { ReleasePlan } from "../src/release-plan.ts";
|
|
76
77
|
|
|
77
78
|
const logger = log.logger("dbx-tools:publish");
|
|
78
79
|
|
|
@@ -104,22 +105,23 @@ function workspaceMembers(root: string): string[] {
|
|
|
104
105
|
return (doc?.packages ?? []).map((m) => resolve(root, m));
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
/** Whether
|
|
108
|
-
function
|
|
108
|
+
/** Whether selected workspace members carry their planned versions. */
|
|
109
|
+
function manifestsMatchVersions(
|
|
110
|
+
root: string,
|
|
111
|
+
members: readonly string[],
|
|
112
|
+
expected: ReadonlyMap<string, string>,
|
|
113
|
+
): boolean {
|
|
109
114
|
return members.every((dir) => {
|
|
110
115
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
111
116
|
version?: string;
|
|
112
117
|
};
|
|
113
|
-
|
|
118
|
+
const path = dir.slice(resolve(root).length + 1).replaceAll("\\", "/");
|
|
119
|
+
return pkg.version === expected.get(path);
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
/** Whether Bun's workspace lock records
|
|
118
|
-
function
|
|
119
|
-
root: string,
|
|
120
|
-
members: readonly string[],
|
|
121
|
-
version: string,
|
|
122
|
-
): boolean {
|
|
123
|
+
/** Whether Bun's workspace lock records each live workspace manifest version. */
|
|
124
|
+
function lockfileMatchesManifestVersions(root: string, members: readonly string[]): boolean {
|
|
123
125
|
const lockfile = join(root, "bun.lock");
|
|
124
126
|
if (!existsSync(lockfile)) return false;
|
|
125
127
|
try {
|
|
@@ -129,11 +131,14 @@ function lockfileMatchesVersion(
|
|
|
129
131
|
workspaces?: Record<string, { version?: string }>;
|
|
130
132
|
};
|
|
131
133
|
return members.every((dir) => {
|
|
134
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
135
|
+
version?: string;
|
|
136
|
+
};
|
|
132
137
|
const relative = dir
|
|
133
138
|
.slice(resolve(root).length + 1)
|
|
134
139
|
.split("\\")
|
|
135
140
|
.join("/");
|
|
136
|
-
return lock.workspaces?.[relative]?.version === version;
|
|
141
|
+
return lock.workspaces?.[relative]?.version === manifest.version;
|
|
137
142
|
});
|
|
138
143
|
} catch {
|
|
139
144
|
return false;
|
|
@@ -256,10 +261,18 @@ function enrichedPath(root: string): string {
|
|
|
256
261
|
return current.split(delimiter).includes(binDir) ? current : `${binDir}${delimiter}${current}`;
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
const
|
|
260
|
-
|
|
264
|
+
const argv = process.argv.slice(2);
|
|
265
|
+
const version = argv[0] && !argv[0].startsWith("--") ? argv[0] : undefined;
|
|
266
|
+
const rest = version ? argv.slice(1) : argv;
|
|
267
|
+
const root = process.cwd();
|
|
268
|
+
const planIdx = rest.indexOf("--plan");
|
|
269
|
+
const plan =
|
|
270
|
+
planIdx >= 0
|
|
271
|
+
? (JSON.parse(readFileSync(resolve(root, rest[planIdx + 1]), "utf8")) as ReleasePlan)
|
|
272
|
+
: undefined;
|
|
273
|
+
if (!version && !plan) {
|
|
261
274
|
logger.error(
|
|
262
|
-
"usage: bun tasks/publish.ts <version> [--registry <url>] [--exclude <dir>] [--dry-run]",
|
|
275
|
+
"usage: bun tasks/publish.ts <version> [--plan <path>] [--registry <url>] [--exclude <dir>] [--dry-run]",
|
|
263
276
|
);
|
|
264
277
|
process.exit(1);
|
|
265
278
|
}
|
|
@@ -278,29 +291,40 @@ const excluded = new Set(
|
|
|
278
291
|
rest.reduce<string[]>((acc, arg, i) => (arg === "--exclude" ? [...acc, rest[i + 1]] : acc), []),
|
|
279
292
|
);
|
|
280
293
|
|
|
281
|
-
const root = process.cwd();
|
|
282
294
|
const path = enrichedPath(root);
|
|
283
295
|
// Registered BEFORE the first manifest edit so no exit path can skip it: a clean
|
|
284
296
|
// finish and a `bun publish` failure mid-loop both land here, so a publish that
|
|
285
297
|
// dies partway does not leave half the workspace stamped.
|
|
286
298
|
if (restore) process.on("exit", restoreManifests);
|
|
287
|
-
const
|
|
299
|
+
const allMembers = workspaceMembers(root)
|
|
288
300
|
.filter((dir) => existsSync(join(dir, "package.json")))
|
|
289
301
|
.filter((dir) => !excluded.has(resolve(root, dir).replace(`${resolve(root)}/`, "")));
|
|
302
|
+
const expectedVersions = new Map(
|
|
303
|
+
plan
|
|
304
|
+
? 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
|
+
]),
|
|
309
|
+
);
|
|
310
|
+
const members = plan
|
|
311
|
+
? allMembers.filter((dir) =>
|
|
312
|
+
expectedVersions.has(dir.slice(resolve(root).length + 1).replaceAll("\\", "/")),
|
|
313
|
+
)
|
|
314
|
+
: allMembers;
|
|
290
315
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
throw new Error(`workspace manifests do not all match release version ${version}; run projen`);
|
|
316
|
+
if (!manifestsMatchVersions(root, members, expectedVersions)) {
|
|
317
|
+
throw new Error("workspace manifests do not match the reviewed release plan; run projen");
|
|
294
318
|
}
|
|
295
|
-
logger.info(`
|
|
319
|
+
logger.info(`validated ${members.length} selected member manifests`);
|
|
296
320
|
|
|
297
321
|
// Ensure the lockfile resolves each `workspace:*` to the release version.
|
|
298
322
|
// `bun publish`/`pm pack` reads workspace versions from the LOCKFILE, not just
|
|
299
323
|
// live manifests. A normal bump's synth/install already makes it current; after
|
|
300
324
|
// fallback stamping, deleting it before install is what forces re-resolution.
|
|
301
325
|
const lockfile = join(root, "bun.lock");
|
|
302
|
-
if (
|
|
303
|
-
logger.info(
|
|
326
|
+
if (lockfileMatchesManifestVersions(root, allMembers)) {
|
|
327
|
+
logger.info("workspace lock already resolves live member versions");
|
|
304
328
|
} else {
|
|
305
329
|
if (existsSync(lockfile)) rmSync(lockfile);
|
|
306
330
|
logger.info("refreshing lockfile so workspace deps resolve to the release version");
|
|
@@ -314,10 +338,16 @@ const publishArgs = [
|
|
|
314
338
|
...(registry ? ["--registry", registry] : []),
|
|
315
339
|
...(dryRun ? ["--dry-run"] : []),
|
|
316
340
|
];
|
|
317
|
-
const publishable: Array<{
|
|
341
|
+
const publishable: Array<{
|
|
342
|
+
dir: string;
|
|
343
|
+
name: string;
|
|
344
|
+
version: string;
|
|
345
|
+
compile: boolean;
|
|
346
|
+
}> = [];
|
|
318
347
|
for (const dir of members) {
|
|
319
348
|
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
|
320
349
|
name?: string;
|
|
350
|
+
version?: string;
|
|
321
351
|
private?: boolean;
|
|
322
352
|
dbxToolsConfig?: { uniffi?: boolean };
|
|
323
353
|
scripts?: Record<string, string>;
|
|
@@ -330,9 +360,11 @@ for (const dir of members) {
|
|
|
330
360
|
logger.info(`skip UniFFI ${pkg.name ?? dirname(dir)}`);
|
|
331
361
|
continue;
|
|
332
362
|
}
|
|
363
|
+
if (!pkg.version) throw new Error(`Missing package version for ${pkg.name ?? dirname(dir)}`);
|
|
333
364
|
publishable.push({
|
|
334
365
|
dir,
|
|
335
366
|
name: pkg.name ?? dirname(dir),
|
|
367
|
+
version: pkg.version,
|
|
336
368
|
compile: Boolean(pkg.scripts && typeof pkg.scripts === "object" && "prepack" in pkg.scripts),
|
|
337
369
|
});
|
|
338
370
|
}
|
|
@@ -355,29 +387,27 @@ for (const { dir } of publishable) {
|
|
|
355
387
|
logger.info(
|
|
356
388
|
`${dryRun ? "dry-run packing" : "publishing"} ${publishable.length} packages with concurrency ${concurrency}`,
|
|
357
389
|
);
|
|
358
|
-
await runConcurrent(publishable, concurrency, async ({ dir, name }) => {
|
|
390
|
+
await runConcurrent(publishable, concurrency, async ({ dir, name, version: packageVersion }) => {
|
|
359
391
|
if (!dryRun) {
|
|
360
392
|
const packed = mkdtempSync(join(tmpdir(), "dbx-tools-npm-release-"));
|
|
361
393
|
try {
|
|
362
394
|
const archive = packNpmPackage(dir, packed, path);
|
|
363
395
|
const local = readNpmArchiveIdentity(archive);
|
|
364
|
-
if (local.name !== name || local.version !==
|
|
396
|
+
if (local.name !== name || local.version !== packageVersion) {
|
|
365
397
|
throw new Error(
|
|
366
|
-
`Packed npm identity ${local.name}@${local.version} does not match ${name}@${
|
|
398
|
+
`Packed npm identity ${local.name}@${local.version} does not match ${name}@${packageVersion}`,
|
|
367
399
|
);
|
|
368
400
|
}
|
|
369
401
|
const published = await publishedNpmRelease(local.name, local.version, registry);
|
|
370
402
|
if (npmReleaseMatches(local, published)) {
|
|
371
|
-
logger.info(`skip published ${name} @ ${
|
|
403
|
+
logger.info(`skip published ${name} @ ${packageVersion}`);
|
|
372
404
|
return;
|
|
373
405
|
}
|
|
374
406
|
} finally {
|
|
375
407
|
rmSync(packed, { recursive: true, force: true });
|
|
376
408
|
}
|
|
377
409
|
}
|
|
378
|
-
logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${
|
|
410
|
+
logger.info(`${dryRun ? "dry-run publishing" : "publishing"} ${name} @ ${packageVersion}`);
|
|
379
411
|
await runAsync(dir, "bun", ["publish", ...publishArgs], path);
|
|
380
412
|
});
|
|
381
|
-
logger.success(
|
|
382
|
-
`${dryRun ? "dry-run: packed" : "published"} ${publishable.length} packages @ ${version}`,
|
|
383
|
-
);
|
|
413
|
+
logger.success(`${dryRun ? "dry-run: packed" : "published"} ${publishable.length} packages`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Bootstrap Release Please state from the generated release-unit graph. */
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { project } from "@dbx-tools/core";
|
|
7
|
+
import { json, log } from "@dbx-tools/shared-core";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
10
|
+
|
|
11
|
+
const logger = log.logger("projen:release-bootstrap");
|
|
12
|
+
|
|
13
|
+
function readGraph(root: string): ReleaseUnitGraph {
|
|
14
|
+
const path = join(root, ".projen/release-units.json");
|
|
15
|
+
const graph = json.parse(readFileSync(path, "utf8")) as ReleaseUnitGraph;
|
|
16
|
+
if (graph.schemaVersion !== 1) {
|
|
17
|
+
throw new Error(`Unsupported release-unit graph schema: ${String(graph.schemaVersion)}`);
|
|
18
|
+
}
|
|
19
|
+
return graph;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function bootstrapReleaseUnits(root: string): void {
|
|
23
|
+
const graph = readGraph(root);
|
|
24
|
+
const manifestPath = join(root, ".release-please-manifest.json");
|
|
25
|
+
const expected = Object.fromEntries(
|
|
26
|
+
graph.units.map((unit) => [`.release-units/${unit.component}`, unit.version]),
|
|
27
|
+
);
|
|
28
|
+
if (existsSync(manifestPath)) {
|
|
29
|
+
const current = json.parseRecord(readFileSync(manifestPath, "utf8"));
|
|
30
|
+
for (const [path, version] of Object.entries(expected)) {
|
|
31
|
+
if (current?.[path] !== version) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Release Please manifest already contains ${path}=${String(current?.[path])}; expected ${version}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
writeFileSync(manifestPath, `${JSON.stringify(expected, null, 2)}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const unit of graph.units) {
|
|
42
|
+
const directory = join(root, ".release-units", unit.component);
|
|
43
|
+
mkdirSync(directory, { recursive: true });
|
|
44
|
+
const versionPath = join(directory, "version.txt");
|
|
45
|
+
if (!existsSync(versionPath)) writeFileSync(versionPath, `${unit.version}\n`);
|
|
46
|
+
const changelogPath = join(directory, "CHANGELOG.md");
|
|
47
|
+
if (!existsSync(changelogPath)) writeFileSync(changelogPath, "# Changelog\n");
|
|
48
|
+
}
|
|
49
|
+
logger.success(`bootstrapped ${graph.units.length} release units`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (import.meta.main) {
|
|
53
|
+
new Command()
|
|
54
|
+
.option("--root <path>", "repository root")
|
|
55
|
+
.action((options: { root?: string }) => {
|
|
56
|
+
bootstrapReleaseUnits(resolve(options.root ?? project.root() ?? process.cwd()));
|
|
57
|
+
})
|
|
58
|
+
.parse();
|
|
59
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Build the affected release plan for the reviewed manifest change. */
|
|
3
|
+
|
|
4
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { exec } from "@dbx-tools/core";
|
|
7
|
+
import { json, log } from "@dbx-tools/shared-core";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
10
|
+
import { buildRecoveryReleasePlan, buildReleasePlan } from "../src/release-plan.ts";
|
|
11
|
+
|
|
12
|
+
const logger = log.logger("projen:release-plan");
|
|
13
|
+
|
|
14
|
+
function graphAtRef(root: string, ref: string): ReleaseUnitGraph | undefined {
|
|
15
|
+
const result = exec.spawnSync("git", ["show", `${ref}:.projen/release-units.json`], {
|
|
16
|
+
cwd: root,
|
|
17
|
+
stdout: "capture",
|
|
18
|
+
stderr: "ignore",
|
|
19
|
+
stdin: "ignore",
|
|
20
|
+
check: false,
|
|
21
|
+
});
|
|
22
|
+
if (result.exitCode !== 0 || !result.stdout) return undefined;
|
|
23
|
+
return json.parse(result.stdout) as ReleaseUnitGraph;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeOutput(name: string, value: unknown): void {
|
|
27
|
+
const output = process.env.GITHUB_OUTPUT;
|
|
28
|
+
if (output) {
|
|
29
|
+
const rendered = typeof value === "string" ? value : JSON.stringify(value);
|
|
30
|
+
appendFileSync(output, `${name}=${rendered}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
new Command()
|
|
35
|
+
.option("--root <path>", "repository root", ".")
|
|
36
|
+
.option("--base-ref <ref>", "previous release graph ref", "HEAD^")
|
|
37
|
+
.option("--component <component>", "component to recover")
|
|
38
|
+
.option("--version <version>", "component version to recover")
|
|
39
|
+
.option("--docs", "include documentation without a package release")
|
|
40
|
+
.option("--output <path>", "release plan output", "dist/release-plan.json")
|
|
41
|
+
.action(
|
|
42
|
+
(options: {
|
|
43
|
+
root: string;
|
|
44
|
+
baseRef: string;
|
|
45
|
+
output: string;
|
|
46
|
+
component?: string;
|
|
47
|
+
version?: string;
|
|
48
|
+
docs?: boolean;
|
|
49
|
+
}) => {
|
|
50
|
+
const root = resolve(options.root);
|
|
51
|
+
const current = json.parse(
|
|
52
|
+
readFileSync(resolve(root, ".projen/release-units.json"), "utf8"),
|
|
53
|
+
) as ReleaseUnitGraph;
|
|
54
|
+
if (Boolean(options.component) !== Boolean(options.version)) {
|
|
55
|
+
throw new Error("Recovery requires both --component and --version");
|
|
56
|
+
}
|
|
57
|
+
const planned =
|
|
58
|
+
options.component && options.version
|
|
59
|
+
? buildRecoveryReleasePlan(current, options.component, options.version)
|
|
60
|
+
: buildReleasePlan(current, graphAtRef(root, options.baseRef));
|
|
61
|
+
const plan = options.docs
|
|
62
|
+
? {
|
|
63
|
+
...planned,
|
|
64
|
+
stages: { ...planned.stages, docs: true },
|
|
65
|
+
omittedStages: planned.omittedStages.filter((stage) => stage !== "docs"),
|
|
66
|
+
}
|
|
67
|
+
: planned;
|
|
68
|
+
const output = resolve(root, options.output);
|
|
69
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
70
|
+
writeFileSync(output, `${JSON.stringify(plan, null, 2)}\n`);
|
|
71
|
+
writeOutput("plan", plan);
|
|
72
|
+
writeOutput("release_sha", process.env.GITHUB_SHA ?? "");
|
|
73
|
+
writeOutput("units", plan.units);
|
|
74
|
+
writeOutput("node_packages", plan.nodePackages);
|
|
75
|
+
writeOutput("python_packages", plan.pythonPackages);
|
|
76
|
+
writeOutput("rust_packages", plan.rustPackages);
|
|
77
|
+
writeOutput("artifacts", plan.artifacts);
|
|
78
|
+
writeOutput("rust_targets", plan.rustTargets);
|
|
79
|
+
writeOutput("stages", plan.stages);
|
|
80
|
+
for (const [stage, enabled] of Object.entries(plan.stages)) {
|
|
81
|
+
writeOutput(stage, enabled);
|
|
82
|
+
}
|
|
83
|
+
logger.success("release plan generated", {
|
|
84
|
+
units: plan.units.length,
|
|
85
|
+
omittedStages: plan.omittedStages,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
.parse();
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Run Release Please with the generated cross-language release-unit plugin. */
|
|
3
|
+
|
|
4
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { exec } from "@dbx-tools/core";
|
|
7
|
+
import { log } from "@dbx-tools/shared-core";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { GitHub } from "release-please";
|
|
10
|
+
import { Manifest } from "release-please/build/src/manifest.js";
|
|
11
|
+
import type { Scm } from "release-please/build/src/scm.js";
|
|
12
|
+
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
13
|
+
import { addReleaseUnitPlugin } from "../src/release-please.ts";
|
|
14
|
+
|
|
15
|
+
const logger = log.logger("projen:release-please");
|
|
16
|
+
|
|
17
|
+
export interface ReleasePleaseRunOptions {
|
|
18
|
+
readonly github: Scm;
|
|
19
|
+
readonly targetBranch: string;
|
|
20
|
+
readonly graph: ReleaseUnitGraph;
|
|
21
|
+
readonly configFile?: string;
|
|
22
|
+
readonly manifestFile?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Create component releases and refresh the combined release pull request. */
|
|
26
|
+
export async function runReleasePlease(options: ReleasePleaseRunOptions) {
|
|
27
|
+
const configFile = options.configFile ?? "release-please-config.json";
|
|
28
|
+
const manifestFile = options.manifestFile ?? ".release-please-manifest.json";
|
|
29
|
+
const manifest = await Manifest.fromManifest(
|
|
30
|
+
options.github,
|
|
31
|
+
options.targetBranch,
|
|
32
|
+
configFile,
|
|
33
|
+
manifestFile,
|
|
34
|
+
);
|
|
35
|
+
addReleaseUnitPlugin(
|
|
36
|
+
manifest.plugins,
|
|
37
|
+
options.github,
|
|
38
|
+
options.targetBranch,
|
|
39
|
+
manifest.repositoryConfig,
|
|
40
|
+
options.graph,
|
|
41
|
+
manifestFile,
|
|
42
|
+
);
|
|
43
|
+
const releases = (await manifest.createReleases()).filter(
|
|
44
|
+
(release): release is NonNullable<typeof release> => release !== undefined,
|
|
45
|
+
);
|
|
46
|
+
const pullRequests = (await manifest.createPullRequests()).filter(
|
|
47
|
+
(pullRequest): pullRequest is NonNullable<typeof pullRequest> => pullRequest !== undefined,
|
|
48
|
+
);
|
|
49
|
+
return { releases, pullRequests };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeOutput(name: string, value: unknown): void {
|
|
53
|
+
const output = process.env.GITHUB_OUTPUT;
|
|
54
|
+
if (!output) return;
|
|
55
|
+
appendFileSync(output, `${name}=${JSON.stringify(value)}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parse GitHub owner and repository from HTTPS, SSH, or SSH-alias syntax. */
|
|
59
|
+
export function parseGitHubRepository(remote: string): { owner: string; repo: string } {
|
|
60
|
+
const location = remote
|
|
61
|
+
.replace(/^git@[^:]+:/, "")
|
|
62
|
+
.replace(/^ssh:\/\/git@[^/]+\//, "")
|
|
63
|
+
.replace(/^https?:\/\/[^/]+\//, "")
|
|
64
|
+
.replace(/\.git$/, "");
|
|
65
|
+
const [owner, repo, ...extra] = location.split("/");
|
|
66
|
+
if (!owner || !repo || extra.length > 0) {
|
|
67
|
+
throw new Error(`Could not resolve GitHub repository from ${remote}`);
|
|
68
|
+
}
|
|
69
|
+
return { owner, repo };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve GitHub owner and repository from environment or the configured remote. */
|
|
73
|
+
export function repositoryCoordinates(root: string): { owner: string; repo: string } {
|
|
74
|
+
const configured = process.env.GITHUB_REPOSITORY;
|
|
75
|
+
const remote = configured
|
|
76
|
+
? configured
|
|
77
|
+
: (exec
|
|
78
|
+
.spawnSync("git", ["remote", "get-url", "origin"], {
|
|
79
|
+
cwd: root,
|
|
80
|
+
stdout: "capture",
|
|
81
|
+
stderr: "ignore",
|
|
82
|
+
stdin: "ignore",
|
|
83
|
+
check: true,
|
|
84
|
+
})
|
|
85
|
+
.stdout?.trim() ?? "");
|
|
86
|
+
return parseGitHubRepository(remote);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function githubToken(): string {
|
|
90
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
91
|
+
return (
|
|
92
|
+
exec
|
|
93
|
+
.spawnSync("gh", ["auth", "token"], {
|
|
94
|
+
stdout: "capture",
|
|
95
|
+
stderr: "inherit",
|
|
96
|
+
stdin: "ignore",
|
|
97
|
+
check: true,
|
|
98
|
+
})
|
|
99
|
+
.stdout?.trim() ?? ""
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (import.meta.main) {
|
|
104
|
+
await new Command()
|
|
105
|
+
.option("--owner <owner>", "GitHub repository owner")
|
|
106
|
+
.option("--repo <repo>", "GitHub repository name")
|
|
107
|
+
.option("--target-branch <branch>", "release target branch", "main")
|
|
108
|
+
.option("--root <path>", "repository root", ".")
|
|
109
|
+
.option("--config-file <path>", "Release Please config", "release-please-config.json")
|
|
110
|
+
.option(
|
|
111
|
+
"--manifest-file <path>",
|
|
112
|
+
"Release Please version manifest",
|
|
113
|
+
".release-please-manifest.json",
|
|
114
|
+
)
|
|
115
|
+
.option("--approve", "compatibility alias for the Release Please release pass")
|
|
116
|
+
.action(
|
|
117
|
+
async (options: {
|
|
118
|
+
owner?: string;
|
|
119
|
+
repo?: string;
|
|
120
|
+
targetBranch: string;
|
|
121
|
+
root: string;
|
|
122
|
+
configFile: string;
|
|
123
|
+
manifestFile: string;
|
|
124
|
+
}) => {
|
|
125
|
+
const root = resolve(options.root);
|
|
126
|
+
const repository =
|
|
127
|
+
options.owner && options.repo
|
|
128
|
+
? { owner: options.owner, repo: options.repo }
|
|
129
|
+
: repositoryCoordinates(root);
|
|
130
|
+
const graph = JSON.parse(
|
|
131
|
+
readFileSync(resolve(root, ".projen/release-units.json"), "utf8"),
|
|
132
|
+
) as ReleaseUnitGraph;
|
|
133
|
+
const github = await GitHub.create({
|
|
134
|
+
owner: repository.owner,
|
|
135
|
+
repo: repository.repo,
|
|
136
|
+
defaultBranch: options.targetBranch,
|
|
137
|
+
token: githubToken(),
|
|
138
|
+
...(process.env.GITHUB_API_URL ? { apiUrl: process.env.GITHUB_API_URL } : {}),
|
|
139
|
+
...(process.env.GITHUB_GRAPHQL_URL
|
|
140
|
+
? {
|
|
141
|
+
graphqlUrl: process.env.GITHUB_GRAPHQL_URL.replace(/\/graphql\/?$/, ""),
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
144
|
+
});
|
|
145
|
+
const result = await runReleasePlease({
|
|
146
|
+
github,
|
|
147
|
+
targetBranch: options.targetBranch,
|
|
148
|
+
graph,
|
|
149
|
+
configFile: options.configFile,
|
|
150
|
+
manifestFile: options.manifestFile,
|
|
151
|
+
});
|
|
152
|
+
logger.success("release state refreshed", {
|
|
153
|
+
releases: result.releases.length,
|
|
154
|
+
pullRequests: result.pullRequests.length,
|
|
155
|
+
});
|
|
156
|
+
writeOutput("releases_created", result.releases.length > 0);
|
|
157
|
+
writeOutput("prs_created", result.pullRequests.length > 0);
|
|
158
|
+
writeOutput("releases", result.releases);
|
|
159
|
+
writeOutput("prs", result.pullRequests);
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
.parseAsync();
|
|
163
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Generate component-qualified release notes from an affected release plan. */
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { log } from "@dbx-tools/shared-core";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { generateReleaseSummary } from "./release-summary.ts";
|
|
9
|
+
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
10
|
+
import type { ReleasePlan } from "../src/release-plan.ts";
|
|
11
|
+
|
|
12
|
+
const logger = log.logger("projen:release-summary-units");
|
|
13
|
+
|
|
14
|
+
if (import.meta.main) {
|
|
15
|
+
await new Command()
|
|
16
|
+
.option("--root <path>", "repository root", ".")
|
|
17
|
+
.option("--plan <path>", "affected release plan", "dist/release-plan.json")
|
|
18
|
+
.option("--from-ref <ref>", "summary base ref", "origin/main")
|
|
19
|
+
.option("--to-ref <ref>", "summary target ref", "HEAD")
|
|
20
|
+
.action(async (options: { root: string; plan: string; fromRef: string; toRef: string }) => {
|
|
21
|
+
const root = resolve(options.root);
|
|
22
|
+
const plan = JSON.parse(readFileSync(resolve(root, options.plan), "utf8")) as ReleasePlan;
|
|
23
|
+
const graph = JSON.parse(
|
|
24
|
+
readFileSync(resolve(root, ".projen/release-units.json"), "utf8"),
|
|
25
|
+
) as ReleaseUnitGraph;
|
|
26
|
+
for (const planned of plan.units) {
|
|
27
|
+
const unit = graph.units.find((candidate) => candidate.id === planned.id);
|
|
28
|
+
if (!unit) throw new Error(`Release plan references unknown unit ${planned.id}`);
|
|
29
|
+
const paths = graph.projects
|
|
30
|
+
.filter((project) => project.unit === unit.id)
|
|
31
|
+
.map((project) => project.path);
|
|
32
|
+
await generateReleaseSummary({
|
|
33
|
+
root,
|
|
34
|
+
component: planned.component,
|
|
35
|
+
version: planned.newVersion,
|
|
36
|
+
paths,
|
|
37
|
+
fromRef: options.fromRef,
|
|
38
|
+
toRef: options.toRef,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
logger.success(`generated ${plan.units.length} component release summaries`);
|
|
42
|
+
})
|
|
43
|
+
.parseAsync();
|
|
44
|
+
}
|
package/tasks/release-summary.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
} from "../src/release-dispatch.ts";
|
|
16
16
|
|
|
17
17
|
const logger = log.logger("projen:release-summary");
|
|
18
|
+
const PROVIDER_TIMEOUT_MS = 60_000;
|
|
19
|
+
const PROVIDER_KILL_GRACE_MS = 5_000;
|
|
18
20
|
|
|
19
21
|
export interface ReleaseSummaryProvider {
|
|
20
22
|
readonly name: ReleaseSummaryProviderName;
|
|
@@ -132,7 +134,7 @@ async function runProvider(
|
|
|
132
134
|
if (!capture(root, provider.command, [...provider.probeArgs])) return undefined;
|
|
133
135
|
let summary: string | undefined;
|
|
134
136
|
const textOutput: string[] = [];
|
|
135
|
-
const
|
|
137
|
+
const child = exec.spawn(provider.command, provider.args(root, prompt), {
|
|
136
138
|
cwd: root,
|
|
137
139
|
stdout: [
|
|
138
140
|
"capture",
|
|
@@ -164,7 +166,21 @@ async function runProvider(
|
|
|
164
166
|
stdin: "ignore",
|
|
165
167
|
check: false,
|
|
166
168
|
});
|
|
167
|
-
|
|
169
|
+
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
170
|
+
const timeout = setTimeout(() => {
|
|
171
|
+
logger.warn("provider-timeout", {
|
|
172
|
+
provider: provider.name,
|
|
173
|
+
timeoutMs: PROVIDER_TIMEOUT_MS,
|
|
174
|
+
});
|
|
175
|
+
child.kill("SIGTERM");
|
|
176
|
+
escalation = setTimeout(() => child.kill("SIGKILL"), PROVIDER_KILL_GRACE_MS);
|
|
177
|
+
escalation.unref();
|
|
178
|
+
}, PROVIDER_TIMEOUT_MS);
|
|
179
|
+
timeout.unref();
|
|
180
|
+
const result = await child.finally(() => {
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
if (escalation) clearTimeout(escalation);
|
|
183
|
+
});
|
|
168
184
|
if (result.exitCode !== 0) {
|
|
169
185
|
logger.warn("provider-failed", { provider: provider.name, exitCode: result.exitCode });
|
|
170
186
|
return undefined;
|
|
@@ -247,12 +263,14 @@ function gitSummary(version: string, commits: string, changedFiles: string): str
|
|
|
247
263
|
export async function generateReleaseSummary(options: {
|
|
248
264
|
readonly root: string;
|
|
249
265
|
readonly version: string;
|
|
266
|
+
readonly component?: string;
|
|
267
|
+
readonly paths?: readonly string[];
|
|
250
268
|
readonly fromRef?: string;
|
|
251
269
|
readonly toRef?: string;
|
|
252
270
|
readonly providers?: readonly ReleaseSummaryProviderName[];
|
|
253
271
|
readonly runner?: ReleaseSummaryRunner;
|
|
254
272
|
}): Promise<string | undefined> {
|
|
255
|
-
const relativeOutput = releaseSummaryFile(options.version);
|
|
273
|
+
const relativeOutput = releaseSummaryFile(options.version, options.component);
|
|
256
274
|
const output = join(options.root, relativeOutput);
|
|
257
275
|
if (existsSync(output)) rmSync(output);
|
|
258
276
|
const target =
|
|
@@ -263,13 +281,21 @@ export async function generateReleaseSummary(options: {
|
|
|
263
281
|
options.fromRef && capture(options.root, "git", ["rev-parse", "--verify", options.fromRef])
|
|
264
282
|
? `${options.fromRef}..${target}`
|
|
265
283
|
: target;
|
|
266
|
-
const
|
|
284
|
+
const scopedPaths = options.paths?.length ? [...options.paths] : ["."];
|
|
285
|
+
const commits = capture(options.root, "git", [
|
|
286
|
+
"log",
|
|
287
|
+
"--no-merges",
|
|
288
|
+
"--format=%s%n%b",
|
|
289
|
+
range,
|
|
290
|
+
"--",
|
|
291
|
+
...scopedPaths,
|
|
292
|
+
]);
|
|
267
293
|
const changedFiles = capture(options.root, "git", [
|
|
268
294
|
"diff",
|
|
269
295
|
"--name-status",
|
|
270
296
|
range,
|
|
271
297
|
"--",
|
|
272
|
-
|
|
298
|
+
...scopedPaths,
|
|
273
299
|
`:(exclude)${relativeOutput}`,
|
|
274
300
|
]);
|
|
275
301
|
const diffStat = capture(options.root, "git", [
|
|
@@ -277,7 +303,7 @@ export async function generateReleaseSummary(options: {
|
|
|
277
303
|
"--stat",
|
|
278
304
|
range,
|
|
279
305
|
"--",
|
|
280
|
-
|
|
306
|
+
...scopedPaths,
|
|
281
307
|
`:(exclude)${relativeOutput}`,
|
|
282
308
|
]);
|
|
283
309
|
const result = await selectReleaseSummary(
|
|
@@ -289,7 +315,7 @@ export async function generateReleaseSummary(options: {
|
|
|
289
315
|
const summary = result?.summary ?? gitSummary(options.version, commits, changedFiles);
|
|
290
316
|
const provider = result?.provider ?? "git";
|
|
291
317
|
if (!result) logger.info("AI providers unavailable; using Git release summary");
|
|
292
|
-
const content = `# Release ${options.version}\n\n${summary.trim()}\n`;
|
|
318
|
+
const content = `# Release ${options.component ? `${options.component} ` : ""}${options.version}\n\n${summary.trim()}\n`;
|
|
293
319
|
mkdirSync(dirname(output), { recursive: true });
|
|
294
320
|
writeFileSync(output, content);
|
|
295
321
|
logger.info("generated", { provider, path: output });
|
package/tasks/uniffi-python.d.ts
CHANGED
|
@@ -6,11 +6,15 @@ export interface TomlCodec {
|
|
|
6
6
|
export interface PythonPackageMapping {
|
|
7
7
|
readonly directory: string;
|
|
8
8
|
readonly name: string;
|
|
9
|
+
readonly version: string;
|
|
10
|
+
readonly uniffi: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface PythonProjectInfo {
|
|
12
14
|
readonly name: string;
|
|
15
|
+
readonly version: string;
|
|
13
16
|
readonly private: boolean;
|
|
17
|
+
readonly uniffi: boolean;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/** Read the project identity and publication flag from parsed TOML metadata. */
|
|
@@ -22,6 +26,7 @@ export function stampPythonProject(
|
|
|
22
26
|
options: {
|
|
23
27
|
readonly packages: readonly PythonPackageMapping[];
|
|
24
28
|
readonly rewriteDependencies?: boolean;
|
|
29
|
+
readonly usePackageVersions?: boolean;
|
|
25
30
|
readonly toml: TomlCodec;
|
|
26
31
|
readonly version: string;
|
|
27
32
|
},
|