@dbx-tools/projen 0.6.224 → 0.6.226
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 +17 -1
- package/index.ts +4 -3
- package/package.json +4 -4
- package/src/project-js.ts +7 -1
- package/src/project-rs.ts +2 -0
- package/src/release-dispatch.ts +10 -0
- package/src/release.ts +37 -2
- package/tasks/release-pr.ts +47 -0
- package/tasks/release-summary.ts +270 -0
package/README.md
CHANGED
|
@@ -57,6 +57,12 @@ Repository policy stays in the consuming `.projenrc.ts`:
|
|
|
57
57
|
artifact upload, and deployment without naming a docs script or output tree.
|
|
58
58
|
- `releasePythonRoot` passes the actual Python package root to local release
|
|
59
59
|
preparation. Omit it when the workspace has no standard Python packages.
|
|
60
|
+
- `releaseValidationTasks` names repository tasks that must pass in the release
|
|
61
|
+
worktree before Cargo tests, compilation, local publication, or approval.
|
|
62
|
+
Use it for repository-specific guards that also run in release CI.
|
|
63
|
+
- `releaseSummary` controls optional AI release notes. It defaults to enabled
|
|
64
|
+
with Cursor, Codex, Claude fallback order; pass `false` to disable or provide
|
|
65
|
+
a `providers` array to choose the order/subset.
|
|
60
66
|
- `pullRequestTitlePolicy` configures semantic title types and scope policy.
|
|
61
67
|
Omit it or pass `false` to disable the title job.
|
|
62
68
|
- `workflowCacheIgnorePaths` excludes generated output trees that may contain
|
|
@@ -441,7 +447,17 @@ it. `--message` sets the source commit message. The task never merges
|
|
|
441
447
|
the PR unless `--approve` is passed; that option merges the release branch
|
|
442
448
|
directly through GitHub's merge API so no PR checks are created. A repository
|
|
443
449
|
that blocks direct merges falls back to an immediate admin-merged PR. The task
|
|
444
|
-
then fast-forwards the still-active source branch to the release commit.
|
|
450
|
+
then fast-forwards the still-active source branch to the release commit.
|
|
451
|
+
|
|
452
|
+
After validation, release preparation optionally generates an immutable
|
|
453
|
+
`docs/releases/v<version>.md`. It tries the installed Cursor agent first, then
|
|
454
|
+
Codex, then Claude, all in non-interactive read-only modes against bounded Git
|
|
455
|
+
context. An unavailable, unauthenticated, failed, or empty provider falls
|
|
456
|
+
through to the next; if none works, release preparation continues without a
|
|
457
|
+
summary. The same file is included in the release PR and prepended to GitHub's
|
|
458
|
+
generated release notes, so repository and GitHub views share one source.
|
|
459
|
+
|
|
460
|
+
npm
|
|
445
461
|
uses `npm config get registry` and publishes to a local Verdaccio automatically.
|
|
446
462
|
Publishable JavaScript members compile once
|
|
447
463
|
from the root in parallel, then upload through a bounded pool without rerunning
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.6.
|
|
6
|
+
export const PACKAGE_VERSION = "0.6.226";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
|
@@ -55,8 +55,9 @@ export { RustReleaseOs, RustReleaseCpu, UNIFFI_RELEASE_TARGETS, DBXToolsRustProj
|
|
|
55
55
|
export type { CargoDependencyOptions, CargoDependency, RustCliOptions, RustPackageOptions, DBXToolsRustWorkspaceOptions, RustReleasePlatform, UniFFIReleaseTarget, RustBindingMapping, RustWorkspaceMapping, RustReleaseBinaryAssetMapping, RustReleaseBinaryMapping } from "./src/project-rs.ts";
|
|
56
56
|
export { COMPILED_DIR, COMPILED_COMPILER_OPTIONS } from "./src/publish.ts";
|
|
57
57
|
export { DBXToolsRelease } from "./src/release.ts";
|
|
58
|
-
export type { ReleaseStage, ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.ts";
|
|
59
|
-
export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION } from "./src/release-dispatch.ts";
|
|
58
|
+
export type { ReleaseStage, ReleaseDocsOptions, ReleaseSummaryOptions, DBXToolsReleaseOptions } from "./src/release.ts";
|
|
59
|
+
export { RELEASE_TAG, RELEASE_SHA, RELEASE_VERSION, RELEASE_SUMMARY_DIRECTORY, RELEASE_SUMMARY_PROVIDER_NAMES, RELEASE_SUMMARY_FILE } from "./src/release-dispatch.ts";
|
|
60
|
+
export type { ReleaseSummaryProviderName } from "./src/release-dispatch.ts";
|
|
60
61
|
export type { GithubRepositoryIdentity } from "./src/release-github.ts";
|
|
61
62
|
export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
|
|
62
63
|
export type { PackageTag } from "./src/tags.ts";
|
package/package.json
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@clack/prompts": "^1.7.0",
|
|
28
|
-
"@dbx-tools/core": "0.6.
|
|
29
|
-
"@dbx-tools/path": "0.6.
|
|
30
|
-
"@dbx-tools/shared-core": "0.6.
|
|
28
|
+
"@dbx-tools/core": "0.6.226",
|
|
29
|
+
"@dbx-tools/path": "0.6.226",
|
|
30
|
+
"@dbx-tools/shared-core": "0.6.226",
|
|
31
31
|
"commander": "^15.0.0",
|
|
32
32
|
"concurrently": "^10.0.3",
|
|
33
33
|
"constructs": "^10.6.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"main": "index.ts",
|
|
49
49
|
"license": "Apache-2.0",
|
|
50
|
-
"version": "0.6.
|
|
50
|
+
"version": "0.6.226",
|
|
51
51
|
"types": "index.ts",
|
|
52
52
|
"type": "module",
|
|
53
53
|
"exports": {
|
package/src/project-js.ts
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
|
|
42
42
|
import type { DBXToolsProject, DBXToolsProjectOptions as CommonProjectOptions } from "./project.ts";
|
|
43
43
|
import { applyCompiledPublish } from "./publish.ts";
|
|
44
|
-
import { DBXToolsRelease, type ReleaseDocsOptions } from "./release.ts";
|
|
44
|
+
import { DBXToolsRelease, type ReleaseDocsOptions, type ReleaseSummaryOptions } from "./release.ts";
|
|
45
45
|
import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
|
|
46
46
|
import { DBXToolsRootTsconfig } from "./tsconfig.ts";
|
|
47
47
|
import { DBXToolsVsCode } from "./vscode.ts";
|
|
@@ -621,6 +621,10 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
621
621
|
readonly releaseDocs?: ReleaseDocsOptions;
|
|
622
622
|
/** Python package root passed to local release preparation when configured. */
|
|
623
623
|
readonly releasePythonRoot?: string;
|
|
624
|
+
/** Repository task names run during local release preparation before publication. */
|
|
625
|
+
readonly releaseValidationTasks?: readonly string[];
|
|
626
|
+
/** Optional AI-generated release summary. Defaults to enabled. */
|
|
627
|
+
readonly releaseSummary?: boolean | ReleaseSummaryOptions;
|
|
624
628
|
/** Set to `false` to omit normal npm workspace publication. */
|
|
625
629
|
readonly nodeRelease?: boolean;
|
|
626
630
|
/** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
|
|
@@ -1540,6 +1544,8 @@ function initProject(
|
|
|
1540
1544
|
nodeRelease: options.nodeRelease,
|
|
1541
1545
|
docs: options.releaseDocs,
|
|
1542
1546
|
pythonRoot: options.releasePythonRoot,
|
|
1547
|
+
validationTasks: options.releaseValidationTasks,
|
|
1548
|
+
summary: options.releaseSummary,
|
|
1543
1549
|
});
|
|
1544
1550
|
}
|
|
1545
1551
|
}
|
package/src/project-rs.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { pythonModuleName, type PythonPackageOptions } from "./project-py.ts";
|
|
|
19
19
|
import type { DBXToolsProject } from "./project.ts";
|
|
20
20
|
import {
|
|
21
21
|
RELEASE_SHA,
|
|
22
|
+
RELEASE_SUMMARY_FILE,
|
|
22
23
|
RELEASE_TAG,
|
|
23
24
|
RELEASE_VERSION,
|
|
24
25
|
releaseSourceSteps,
|
|
@@ -1359,6 +1360,7 @@ function rustGitHubReleaseJob(): Job {
|
|
|
1359
1360
|
uses: "softprops/action-gh-release@v2",
|
|
1360
1361
|
with: {
|
|
1361
1362
|
files: "dist/rust-release/*",
|
|
1363
|
+
body_path: RELEASE_SUMMARY_FILE,
|
|
1362
1364
|
"generate-release-notes": true,
|
|
1363
1365
|
tag_name: RELEASE_TAG,
|
|
1364
1366
|
target_commitish: RELEASE_SHA,
|
package/src/release-dispatch.ts
CHANGED
|
@@ -4,6 +4,16 @@ import type { JobStep } from "projen/lib/github/workflows-model";
|
|
|
4
4
|
export const RELEASE_TAG = "${{ needs.verify-context.outputs.release_tag }}";
|
|
5
5
|
export const RELEASE_SHA = "${{ needs.verify-context.outputs.expected_sha }}";
|
|
6
6
|
export const RELEASE_VERSION = "${{ needs.verify-context.outputs.release_version }}";
|
|
7
|
+
export const RELEASE_SUMMARY_DIRECTORY = "docs/releases";
|
|
8
|
+
export const RELEASE_SUMMARY_PROVIDER_NAMES = ["cursor", "codex", "claude"] as const;
|
|
9
|
+
export type ReleaseSummaryProviderName = (typeof RELEASE_SUMMARY_PROVIDER_NAMES)[number];
|
|
10
|
+
|
|
11
|
+
/** Immutable repository path for one version's release summary. */
|
|
12
|
+
export function releaseSummaryFile(version: string): string {
|
|
13
|
+
return `${RELEASE_SUMMARY_DIRECTORY}/v${version.replace(/^v/, "")}.md`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const RELEASE_SUMMARY_FILE = releaseSummaryFile(RELEASE_VERSION);
|
|
7
17
|
|
|
8
18
|
/** Check out and verify the immutable commit selected by the release tag. */
|
|
9
19
|
export function releaseSourceSteps(): readonly JobStep[] {
|
package/src/release.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflo
|
|
|
5
5
|
import { BUN_VERSION, bunCacheRestoreSteps, bunCacheSaveStep } from "./bun-workflow.ts";
|
|
6
6
|
import { projectReleaseBranch, type DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
7
7
|
import { applyTasks, taskScript } from "./project.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
RELEASE_VERSION,
|
|
10
|
+
releaseSourceSteps,
|
|
11
|
+
type ReleaseSummaryProviderName,
|
|
12
|
+
} from "./release-dispatch.ts";
|
|
13
|
+
import { readWorkspaceVersion } from "./workspace-version.ts";
|
|
9
14
|
|
|
10
15
|
const NODE_VERSION = "lts/*";
|
|
11
16
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
@@ -28,6 +33,16 @@ export interface ReleaseDocsOptions {
|
|
|
28
33
|
readonly artifactPath: string;
|
|
29
34
|
}
|
|
30
35
|
|
|
36
|
+
/** Optional local AI summary generation during reviewed release preparation. */
|
|
37
|
+
export interface ReleaseSummaryOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Provider fallback order.
|
|
40
|
+
*
|
|
41
|
+
* @default ["cursor", "codex", "claude"]
|
|
42
|
+
*/
|
|
43
|
+
readonly providers?: readonly ReleaseSummaryProviderName[];
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/** Options for {@link DBXToolsRelease}. */
|
|
32
47
|
export interface DBXToolsReleaseOptions {
|
|
33
48
|
/** Git tag prefix. Defaults to `v`. */
|
|
@@ -38,6 +53,13 @@ export interface DBXToolsReleaseOptions {
|
|
|
38
53
|
readonly docs?: ReleaseDocsOptions;
|
|
39
54
|
/** Python package root passed to local release preparation. */
|
|
40
55
|
readonly pythonRoot?: string;
|
|
56
|
+
/** Repository task names run in the release worktree before expensive validation and publication. */
|
|
57
|
+
readonly validationTasks?: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* Generate a versioned release summary locally. Defaults to enabled with the
|
|
60
|
+
* standard Cursor, Codex, Claude fallback order.
|
|
61
|
+
*/
|
|
62
|
+
readonly summary?: boolean | ReleaseSummaryOptions;
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
/** Locate the unified workflow so attached language workspaces can add jobs. */
|
|
@@ -320,6 +342,13 @@ export class DBXToolsRelease extends Component {
|
|
|
320
342
|
super(project);
|
|
321
343
|
const tagPrefix = options.tagPrefix ?? "v";
|
|
322
344
|
const releaseBranch = projectReleaseBranch(project);
|
|
345
|
+
const summary = options.summary ?? true;
|
|
346
|
+
const summaryArgs =
|
|
347
|
+
summary === false || (typeof summary === "object" && summary.providers?.length === 0)
|
|
348
|
+
? ["--no-release-summary"]
|
|
349
|
+
: typeof summary === "object" && summary.providers
|
|
350
|
+
? [`--release-summary-providers ${summary.providers.join(",")}`]
|
|
351
|
+
: [];
|
|
323
352
|
releaseTagPrefixes.set(project, tagPrefix);
|
|
324
353
|
if (options.nodeRelease !== false) nodeReleaseProjects.add(project);
|
|
325
354
|
applyTasks(project, {
|
|
@@ -344,6 +373,10 @@ export class DBXToolsRelease extends Component {
|
|
|
344
373
|
...(options.pythonRoot
|
|
345
374
|
? [`--python-root ${JSON.stringify(options.pythonRoot)}`]
|
|
346
375
|
: []),
|
|
376
|
+
...(options.validationTasks ?? []).map(
|
|
377
|
+
(task) => `--validate-task ${JSON.stringify(task)}`,
|
|
378
|
+
),
|
|
379
|
+
...summaryArgs,
|
|
347
380
|
].join(" "),
|
|
348
381
|
),
|
|
349
382
|
receiveArgs: true,
|
|
@@ -359,8 +392,10 @@ export class DBXToolsRelease extends Component {
|
|
|
359
392
|
limitConcurrency: true,
|
|
360
393
|
concurrencyOptions: { group: "release", cancelInProgress: false },
|
|
361
394
|
});
|
|
395
|
+
const version = readWorkspaceVersion(project.outdir);
|
|
362
396
|
workflow.runName =
|
|
363
|
-
|
|
397
|
+
`release ${version} ` +
|
|
398
|
+
"${{ github.event_name == 'push' && github.sha || inputs.release_tag }}";
|
|
364
399
|
workflow.on({
|
|
365
400
|
push: { branches: [releaseBranch], paths: ["VERSION"] },
|
|
366
401
|
workflowDispatch: {
|
package/tasks/release-pr.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { exec, project } from "@dbx-tools/core";
|
|
|
14
14
|
import { log } from "@dbx-tools/shared-core";
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { publishLocalRelease } from "./local-publish.ts";
|
|
17
|
+
import { generateReleaseSummary } from "./release-summary.ts";
|
|
17
18
|
import {
|
|
18
19
|
releaseArchitectureOption,
|
|
19
20
|
releaseLevelOption,
|
|
@@ -28,6 +29,10 @@ import {
|
|
|
28
29
|
githubTokenArguments,
|
|
29
30
|
githubTokenEnvironmentName,
|
|
30
31
|
} from "../src/release-github.ts";
|
|
32
|
+
import {
|
|
33
|
+
RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
34
|
+
type ReleaseSummaryProviderName,
|
|
35
|
+
} from "../src/release-dispatch.ts";
|
|
31
36
|
import { readWorkspaceVersion, resolveNextVersion } from "../src/workspace-version.ts";
|
|
32
37
|
|
|
33
38
|
const logger = log.logger("projen:release");
|
|
@@ -130,6 +135,22 @@ function githubAccount(root: string): {
|
|
|
130
135
|
return { ...identity, token };
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
function releaseSummaryProviders(
|
|
139
|
+
value: string | undefined,
|
|
140
|
+
): ReleaseSummaryProviderName[] | undefined {
|
|
141
|
+
if (value === undefined) return undefined;
|
|
142
|
+
const providers = value
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((provider) => provider.trim())
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
for (const provider of providers) {
|
|
147
|
+
if (!RELEASE_SUMMARY_PROVIDER_NAMES.includes(provider as ReleaseSummaryProviderName)) {
|
|
148
|
+
throw new Error(`Unknown release summary provider: ${provider}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return providers as ReleaseSummaryProviderName[];
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
const program = new Command();
|
|
134
155
|
program
|
|
135
156
|
.description("Prepare, validate, locally publish, and open a reviewed release PR")
|
|
@@ -142,6 +163,17 @@ program
|
|
|
142
163
|
.option("--local-registry <value>", "local npm registry: auto, false, or an explicit URL", "auto")
|
|
143
164
|
.option("--local-pypi <value>", "local PyPI index: auto, false, or an explicit URL", "auto")
|
|
144
165
|
.option("--python-root <path>", "Python workspace package root")
|
|
166
|
+
.option(
|
|
167
|
+
"--validate-task <task>",
|
|
168
|
+
"repository task to run before release validation; repeatable",
|
|
169
|
+
(task: string, tasks: string[]) => [...tasks, task],
|
|
170
|
+
[],
|
|
171
|
+
)
|
|
172
|
+
.option("--no-release-summary", "skip optional AI release summary generation")
|
|
173
|
+
.option(
|
|
174
|
+
"--release-summary-providers <providers>",
|
|
175
|
+
"comma-separated provider order: cursor,codex,claude",
|
|
176
|
+
)
|
|
145
177
|
.option("--no-local-cargo", "skip local Cargo publication")
|
|
146
178
|
.option("--approve", "merge the release branch directly into the release base")
|
|
147
179
|
.action(
|
|
@@ -155,6 +187,9 @@ program
|
|
|
155
187
|
localRegistry: string;
|
|
156
188
|
localPypi: string;
|
|
157
189
|
pythonRoot?: string;
|
|
190
|
+
validateTask: string[];
|
|
191
|
+
releaseSummary: boolean;
|
|
192
|
+
releaseSummaryProviders?: string;
|
|
158
193
|
localCargo: boolean;
|
|
159
194
|
approve: boolean;
|
|
160
195
|
}) => {
|
|
@@ -242,6 +277,9 @@ program
|
|
|
242
277
|
runIgnoringStdout(releaseRoot, "cargo", ["metadata", "--format-version", "1"]);
|
|
243
278
|
}
|
|
244
279
|
run(releaseRoot, process.execPath, [versionCheckScript]);
|
|
280
|
+
for (const task of opts.validateTask) {
|
|
281
|
+
run(releaseRoot, process.execPath, ["run", task]);
|
|
282
|
+
}
|
|
245
283
|
if (existsSync(join(releaseRoot, "Cargo.toml"))) {
|
|
246
284
|
run(releaseRoot, "cargo", [
|
|
247
285
|
"test",
|
|
@@ -259,6 +297,14 @@ program
|
|
|
259
297
|
localCargo: opts.localCargo,
|
|
260
298
|
});
|
|
261
299
|
run(releaseRoot, process.execPath, [versionCheckScript]);
|
|
300
|
+
const releaseSummary = opts.releaseSummary
|
|
301
|
+
? await generateReleaseSummary({
|
|
302
|
+
root: releaseRoot,
|
|
303
|
+
version: next.version,
|
|
304
|
+
fromRef: `${opts.prefix}${next.base}`,
|
|
305
|
+
providers: releaseSummaryProviders(opts.releaseSummaryProviders),
|
|
306
|
+
})
|
|
307
|
+
: undefined;
|
|
262
308
|
|
|
263
309
|
git(releaseRoot, ["add", "-A"]);
|
|
264
310
|
const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
|
|
@@ -279,6 +325,7 @@ program
|
|
|
279
325
|
`Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
|
|
280
326
|
"",
|
|
281
327
|
"Merging this PR updates VERSION on main and starts the public release workflow.",
|
|
328
|
+
...(releaseSummary ? ["", releaseSummary] : []),
|
|
282
329
|
].join("\n");
|
|
283
330
|
const githubEnvironment = {
|
|
284
331
|
...process.env,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/**
|
|
3
|
+
* Optional AI-generated release summary with CLI fallback.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { exec } from "@dbx-tools/core";
|
|
10
|
+
import { json, log, object, string } from "@dbx-tools/shared-core";
|
|
11
|
+
import {
|
|
12
|
+
RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
13
|
+
releaseSummaryFile,
|
|
14
|
+
type ReleaseSummaryProviderName,
|
|
15
|
+
} from "../src/release-dispatch.ts";
|
|
16
|
+
|
|
17
|
+
const logger = log.logger("projen:release-summary");
|
|
18
|
+
|
|
19
|
+
export interface ReleaseSummaryProvider {
|
|
20
|
+
readonly name: ReleaseSummaryProviderName;
|
|
21
|
+
readonly command: string;
|
|
22
|
+
readonly probeArgs: readonly string[];
|
|
23
|
+
args(root: string, prompt: string): string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const RELEASE_SUMMARY_PROVIDERS: readonly ReleaseSummaryProvider[] = [
|
|
27
|
+
{
|
|
28
|
+
name: "cursor",
|
|
29
|
+
command: "cursor",
|
|
30
|
+
probeArgs: ["agent", "--version"],
|
|
31
|
+
args: (root, prompt) => [
|
|
32
|
+
"agent",
|
|
33
|
+
"--print",
|
|
34
|
+
"--mode",
|
|
35
|
+
"ask",
|
|
36
|
+
"--output-format",
|
|
37
|
+
"stream-json",
|
|
38
|
+
"--trust",
|
|
39
|
+
"--workspace",
|
|
40
|
+
root,
|
|
41
|
+
prompt,
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "codex",
|
|
46
|
+
command: "codex",
|
|
47
|
+
probeArgs: ["--version"],
|
|
48
|
+
args: (root, prompt) => [
|
|
49
|
+
"exec",
|
|
50
|
+
"--sandbox",
|
|
51
|
+
"read-only",
|
|
52
|
+
"--skip-git-repo-check",
|
|
53
|
+
"--json",
|
|
54
|
+
"--color",
|
|
55
|
+
"never",
|
|
56
|
+
"-C",
|
|
57
|
+
root,
|
|
58
|
+
prompt,
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "claude",
|
|
63
|
+
command: "claude",
|
|
64
|
+
probeArgs: ["--version"],
|
|
65
|
+
args: (_root, prompt) => [
|
|
66
|
+
"--print",
|
|
67
|
+
"--no-session-persistence",
|
|
68
|
+
"--output-format",
|
|
69
|
+
"stream-json",
|
|
70
|
+
"--verbose",
|
|
71
|
+
"--tools",
|
|
72
|
+
"",
|
|
73
|
+
prompt,
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export type ReleaseSummaryRunner = (
|
|
79
|
+
provider: ReleaseSummaryProvider,
|
|
80
|
+
root: string,
|
|
81
|
+
prompt: string,
|
|
82
|
+
) => string | undefined | Promise<string | undefined>;
|
|
83
|
+
|
|
84
|
+
function capture(root: string, command: string, args: string[]): string {
|
|
85
|
+
const result = exec.spawnSync(command, args, {
|
|
86
|
+
cwd: root,
|
|
87
|
+
stdout: "capture",
|
|
88
|
+
stderr: "ignore",
|
|
89
|
+
stdin: "ignore",
|
|
90
|
+
check: false,
|
|
91
|
+
});
|
|
92
|
+
return result.exitCode === 0 ? (result.stdout?.trim() ?? "") : "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function contentText(value: unknown): string | undefined {
|
|
96
|
+
if (typeof value === "string") return string.trimToNull(value) ?? undefined;
|
|
97
|
+
if (!Array.isArray(value)) return undefined;
|
|
98
|
+
const text = value
|
|
99
|
+
.flatMap((item) => (object.isRecord(item) && typeof item.text === "string" ? [item.text] : []))
|
|
100
|
+
.join("");
|
|
101
|
+
return string.trimToNull(text) ?? undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function eventText(event: Record<string, unknown>): string | undefined {
|
|
105
|
+
if (typeof event.result === "string") return string.trimToNull(event.result) ?? undefined;
|
|
106
|
+
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
107
|
+
if (item?.type === "agent_message" && typeof item.text === "string") {
|
|
108
|
+
return string.trimToNull(item.text) ?? undefined;
|
|
109
|
+
}
|
|
110
|
+
const message = object.isRecord(event.message) ? event.message : undefined;
|
|
111
|
+
return contentText(message?.content);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function eventType(event: Record<string, unknown>): string {
|
|
115
|
+
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
116
|
+
return [event.type, item?.type].filter((value) => typeof value === "string").join(":") || "event";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function eventDetail(event: Record<string, unknown>): string | undefined {
|
|
120
|
+
const item = object.isRecord(event.item) ? event.item : undefined;
|
|
121
|
+
for (const value of [item?.message, event.message, event.error]) {
|
|
122
|
+
if (typeof value === "string") return string.trimToNull(value) ?? undefined;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runProvider(
|
|
128
|
+
provider: ReleaseSummaryProvider,
|
|
129
|
+
root: string,
|
|
130
|
+
prompt: string,
|
|
131
|
+
): Promise<string | undefined> {
|
|
132
|
+
if (!capture(root, provider.command, [...provider.probeArgs])) return undefined;
|
|
133
|
+
let summary: string | undefined;
|
|
134
|
+
const textOutput: string[] = [];
|
|
135
|
+
const process = exec.spawn(provider.command, provider.args(root, prompt), {
|
|
136
|
+
cwd: root,
|
|
137
|
+
stdout: [
|
|
138
|
+
"capture",
|
|
139
|
+
(line) => {
|
|
140
|
+
const event = json.parseRecord(line);
|
|
141
|
+
if (!event) {
|
|
142
|
+
const text = string.trimToNull(line);
|
|
143
|
+
if (text) {
|
|
144
|
+
textOutput.push(text);
|
|
145
|
+
logger.info("provider-event", { provider: provider.name, type: "text", text });
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const text = eventText(event);
|
|
150
|
+
const detail = eventDetail(event);
|
|
151
|
+
if (text) summary = text;
|
|
152
|
+
logger.info("provider-event", {
|
|
153
|
+
provider: provider.name,
|
|
154
|
+
type: eventType(event),
|
|
155
|
+
...(text ? { text } : {}),
|
|
156
|
+
...(detail ? { detail } : {}),
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
stderr: (line) => {
|
|
161
|
+
const message = string.trimToNull(line);
|
|
162
|
+
if (message) logger.warn("provider-stderr", { provider: provider.name, message });
|
|
163
|
+
},
|
|
164
|
+
stdin: "ignore",
|
|
165
|
+
check: false,
|
|
166
|
+
});
|
|
167
|
+
const result = await process;
|
|
168
|
+
if (result.exitCode !== 0) {
|
|
169
|
+
logger.warn("provider-failed", { provider: provider.name, exitCode: result.exitCode });
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return summary ?? (textOutput.length ? textOutput.join("\n") : undefined);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Return the first non-empty provider response in Cursor, Codex, Claude order. */
|
|
176
|
+
export async function selectReleaseSummary(
|
|
177
|
+
root: string,
|
|
178
|
+
prompt: string,
|
|
179
|
+
runner: ReleaseSummaryRunner = runProvider,
|
|
180
|
+
providers: readonly ReleaseSummaryProviderName[] = RELEASE_SUMMARY_PROVIDER_NAMES,
|
|
181
|
+
): Promise<{ provider: ReleaseSummaryProvider["name"]; summary: string } | undefined> {
|
|
182
|
+
for (const name of providers) {
|
|
183
|
+
const provider = RELEASE_SUMMARY_PROVIDERS.find((candidate) => candidate.name === name);
|
|
184
|
+
if (!provider) continue;
|
|
185
|
+
const summary = (await runner(provider, root, prompt))?.trim();
|
|
186
|
+
if (summary) return { provider: provider.name, summary };
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function summaryPrompt(
|
|
192
|
+
version: string,
|
|
193
|
+
fromRef: string | undefined,
|
|
194
|
+
commits: string,
|
|
195
|
+
changedFiles: string,
|
|
196
|
+
diffStat: string,
|
|
197
|
+
): string {
|
|
198
|
+
return [
|
|
199
|
+
`Write a concise user-facing Markdown release summary for dbx-tools ${version}.`,
|
|
200
|
+
"Use only the supplied Git context. Do not run commands, edit files, speculate,",
|
|
201
|
+
"mention commit hashes, use emojis, or use em/en dashes.",
|
|
202
|
+
"Return no title and no fenced block. Start with one short paragraph, then",
|
|
203
|
+
"group concrete changes under brief Markdown headings with bullets.",
|
|
204
|
+
"",
|
|
205
|
+
`Comparison base: ${fromRef ?? "repository root"}`,
|
|
206
|
+
"",
|
|
207
|
+
"Commits:",
|
|
208
|
+
commits || "(none)",
|
|
209
|
+
"",
|
|
210
|
+
"Changed files:",
|
|
211
|
+
changedFiles || "(none)",
|
|
212
|
+
"",
|
|
213
|
+
"Diff stat:",
|
|
214
|
+
diffStat || "(none)",
|
|
215
|
+
].join("\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Generate one immutable versioned summary, or remove a stale retry artifact. */
|
|
219
|
+
export async function generateReleaseSummary(options: {
|
|
220
|
+
readonly root: string;
|
|
221
|
+
readonly version: string;
|
|
222
|
+
readonly fromRef?: string;
|
|
223
|
+
readonly toRef?: string;
|
|
224
|
+
readonly providers?: readonly ReleaseSummaryProviderName[];
|
|
225
|
+
readonly runner?: ReleaseSummaryRunner;
|
|
226
|
+
}): Promise<string | undefined> {
|
|
227
|
+
const relativeOutput = releaseSummaryFile(options.version);
|
|
228
|
+
const output = join(options.root, relativeOutput);
|
|
229
|
+
if (existsSync(output)) rmSync(output);
|
|
230
|
+
const target =
|
|
231
|
+
options.toRef && capture(options.root, "git", ["rev-parse", "--verify", options.toRef])
|
|
232
|
+
? options.toRef
|
|
233
|
+
: "HEAD";
|
|
234
|
+
const range =
|
|
235
|
+
options.fromRef && capture(options.root, "git", ["rev-parse", "--verify", options.fromRef])
|
|
236
|
+
? `${options.fromRef}..${target}`
|
|
237
|
+
: target;
|
|
238
|
+
const commits = capture(options.root, "git", ["log", "--no-merges", "--format=%s%n%b", range]);
|
|
239
|
+
const changedFiles = capture(options.root, "git", [
|
|
240
|
+
"diff",
|
|
241
|
+
"--name-status",
|
|
242
|
+
range,
|
|
243
|
+
"--",
|
|
244
|
+
".",
|
|
245
|
+
`:(exclude)${relativeOutput}`,
|
|
246
|
+
]);
|
|
247
|
+
const diffStat = capture(options.root, "git", [
|
|
248
|
+
"diff",
|
|
249
|
+
"--stat",
|
|
250
|
+
range,
|
|
251
|
+
"--",
|
|
252
|
+
".",
|
|
253
|
+
`:(exclude)${relativeOutput}`,
|
|
254
|
+
]);
|
|
255
|
+
const result = await selectReleaseSummary(
|
|
256
|
+
options.root,
|
|
257
|
+
summaryPrompt(options.version, options.fromRef, commits, changedFiles, diffStat),
|
|
258
|
+
options.runner,
|
|
259
|
+
options.providers,
|
|
260
|
+
);
|
|
261
|
+
if (!result) {
|
|
262
|
+
logger.info("no supported AI CLI available; skipping release summary");
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const content = `# Release ${options.version}\n\n${result.summary.trim()}\n`;
|
|
266
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
267
|
+
writeFileSync(output, content);
|
|
268
|
+
logger.info("generated", { provider: result.provider, path: output });
|
|
269
|
+
return content;
|
|
270
|
+
}
|