@dbx-tools/projen 0.9.5 → 0.9.7
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 +69 -17
- package/index.ts +7 -5
- package/package.json +9 -6
- package/src/_rust-project.ts +356 -0
- package/src/_rust-release-workflow.ts +954 -0
- package/src/_rust-release.ts +605 -0
- package/src/_task-command.ts +89 -0
- package/src/barrels.ts +12 -11
- package/src/bun-app.ts +96 -85
- package/src/bun-workflow.ts +60 -52
- package/src/engine-root.ts +5 -5
- package/src/openapi.ts +16 -9
- 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 +28 -23
- 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 +212 -177
- 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/sync.ts +3 -3
- package/tasks/uniffi-python.js +1 -1
- package/tasks/uniffi.ts +7 -1
- package/tasks/version-check.ts +6 -5
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]);
|
package/tasks/release-request.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import
|
|
7
|
-
import { log,
|
|
6
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
7
|
+
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { selectReleaseSummary } from "./release-summary.ts";
|
|
10
|
+
import { captureTaskCommand, runTaskCommand } from "../src/_task-command.ts";
|
|
10
11
|
|
|
11
12
|
const logger = log.logger("projen:release-request");
|
|
12
13
|
|
|
@@ -33,13 +34,7 @@ export async function prepareReleaseRequest(
|
|
|
33
34
|
}
|
|
34
35
|
const baseRef = options.push === false ? options.baseBranch : `origin/${options.baseBranch}`;
|
|
35
36
|
if (options.synthesize !== false) {
|
|
36
|
-
|
|
37
|
-
cwd: root,
|
|
38
|
-
stdout: "inherit",
|
|
39
|
-
stderr: "inherit",
|
|
40
|
-
stdin: "ignore",
|
|
41
|
-
check: true,
|
|
42
|
-
});
|
|
37
|
+
runTaskCommand(root, process.execPath, ["x", "projen"]);
|
|
43
38
|
}
|
|
44
39
|
const dirty = capture(root, ["status", "--porcelain"]);
|
|
45
40
|
if (dirty) {
|
|
@@ -74,7 +69,7 @@ export async function prepareReleaseRequest(
|
|
|
74
69
|
throw new Error(`Release requests must run from a branch other than ${options.baseBranch}`);
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
const customNotes =
|
|
72
|
+
const customNotes = stringUtils.trimToNull(
|
|
78
73
|
options.notesFile ? readFileSync(resolve(root, options.notesFile), "utf8") : options.notes,
|
|
79
74
|
);
|
|
80
75
|
const notes =
|
|
@@ -130,27 +125,11 @@ async function generateRequestNotes(root: string, range: string): Promise<string
|
|
|
130
125
|
}
|
|
131
126
|
|
|
132
127
|
function capture(root: string, args: string[]): string {
|
|
133
|
-
return (
|
|
134
|
-
exec
|
|
135
|
-
.spawnSync("git", args, {
|
|
136
|
-
cwd: root,
|
|
137
|
-
stdout: "capture",
|
|
138
|
-
stderr: "inherit",
|
|
139
|
-
stdin: "ignore",
|
|
140
|
-
check: true,
|
|
141
|
-
})
|
|
142
|
-
.stdout?.trim() ?? ""
|
|
143
|
-
);
|
|
128
|
+
return captureTaskCommand(root, "git", args, { check: true, stderr: "inherit" });
|
|
144
129
|
}
|
|
145
130
|
|
|
146
131
|
function run(root: string, args: string[]): void {
|
|
147
|
-
|
|
148
|
-
cwd: root,
|
|
149
|
-
stdout: "inherit",
|
|
150
|
-
stderr: "inherit",
|
|
151
|
-
stdin: "ignore",
|
|
152
|
-
check: true,
|
|
153
|
-
});
|
|
132
|
+
runTaskCommand(root, "git", args);
|
|
154
133
|
}
|
|
155
134
|
|
|
156
135
|
/** Restore option text after Projen forwards task arguments without shell quoting. */
|
|
@@ -175,7 +154,7 @@ if (import.meta.main) {
|
|
|
175
154
|
throw new Error("Use --notes or --notes-file, not both");
|
|
176
155
|
}
|
|
177
156
|
await prepareReleaseRequest({
|
|
178
|
-
root:
|
|
157
|
+
root: projectUtils.root() ?? process.cwd(),
|
|
179
158
|
baseBranch: options.base,
|
|
180
159
|
message: optionText(options.message),
|
|
181
160
|
notes: optionText(options.notes),
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
|
-
import { exec } from "@dbx-tools/core";
|
|
7
6
|
import { log } from "@dbx-tools/shared-core";
|
|
8
7
|
import { Command } from "commander";
|
|
9
8
|
import { generateReleaseSummary } from "./release-summary.ts";
|
|
9
|
+
import { captureTaskCommand } from "../src/_task-command.ts";
|
|
10
10
|
import type { ReleaseUnitGraph } from "../src/release-catalog.ts";
|
|
11
11
|
import type { ReleasePlan } from "../src/release-plan.ts";
|
|
12
12
|
|
|
@@ -71,12 +71,5 @@ function requestNotes(root: string): { summary?: string; paths: string[] } {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function gitCapture(root: string, args: string[]): string {
|
|
74
|
-
|
|
75
|
-
cwd: root,
|
|
76
|
-
stdout: "capture",
|
|
77
|
-
stderr: "ignore",
|
|
78
|
-
stdin: "ignore",
|
|
79
|
-
check: false,
|
|
80
|
-
});
|
|
81
|
-
return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
|
|
74
|
+
return captureTaskCommand(root, "git", args);
|
|
82
75
|
}
|
package/tasks/release-summary.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
10
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
11
|
+
import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
12
|
+
import { captureTaskCommand } from "../src/_task-command.ts";
|
|
11
13
|
import {
|
|
12
14
|
RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
13
15
|
releaseSummaryFile,
|
|
@@ -84,30 +86,23 @@ export type ReleaseSummaryRunner = (
|
|
|
84
86
|
) => string | undefined | Promise<string | undefined>;
|
|
85
87
|
|
|
86
88
|
function capture(root: string, command: string, args: string[]): string {
|
|
87
|
-
|
|
88
|
-
cwd: root,
|
|
89
|
-
stdout: "capture",
|
|
90
|
-
stderr: "ignore",
|
|
91
|
-
stdin: "ignore",
|
|
92
|
-
check: false,
|
|
93
|
-
});
|
|
94
|
-
return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
|
|
89
|
+
return captureTaskCommand(root, command, args);
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
function contentText(value: unknown): string | undefined {
|
|
98
|
-
if (typeof value === "string") return
|
|
93
|
+
if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
|
|
99
94
|
if (!Array.isArray(value)) return undefined;
|
|
100
95
|
const text = value
|
|
101
96
|
.flatMap((item) => (object.isRecord(item) && typeof item.text === "string" ? [item.text] : []))
|
|
102
97
|
.join("");
|
|
103
|
-
return
|
|
98
|
+
return stringUtils.trimToNull(text) ?? undefined;
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
function eventText(event: Record<string, unknown>): string | undefined {
|
|
107
|
-
if (typeof event.result === "string") return
|
|
102
|
+
if (typeof event.result === "string") return stringUtils.trimToNull(event.result) ?? undefined;
|
|
108
103
|
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
109
104
|
if (item?.type === "agent_message" && typeof item.text === "string") {
|
|
110
|
-
return
|
|
105
|
+
return stringUtils.trimToNull(item.text) ?? undefined;
|
|
111
106
|
}
|
|
112
107
|
const message = object.isRecord(event.message) ? event.message : undefined;
|
|
113
108
|
return contentText(message?.content);
|
|
@@ -121,7 +116,7 @@ function eventType(event: Record<string, unknown>): string {
|
|
|
121
116
|
function eventDetail(event: Record<string, unknown>): string | undefined {
|
|
122
117
|
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
123
118
|
for (const value of [item?.message, event.message, event.error]) {
|
|
124
|
-
if (typeof value === "string") return
|
|
119
|
+
if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
|
|
125
120
|
}
|
|
126
121
|
return undefined;
|
|
127
122
|
}
|
|
@@ -129,7 +124,7 @@ function eventDetail(event: Record<string, unknown>): string | undefined {
|
|
|
129
124
|
function eventToolName(event: Record<string, unknown>): string | undefined {
|
|
130
125
|
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
131
126
|
for (const value of [event.tool_name, event.toolName, event.name, item?.tool_name, item?.name]) {
|
|
132
|
-
if (typeof value === "string") return
|
|
127
|
+
if (typeof value === "string") return stringUtils.trimToNull(value) ?? undefined;
|
|
133
128
|
}
|
|
134
129
|
const toolCall = object.isRecord(event.tool_call)
|
|
135
130
|
? event.tool_call
|
|
@@ -185,7 +180,7 @@ async function runProvider(
|
|
|
185
180
|
(line) => {
|
|
186
181
|
const event = json.parseRecord(line);
|
|
187
182
|
if (!event) {
|
|
188
|
-
const text =
|
|
183
|
+
const text = stringUtils.trimToNull(line);
|
|
189
184
|
if (text) {
|
|
190
185
|
textOutput.push(text);
|
|
191
186
|
logEvent({ type: "text", text });
|
|
@@ -198,7 +193,7 @@ async function runProvider(
|
|
|
198
193
|
},
|
|
199
194
|
],
|
|
200
195
|
stderr: (line) => {
|
|
201
|
-
const message =
|
|
196
|
+
const message = stringUtils.trimToNull(line);
|
|
202
197
|
if (message) logger.warn("provider-stderr", { provider: provider.name, message });
|
|
203
198
|
},
|
|
204
199
|
stdin: "ignore",
|
|
@@ -243,6 +238,7 @@ export async function selectReleaseSummary(
|
|
|
243
238
|
}
|
|
244
239
|
|
|
245
240
|
function summaryPrompt(
|
|
241
|
+
projectName: string,
|
|
246
242
|
version: string,
|
|
247
243
|
fromRef: string | undefined,
|
|
248
244
|
commits: string,
|
|
@@ -250,7 +246,7 @@ function summaryPrompt(
|
|
|
250
246
|
diffStat: string,
|
|
251
247
|
): string {
|
|
252
248
|
return [
|
|
253
|
-
`Write a concise user-facing Markdown release summary for
|
|
249
|
+
`Write a concise user-facing Markdown release summary for ${projectName} ${version}.`,
|
|
254
250
|
"Use only the supplied Git context. Do not run commands, edit files, speculate,",
|
|
255
251
|
"mention commit hashes, use emojis, or use em/en dashes.",
|
|
256
252
|
"Return no title and no fenced block. Start with one short paragraph, then",
|
|
@@ -270,7 +266,12 @@ function summaryPrompt(
|
|
|
270
266
|
}
|
|
271
267
|
|
|
272
268
|
/** Deterministic fallback when every configured AI provider is unavailable. */
|
|
273
|
-
function gitSummary(
|
|
269
|
+
function gitSummary(
|
|
270
|
+
projectName: string,
|
|
271
|
+
version: string,
|
|
272
|
+
commits: string,
|
|
273
|
+
changedFiles: string,
|
|
274
|
+
): string {
|
|
274
275
|
const subjects = commits
|
|
275
276
|
.split("\n")
|
|
276
277
|
.map((line) => line.trim())
|
|
@@ -290,7 +291,7 @@ function gitSummary(version: string, commits: string, changedFiles: string): str
|
|
|
290
291
|
.slice(0, 12);
|
|
291
292
|
const bullets = subjects.length ? subjects : areas.map((area) => `Updated ${area}`);
|
|
292
293
|
return [
|
|
293
|
-
|
|
294
|
+
`${projectName} ${version} contains the reviewed changes listed below.`,
|
|
294
295
|
"",
|
|
295
296
|
"## Changes",
|
|
296
297
|
...(bullets.length ? bullets.map((item) => `- ${item}`) : ["- Release metadata updated."]),
|
|
@@ -310,6 +311,7 @@ export async function generateReleaseSummary(options: {
|
|
|
310
311
|
readonly providers?: readonly ReleaseSummaryProviderName[];
|
|
311
312
|
readonly runner?: ReleaseSummaryRunner;
|
|
312
313
|
}): Promise<string | undefined> {
|
|
314
|
+
const projectName = projectUtils.name(options.root);
|
|
313
315
|
const relativeOutput =
|
|
314
316
|
options.outputFile ?? releaseSummaryFile(options.version, options.component);
|
|
315
317
|
const output = join(options.root, relativeOutput);
|
|
@@ -347,17 +349,26 @@ export async function generateReleaseSummary(options: {
|
|
|
347
349
|
...scopedPaths,
|
|
348
350
|
`:(exclude)${relativeOutput}`,
|
|
349
351
|
]);
|
|
350
|
-
const customSummary =
|
|
352
|
+
const customSummary = stringUtils.trimToNull(options.customSummary);
|
|
351
353
|
const result = customSummary
|
|
352
354
|
? undefined
|
|
353
355
|
: await selectReleaseSummary(
|
|
354
356
|
options.root,
|
|
355
|
-
summaryPrompt(
|
|
357
|
+
summaryPrompt(
|
|
358
|
+
projectName,
|
|
359
|
+
options.version,
|
|
360
|
+
options.fromRef,
|
|
361
|
+
commits,
|
|
362
|
+
changedFiles,
|
|
363
|
+
diffStat,
|
|
364
|
+
),
|
|
356
365
|
options.runner,
|
|
357
366
|
options.providers,
|
|
358
367
|
);
|
|
359
368
|
const summary =
|
|
360
|
-
customSummary ??
|
|
369
|
+
customSummary ??
|
|
370
|
+
result?.summary ??
|
|
371
|
+
gitSummary(projectName, options.version, commits, changedFiles);
|
|
361
372
|
const provider = customSummary ? "custom" : (result?.provider ?? "git");
|
|
362
373
|
if (!customSummary && !result) {
|
|
363
374
|
logger.info("AI providers unavailable; using Git release summary");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Validate one stable release version against the canonical workspace policy. */
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import * as projectUtils from "@dbx-tools/core/project-utils";
|
|
6
|
+
import { compareSemver, parseSemver, resolveRemoteVersion } from "../src/workspace-version.ts";
|
|
7
|
+
|
|
8
|
+
export function assertReleaseVersion(
|
|
9
|
+
version: string,
|
|
10
|
+
options: { root?: string; prefixes?: readonly string[]; assertNext?: boolean } = {},
|
|
11
|
+
): void {
|
|
12
|
+
const parsed = parseSemver(version);
|
|
13
|
+
if (!parsed) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`release version must be an exact stable x.y.z, got ${JSON.stringify(version)}`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (!options.assertNext) return;
|
|
19
|
+
const root = resolve(options.root ?? projectUtils.root() ?? process.cwd());
|
|
20
|
+
const prefixes = options.prefixes?.length ? options.prefixes : ["v"];
|
|
21
|
+
const current = resolveRemoteVersion(root, prefixes, { fetch: false });
|
|
22
|
+
const currentParsed = current ? parseSemver(current) : undefined;
|
|
23
|
+
if (currentParsed && compareSemver(parsed, currentParsed) <= 0) {
|
|
24
|
+
throw new Error(`release version ${version} must be greater than published ${current}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (import.meta.main) {
|
|
29
|
+
const { values } = parseArgs({
|
|
30
|
+
options: {
|
|
31
|
+
version: { type: "string" },
|
|
32
|
+
root: { type: "string" },
|
|
33
|
+
prefix: { type: "string", multiple: true },
|
|
34
|
+
"assert-next": { type: "boolean", default: false },
|
|
35
|
+
},
|
|
36
|
+
strict: true,
|
|
37
|
+
});
|
|
38
|
+
if (!values.version) throw new Error("--version is required");
|
|
39
|
+
assertReleaseVersion(values.version, {
|
|
40
|
+
...(values.root ? { root: values.root } : {}),
|
|
41
|
+
...(values.prefix ? { prefixes: values.prefix } : {}),
|
|
42
|
+
assertNext: values["assert-next"],
|
|
43
|
+
});
|
|
44
|
+
}
|