@dbx-tools/projen 0.6.224 → 0.6.225
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 +14 -1
- package/index.ts +4 -3
- package/package.json +4 -4
- package/src/project-js.ts +4 -1
- package/src/project-rs.ts +2 -0
- package/src/release-dispatch.ts +10 -0
- package/src/release.ts +32 -2
- package/tasks/release-pr.ts +37 -0
- package/tasks/release-summary.ts +270 -0
package/README.md
CHANGED
|
@@ -57,6 +57,9 @@ 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
|
+
- `releaseSummary` controls optional AI release notes. It defaults to enabled
|
|
61
|
+
with Cursor, Codex, Claude fallback order; pass `false` to disable or provide
|
|
62
|
+
a `providers` array to choose the order/subset.
|
|
60
63
|
- `pullRequestTitlePolicy` configures semantic title types and scope policy.
|
|
61
64
|
Omit it or pass `false` to disable the title job.
|
|
62
65
|
- `workflowCacheIgnorePaths` excludes generated output trees that may contain
|
|
@@ -441,7 +444,17 @@ it. `--message` sets the source commit message. The task never merges
|
|
|
441
444
|
the PR unless `--approve` is passed; that option merges the release branch
|
|
442
445
|
directly through GitHub's merge API so no PR checks are created. A repository
|
|
443
446
|
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.
|
|
447
|
+
then fast-forwards the still-active source branch to the release commit.
|
|
448
|
+
|
|
449
|
+
After validation, release preparation optionally generates an immutable
|
|
450
|
+
`docs/releases/v<version>.md`. It tries the installed Cursor agent first, then
|
|
451
|
+
Codex, then Claude, all in non-interactive read-only modes against bounded Git
|
|
452
|
+
context. An unavailable, unauthenticated, failed, or empty provider falls
|
|
453
|
+
through to the next; if none works, release preparation continues without a
|
|
454
|
+
summary. The same file is included in the release PR and prepended to GitHub's
|
|
455
|
+
generated release notes, so repository and GitHub views share one source.
|
|
456
|
+
|
|
457
|
+
npm
|
|
445
458
|
uses `npm config get registry` and publishes to a local Verdaccio automatically.
|
|
446
459
|
Publishable JavaScript members compile once
|
|
447
460
|
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.225";
|
|
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.225",
|
|
29
|
+
"@dbx-tools/path": "0.6.225",
|
|
30
|
+
"@dbx-tools/shared-core": "0.6.225",
|
|
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.225",
|
|
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,8 @@ 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
|
+
/** Optional AI-generated release summary. Defaults to enabled. */
|
|
625
|
+
readonly releaseSummary?: boolean | ReleaseSummaryOptions;
|
|
624
626
|
/** Set to `false` to omit normal npm workspace publication. */
|
|
625
627
|
readonly nodeRelease?: boolean;
|
|
626
628
|
/** Unified dbx-tools release workflow, or no release surface. Defaults to `dbx-tools`. */
|
|
@@ -1540,6 +1542,7 @@ function initProject(
|
|
|
1540
1542
|
nodeRelease: options.nodeRelease,
|
|
1541
1543
|
docs: options.releaseDocs,
|
|
1542
1544
|
pythonRoot: options.releasePythonRoot,
|
|
1545
|
+
summary: options.releaseSummary,
|
|
1543
1546
|
});
|
|
1544
1547
|
}
|
|
1545
1548
|
}
|
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,11 @@ export interface DBXToolsReleaseOptions {
|
|
|
38
53
|
readonly docs?: ReleaseDocsOptions;
|
|
39
54
|
/** Python package root passed to local release preparation. */
|
|
40
55
|
readonly pythonRoot?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Generate a versioned release summary locally. Defaults to enabled with the
|
|
58
|
+
* standard Cursor, Codex, Claude fallback order.
|
|
59
|
+
*/
|
|
60
|
+
readonly summary?: boolean | ReleaseSummaryOptions;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
/** Locate the unified workflow so attached language workspaces can add jobs. */
|
|
@@ -320,6 +340,13 @@ export class DBXToolsRelease extends Component {
|
|
|
320
340
|
super(project);
|
|
321
341
|
const tagPrefix = options.tagPrefix ?? "v";
|
|
322
342
|
const releaseBranch = projectReleaseBranch(project);
|
|
343
|
+
const summary = options.summary ?? true;
|
|
344
|
+
const summaryArgs =
|
|
345
|
+
summary === false || (typeof summary === "object" && summary.providers?.length === 0)
|
|
346
|
+
? ["--no-release-summary"]
|
|
347
|
+
: typeof summary === "object" && summary.providers
|
|
348
|
+
? [`--release-summary-providers ${summary.providers.join(",")}`]
|
|
349
|
+
: [];
|
|
323
350
|
releaseTagPrefixes.set(project, tagPrefix);
|
|
324
351
|
if (options.nodeRelease !== false) nodeReleaseProjects.add(project);
|
|
325
352
|
applyTasks(project, {
|
|
@@ -344,6 +371,7 @@ export class DBXToolsRelease extends Component {
|
|
|
344
371
|
...(options.pythonRoot
|
|
345
372
|
? [`--python-root ${JSON.stringify(options.pythonRoot)}`]
|
|
346
373
|
: []),
|
|
374
|
+
...summaryArgs,
|
|
347
375
|
].join(" "),
|
|
348
376
|
),
|
|
349
377
|
receiveArgs: true,
|
|
@@ -359,8 +387,10 @@ export class DBXToolsRelease extends Component {
|
|
|
359
387
|
limitConcurrency: true,
|
|
360
388
|
concurrencyOptions: { group: "release", cancelInProgress: false },
|
|
361
389
|
});
|
|
390
|
+
const version = readWorkspaceVersion(project.outdir);
|
|
362
391
|
workflow.runName =
|
|
363
|
-
|
|
392
|
+
`release ${version} ` +
|
|
393
|
+
"${{ github.event_name == 'push' && github.sha || inputs.release_tag }}";
|
|
364
394
|
workflow.on({
|
|
365
395
|
push: { branches: [releaseBranch], paths: ["VERSION"] },
|
|
366
396
|
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,11 @@ 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("--no-release-summary", "skip optional AI release summary generation")
|
|
167
|
+
.option(
|
|
168
|
+
"--release-summary-providers <providers>",
|
|
169
|
+
"comma-separated provider order: cursor,codex,claude",
|
|
170
|
+
)
|
|
145
171
|
.option("--no-local-cargo", "skip local Cargo publication")
|
|
146
172
|
.option("--approve", "merge the release branch directly into the release base")
|
|
147
173
|
.action(
|
|
@@ -155,6 +181,8 @@ program
|
|
|
155
181
|
localRegistry: string;
|
|
156
182
|
localPypi: string;
|
|
157
183
|
pythonRoot?: string;
|
|
184
|
+
releaseSummary: boolean;
|
|
185
|
+
releaseSummaryProviders?: string;
|
|
158
186
|
localCargo: boolean;
|
|
159
187
|
approve: boolean;
|
|
160
188
|
}) => {
|
|
@@ -259,6 +287,14 @@ program
|
|
|
259
287
|
localCargo: opts.localCargo,
|
|
260
288
|
});
|
|
261
289
|
run(releaseRoot, process.execPath, [versionCheckScript]);
|
|
290
|
+
const releaseSummary = opts.releaseSummary
|
|
291
|
+
? await generateReleaseSummary({
|
|
292
|
+
root: releaseRoot,
|
|
293
|
+
version: next.version,
|
|
294
|
+
fromRef: `${opts.prefix}${next.base}`,
|
|
295
|
+
providers: releaseSummaryProviders(opts.releaseSummaryProviders),
|
|
296
|
+
})
|
|
297
|
+
: undefined;
|
|
262
298
|
|
|
263
299
|
git(releaseRoot, ["add", "-A"]);
|
|
264
300
|
const staged = git(releaseRoot, ["diff", "--cached", "--name-only"], { capture: true });
|
|
@@ -279,6 +315,7 @@ program
|
|
|
279
315
|
`Source commit: ${git(releaseRoot, ["rev-parse", `${releaseBranch}^`], { capture: true })}`,
|
|
280
316
|
"",
|
|
281
317
|
"Merging this PR updates VERSION on main and starts the public release workflow.",
|
|
318
|
+
...(releaseSummary ? ["", releaseSummary] : []),
|
|
282
319
|
].join("\n");
|
|
283
320
|
const githubEnvironment = {
|
|
284
321
|
...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
|
+
}
|