postgresai 0.14.0-dev.87 → 0.14.0-dev.89
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/bin/postgres-ai.ts
CHANGED
|
@@ -2115,10 +2115,10 @@ async function runCompose(args: string[], grafanaPassword?: string): Promise<num
|
|
|
2115
2115
|
}
|
|
2116
2116
|
}
|
|
2117
2117
|
|
|
2118
|
-
// On macOS, node-exporter can't mount host root filesystem - skip it
|
|
2118
|
+
// On macOS, self-node-exporter can't mount host root filesystem - skip it
|
|
2119
2119
|
const finalArgs = [...args];
|
|
2120
2120
|
if (process.platform === "darwin" && args.includes("up")) {
|
|
2121
|
-
finalArgs.push("--scale", "node-exporter=0");
|
|
2121
|
+
finalArgs.push("--scale", "self-node-exporter=0");
|
|
2122
2122
|
}
|
|
2123
2123
|
|
|
2124
2124
|
return new Promise<number>((resolve) => {
|
|
@@ -2496,15 +2496,15 @@ mon
|
|
|
2496
2496
|
// Known container names for cleanup
|
|
2497
2497
|
const MONITORING_CONTAINERS = [
|
|
2498
2498
|
"postgres-ai-config-init",
|
|
2499
|
-
"node-exporter",
|
|
2500
|
-
"cadvisor",
|
|
2499
|
+
"self-node-exporter",
|
|
2500
|
+
"self-cadvisor",
|
|
2501
2501
|
"grafana-with-datasources",
|
|
2502
2502
|
"sink-postgres",
|
|
2503
2503
|
"sink-prometheus",
|
|
2504
2504
|
"target-db",
|
|
2505
2505
|
"pgwatch-postgres",
|
|
2506
2506
|
"pgwatch-prometheus",
|
|
2507
|
-
"postgres-exporter
|
|
2507
|
+
"self-postgres-exporter",
|
|
2508
2508
|
"flask-pgss-api",
|
|
2509
2509
|
"sources-generator",
|
|
2510
2510
|
"postgres-reports",
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-dev.
|
|
13067
|
+
version: "0.14.0-dev.89",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -13100,7 +13100,8 @@ var package_default = {
|
|
|
13100
13100
|
test: "bun run embed-all && bun test",
|
|
13101
13101
|
"test:fast": "bun run embed-all && bun test --coverage=false",
|
|
13102
13102
|
"test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
13103
|
-
typecheck: "bun run embed-all && bunx tsc --noEmit"
|
|
13103
|
+
typecheck: "bun run embed-all && bunx tsc --noEmit",
|
|
13104
|
+
"release-notes": "bun run scripts/generate-release-notes.ts"
|
|
13104
13105
|
},
|
|
13105
13106
|
dependencies: {
|
|
13106
13107
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
@@ -15889,7 +15890,7 @@ var Result = import_lib.default.Result;
|
|
|
15889
15890
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15890
15891
|
var defaults = import_lib.default.defaults;
|
|
15891
15892
|
// package.json
|
|
15892
|
-
var version = "0.14.0-dev.
|
|
15893
|
+
var version = "0.14.0-dev.89";
|
|
15893
15894
|
var package_default2 = {
|
|
15894
15895
|
name: "postgresai",
|
|
15895
15896
|
version,
|
|
@@ -15928,7 +15929,8 @@ var package_default2 = {
|
|
|
15928
15929
|
test: "bun run embed-all && bun test",
|
|
15929
15930
|
"test:fast": "bun run embed-all && bun test --coverage=false",
|
|
15930
15931
|
"test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
15931
|
-
typecheck: "bun run embed-all && bunx tsc --noEmit"
|
|
15932
|
+
typecheck: "bun run embed-all && bunx tsc --noEmit",
|
|
15933
|
+
"release-notes": "bun run scripts/generate-release-notes.ts"
|
|
15932
15934
|
},
|
|
15933
15935
|
dependencies: {
|
|
15934
15936
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
@@ -30075,7 +30077,7 @@ async function runCompose(args, grafanaPassword) {
|
|
|
30075
30077
|
}
|
|
30076
30078
|
const finalArgs = [...args];
|
|
30077
30079
|
if (process.platform === "darwin" && args.includes("up")) {
|
|
30078
|
-
finalArgs.push("--scale", "node-exporter=0");
|
|
30080
|
+
finalArgs.push("--scale", "self-node-exporter=0");
|
|
30079
30081
|
}
|
|
30080
30082
|
return new Promise((resolve6) => {
|
|
30081
30083
|
const child = spawn2(cmd[0], [...cmd.slice(1), "-f", composeFile, ...finalArgs], {
|
|
@@ -30439,15 +30441,15 @@ mon.command("start").description("start monitoring services").action(async () =>
|
|
|
30439
30441
|
});
|
|
30440
30442
|
var MONITORING_CONTAINERS = [
|
|
30441
30443
|
"postgres-ai-config-init",
|
|
30442
|
-
"node-exporter",
|
|
30443
|
-
"cadvisor",
|
|
30444
|
+
"self-node-exporter",
|
|
30445
|
+
"self-cadvisor",
|
|
30444
30446
|
"grafana-with-datasources",
|
|
30445
30447
|
"sink-postgres",
|
|
30446
30448
|
"sink-prometheus",
|
|
30447
30449
|
"target-db",
|
|
30448
30450
|
"pgwatch-postgres",
|
|
30449
30451
|
"pgwatch-prometheus",
|
|
30450
|
-
"postgres-exporter
|
|
30452
|
+
"self-postgres-exporter",
|
|
30451
30453
|
"flask-pgss-api",
|
|
30452
30454
|
"sources-generator",
|
|
30453
30455
|
"postgres-reports"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresai",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
3
|
+
"version": "0.14.0-dev.89",
|
|
4
4
|
"description": "postgres_ai CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"test": "bun run embed-all && bun test",
|
|
37
37
|
"test:fast": "bun run embed-all && bun test --coverage=false",
|
|
38
38
|
"test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
39
|
-
"typecheck": "bun run embed-all && bunx tsc --noEmit"
|
|
39
|
+
"typecheck": "bun run embed-all && bunx tsc --noEmit",
|
|
40
|
+
"release-notes": "bun run scripts/generate-release-notes.ts"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Release Notes Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates release notes from git commit history using conventional commits.
|
|
6
|
+
* Analyzes commits between two references (tags, commits, or branches).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run scripts/generate-release-notes.ts [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --since <ref> Start reference (tag, commit, branch). Default: auto-detect last tag
|
|
13
|
+
* --until <ref> End reference. Default: HEAD
|
|
14
|
+
* --version <ver> Version string for the release (e.g., "0.14.0")
|
|
15
|
+
* --format <fmt> Output format: markdown (default), json
|
|
16
|
+
* --output <file> Write to file instead of stdout
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFileSync } from "child_process";
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
|
|
22
|
+
// Valid git ref pattern: alphanumeric, dots, hyphens, underscores, slashes, tildes, carets
|
|
23
|
+
const GIT_REF_PATTERN = /^[a-zA-Z0-9._~^/+-]+$/;
|
|
24
|
+
// Valid git SHA pattern: 7-40 hex characters
|
|
25
|
+
const GIT_SHA_PATTERN = /^[a-f0-9]{7,40}$/i;
|
|
26
|
+
|
|
27
|
+
function isValidGitRef(ref: string): boolean {
|
|
28
|
+
return GIT_REF_PATTERN.test(ref) && !ref.includes("..");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isValidGitSha(sha: string): boolean {
|
|
32
|
+
return GIT_SHA_PATTERN.test(sha);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Conventional commit types and their display names
|
|
36
|
+
const COMMIT_TYPES: Record<string, { title: string; emoji: string; priority: number }> = {
|
|
37
|
+
feat: { title: "New Features", emoji: "🚀", priority: 1 },
|
|
38
|
+
fix: { title: "Bug Fixes", emoji: "🐛", priority: 2 },
|
|
39
|
+
perf: { title: "Performance Improvements", emoji: "⚡", priority: 3 },
|
|
40
|
+
refactor: { title: "Refactoring", emoji: "♻️", priority: 4 },
|
|
41
|
+
docs: { title: "Documentation", emoji: "📚", priority: 5 },
|
|
42
|
+
chore: { title: "Maintenance", emoji: "🔧", priority: 6 },
|
|
43
|
+
test: { title: "Testing", emoji: "🧪", priority: 7 },
|
|
44
|
+
ci: { title: "CI/CD", emoji: "🔄", priority: 8 },
|
|
45
|
+
build: { title: "Build System", emoji: "📦", priority: 9 },
|
|
46
|
+
style: { title: "Code Style", emoji: "💅", priority: 10 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Scopes to highlight (CLI, monitoring, etc.)
|
|
50
|
+
const KNOWN_SCOPES = ["cli", "monitoring", "reporter", "grafana", "mcp", "prepare-db", "checkup", "deps", "ci", "formula", "pgai", "dashboards"];
|
|
51
|
+
|
|
52
|
+
// Author name mappings for deduplication
|
|
53
|
+
const AUTHOR_ALIASES: Record<string, string> = {
|
|
54
|
+
"Nik Samokhvalov": "Nikolay Samokhvalov",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Authors to exclude from contributors list (bots, AI assistants)
|
|
58
|
+
const EXCLUDED_AUTHORS = ["Claude", "dependabot[bot]", "github-actions[bot]"];
|
|
59
|
+
|
|
60
|
+
function normalizeAuthor(author: string): string {
|
|
61
|
+
return AUTHOR_ALIASES[author] || author;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isExcludedAuthor(author: string): boolean {
|
|
65
|
+
return EXCLUDED_AUTHORS.some((excluded) => author.toLowerCase().includes(excluded.toLowerCase()));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ParsedCommit {
|
|
69
|
+
hash: string;
|
|
70
|
+
shortHash: string;
|
|
71
|
+
type: string;
|
|
72
|
+
scope: string | null;
|
|
73
|
+
subject: string;
|
|
74
|
+
body: string;
|
|
75
|
+
breaking: boolean;
|
|
76
|
+
date: string;
|
|
77
|
+
author: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ReleaseNotes {
|
|
81
|
+
version: string;
|
|
82
|
+
date: string;
|
|
83
|
+
sinceRef: string;
|
|
84
|
+
untilRef: string;
|
|
85
|
+
commits: ParsedCommit[];
|
|
86
|
+
categories: Record<string, ParsedCommit[]>;
|
|
87
|
+
breaking: ParsedCommit[];
|
|
88
|
+
stats: {
|
|
89
|
+
total: number;
|
|
90
|
+
features: number;
|
|
91
|
+
fixes: number;
|
|
92
|
+
contributors: string[];
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function gitExec(args: string[]): string {
|
|
97
|
+
try {
|
|
98
|
+
const result = execFileSync("git", args, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 });
|
|
99
|
+
return result.trim();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseArgs(): { since: string; until: string; version: string; format: string; output: string | null } {
|
|
106
|
+
const args = process.argv.slice(2);
|
|
107
|
+
const result = { since: "", until: "HEAD", version: "", format: "markdown", output: null as string | null };
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < args.length; i++) {
|
|
110
|
+
const arg = args[i];
|
|
111
|
+
const next = args[i + 1];
|
|
112
|
+
switch (arg) {
|
|
113
|
+
case "--since":
|
|
114
|
+
result.since = next || "";
|
|
115
|
+
i++;
|
|
116
|
+
break;
|
|
117
|
+
case "--until":
|
|
118
|
+
result.until = next || "HEAD";
|
|
119
|
+
i++;
|
|
120
|
+
break;
|
|
121
|
+
case "--version":
|
|
122
|
+
result.version = next || "";
|
|
123
|
+
i++;
|
|
124
|
+
break;
|
|
125
|
+
case "--format":
|
|
126
|
+
result.format = next || "markdown";
|
|
127
|
+
i++;
|
|
128
|
+
break;
|
|
129
|
+
case "--output":
|
|
130
|
+
result.output = next || null;
|
|
131
|
+
i++;
|
|
132
|
+
break;
|
|
133
|
+
case "--help":
|
|
134
|
+
console.log(`
|
|
135
|
+
Release Notes Generator
|
|
136
|
+
|
|
137
|
+
Usage: bun run scripts/generate-release-notes.ts [options]
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
--since <ref> Start reference (tag, commit, or branch)
|
|
141
|
+
Default: auto-detect last release tag
|
|
142
|
+
--until <ref> End reference (tag, commit, or branch)
|
|
143
|
+
Default: HEAD
|
|
144
|
+
--version <ver> Version string for the release header
|
|
145
|
+
Default: derived from --until or current date
|
|
146
|
+
--format <fmt> Output format: markdown (default) or json
|
|
147
|
+
--output <file> Write to file instead of stdout
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
# Generate notes for upcoming 0.14.0 release
|
|
151
|
+
bun run scripts/generate-release-notes.ts --version 0.14.0
|
|
152
|
+
|
|
153
|
+
# Generate notes between two commits
|
|
154
|
+
bun run scripts/generate-release-notes.ts --since abc123 --until def456
|
|
155
|
+
|
|
156
|
+
# Output as JSON
|
|
157
|
+
bun run scripts/generate-release-notes.ts --format json
|
|
158
|
+
`);
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function detectLastTag(): string {
|
|
166
|
+
// Try to find the last version tag
|
|
167
|
+
const tags = gitExec(["tag", "--sort=-version:refname"]).split("\n").filter(Boolean);
|
|
168
|
+
|
|
169
|
+
// Look for semantic version tags
|
|
170
|
+
for (const tag of tags) {
|
|
171
|
+
if (/^v?\d+\.\d+/.test(tag)) {
|
|
172
|
+
return tag;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Fallback: find a meaningful starting point from commit messages
|
|
177
|
+
const versionCommits = gitExec(["log", "--grep=prepare-for-0.14\\|0.13\\|release", "--format=%H"]).split("\n").filter(Boolean);
|
|
178
|
+
if (versionCommits.length > 0 && versionCommits[0]) {
|
|
179
|
+
return versionCommits[0];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Last resort: 100 commits back
|
|
183
|
+
return "HEAD~100";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getCommitsBetween(since: string, until: string): string[] {
|
|
187
|
+
// Validate refs to prevent command injection
|
|
188
|
+
if (since && !isValidGitRef(since)) {
|
|
189
|
+
throw new Error(`Invalid git ref: ${since}`);
|
|
190
|
+
}
|
|
191
|
+
if (!isValidGitRef(until)) {
|
|
192
|
+
throw new Error(`Invalid git ref: ${until}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Get commit hashes between the two refs
|
|
196
|
+
const range = since ? `${since}..${until}` : until;
|
|
197
|
+
const output = gitExec(["log", range, "--format=%H", "--no-merges"]);
|
|
198
|
+
return output.split("\n").filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseCommit(hash: string): ParsedCommit | null {
|
|
202
|
+
// Validate hash to prevent command injection
|
|
203
|
+
if (!isValidGitSha(hash)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Use null byte as delimiter (unlikely to appear in commit messages)
|
|
208
|
+
const format = "%H%x00%h%x00%s%x00%b%x00%ad%x00%an";
|
|
209
|
+
const output = gitExec(["log", "-1", `--format=${format}`, "--date=short", hash]);
|
|
210
|
+
|
|
211
|
+
if (!output) return null;
|
|
212
|
+
|
|
213
|
+
const parts = output.split("\x00");
|
|
214
|
+
const [fullHash, shortHash, subject, body, date, author] = parts;
|
|
215
|
+
|
|
216
|
+
if (!subject) return null;
|
|
217
|
+
|
|
218
|
+
const trimmedBody = (body || "").trim();
|
|
219
|
+
|
|
220
|
+
// Parse conventional commit format: type(scope): subject
|
|
221
|
+
// Also handle: type: subject, type!: subject (breaking)
|
|
222
|
+
const match = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
|
|
223
|
+
|
|
224
|
+
let type = "other";
|
|
225
|
+
let scope: string | null = null;
|
|
226
|
+
let breaking = false;
|
|
227
|
+
let cleanSubject = subject;
|
|
228
|
+
|
|
229
|
+
if (match) {
|
|
230
|
+
type = match[1]?.toLowerCase() || "other";
|
|
231
|
+
scope = match[2] || null;
|
|
232
|
+
breaking = !!match[3] || trimmedBody.includes("BREAKING CHANGE");
|
|
233
|
+
cleanSubject = match[4] || subject;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Normalize type aliases
|
|
237
|
+
if (type === "feature") type = "feat";
|
|
238
|
+
if (type === "bugfix") type = "fix";
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
hash: fullHash || hash,
|
|
242
|
+
shortHash: shortHash || hash.slice(0, 7),
|
|
243
|
+
type,
|
|
244
|
+
scope,
|
|
245
|
+
subject: cleanSubject,
|
|
246
|
+
body: trimmedBody,
|
|
247
|
+
breaking,
|
|
248
|
+
date: date || new Date().toISOString().split("T")[0] || "",
|
|
249
|
+
author: (author || "").trim(),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function categorizeCommits(commits: ParsedCommit[]): Record<string, ParsedCommit[]> {
|
|
254
|
+
const categories: Record<string, ParsedCommit[]> = {};
|
|
255
|
+
|
|
256
|
+
for (const commit of commits) {
|
|
257
|
+
const type = COMMIT_TYPES[commit.type] ? commit.type : "other";
|
|
258
|
+
if (!categories[type]) {
|
|
259
|
+
categories[type] = [];
|
|
260
|
+
}
|
|
261
|
+
categories[type].push(commit);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return categories;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function generateMarkdown(notes: ReleaseNotes): string {
|
|
268
|
+
const lines: string[] = [];
|
|
269
|
+
|
|
270
|
+
// Header
|
|
271
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
272
|
+
lines.push(`# Release ${notes.version || "Notes"}`);
|
|
273
|
+
lines.push("");
|
|
274
|
+
lines.push(`**Release Date:** ${dateStr}`);
|
|
275
|
+
lines.push("");
|
|
276
|
+
|
|
277
|
+
// Stats summary
|
|
278
|
+
lines.push("## Summary");
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push(`This release includes **${notes.stats.total}** changes:`);
|
|
281
|
+
lines.push(`- ${notes.stats.features} new features`);
|
|
282
|
+
lines.push(`- ${notes.stats.fixes} bug fixes`);
|
|
283
|
+
lines.push("");
|
|
284
|
+
|
|
285
|
+
// Breaking changes (if any)
|
|
286
|
+
if (notes.breaking.length > 0) {
|
|
287
|
+
lines.push("## Breaking Changes");
|
|
288
|
+
lines.push("");
|
|
289
|
+
for (const commit of notes.breaking) {
|
|
290
|
+
const scopeStr = commit.scope ? `**${commit.scope}:** ` : "";
|
|
291
|
+
lines.push(`- ${scopeStr}${commit.subject}`);
|
|
292
|
+
}
|
|
293
|
+
lines.push("");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Categories sorted by priority
|
|
297
|
+
const sortedTypes = Object.keys(notes.categories).sort((a, b) => {
|
|
298
|
+
const pa = COMMIT_TYPES[a]?.priority ?? 99;
|
|
299
|
+
const pb = COMMIT_TYPES[b]?.priority ?? 99;
|
|
300
|
+
return pa - pb;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
for (const type of sortedTypes) {
|
|
304
|
+
const commits = notes.categories[type];
|
|
305
|
+
if (!commits || commits.length === 0) continue;
|
|
306
|
+
|
|
307
|
+
const typeInfo = COMMIT_TYPES[type] || { title: "Other Changes", emoji: "📝", priority: 99 };
|
|
308
|
+
lines.push(`## ${typeInfo.emoji} ${typeInfo.title}`);
|
|
309
|
+
lines.push("");
|
|
310
|
+
|
|
311
|
+
// Group by scope within each type
|
|
312
|
+
const byScope: Record<string, ParsedCommit[]> = {};
|
|
313
|
+
for (const commit of commits) {
|
|
314
|
+
const scope = commit.scope || "_general";
|
|
315
|
+
if (!byScope[scope]) byScope[scope] = [];
|
|
316
|
+
byScope[scope].push(commit);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Sort scopes: known scopes first, then alphabetically
|
|
320
|
+
const scopes = Object.keys(byScope).sort((a, b) => {
|
|
321
|
+
if (a === "_general") return 1;
|
|
322
|
+
if (b === "_general") return -1;
|
|
323
|
+
const aKnown = KNOWN_SCOPES.includes(a);
|
|
324
|
+
const bKnown = KNOWN_SCOPES.includes(b);
|
|
325
|
+
if (aKnown && !bKnown) return -1;
|
|
326
|
+
if (!aKnown && bKnown) return 1;
|
|
327
|
+
return a.localeCompare(b);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
for (const scope of scopes) {
|
|
331
|
+
const scopeCommits = byScope[scope] || [];
|
|
332
|
+
if (scope !== "_general" && scopeCommits.length > 0) {
|
|
333
|
+
lines.push(`### ${scope}`);
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
for (const commit of scopeCommits) {
|
|
337
|
+
lines.push(`- ${commit.subject} (\`${commit.shortHash}\`)`);
|
|
338
|
+
}
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Contributors
|
|
344
|
+
if (notes.stats.contributors.length > 0) {
|
|
345
|
+
lines.push("## Contributors");
|
|
346
|
+
lines.push("");
|
|
347
|
+
lines.push("Thank you to all contributors:");
|
|
348
|
+
lines.push("");
|
|
349
|
+
for (const contributor of notes.stats.contributors.sort()) {
|
|
350
|
+
lines.push(`- ${contributor}`);
|
|
351
|
+
}
|
|
352
|
+
lines.push("");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function generateJson(notes: ReleaseNotes): string {
|
|
359
|
+
return JSON.stringify(notes, null, 2);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function main() {
|
|
363
|
+
const args = parseArgs();
|
|
364
|
+
|
|
365
|
+
// Determine the range
|
|
366
|
+
const since = args.since || detectLastTag();
|
|
367
|
+
const until = args.until;
|
|
368
|
+
|
|
369
|
+
const log = (msg: string) => process.stderr.write(msg + "\n");
|
|
370
|
+
log(`Analyzing commits from ${since} to ${until}...`);
|
|
371
|
+
|
|
372
|
+
// Get and parse commits
|
|
373
|
+
const hashes = getCommitsBetween(since, until);
|
|
374
|
+
log(`Found ${hashes.length} commits to analyze`);
|
|
375
|
+
|
|
376
|
+
const commits: ParsedCommit[] = [];
|
|
377
|
+
for (const hash of hashes) {
|
|
378
|
+
const parsed = parseCommit(hash);
|
|
379
|
+
if (parsed) {
|
|
380
|
+
commits.push(parsed);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Build release notes structure
|
|
385
|
+
const categories = categorizeCommits(commits);
|
|
386
|
+
const breaking = commits.filter((c) => c.breaking);
|
|
387
|
+
|
|
388
|
+
// Normalize author names and exclude bots/AI
|
|
389
|
+
const contributors = [
|
|
390
|
+
...new Set(
|
|
391
|
+
commits
|
|
392
|
+
.map((c) => normalizeAuthor(c.author))
|
|
393
|
+
.filter((author) => author && !isExcludedAuthor(author))
|
|
394
|
+
),
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
const notes: ReleaseNotes = {
|
|
398
|
+
version: args.version || "",
|
|
399
|
+
date: new Date().toISOString().split("T")[0] || "",
|
|
400
|
+
sinceRef: since,
|
|
401
|
+
untilRef: until,
|
|
402
|
+
commits,
|
|
403
|
+
categories,
|
|
404
|
+
breaking,
|
|
405
|
+
stats: {
|
|
406
|
+
total: commits.length,
|
|
407
|
+
features: categories["feat"]?.length || 0,
|
|
408
|
+
fixes: categories["fix"]?.length || 0,
|
|
409
|
+
contributors,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Generate output
|
|
414
|
+
let output: string;
|
|
415
|
+
if (args.format === "json") {
|
|
416
|
+
output = generateJson(notes);
|
|
417
|
+
} else {
|
|
418
|
+
output = generateMarkdown(notes);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Write output
|
|
422
|
+
if (args.output) {
|
|
423
|
+
fs.writeFileSync(args.output, output, "utf8");
|
|
424
|
+
log(`Release notes written to: ${args.output}`);
|
|
425
|
+
} else {
|
|
426
|
+
console.log(output);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
main().catch((err) => {
|
|
431
|
+
console.error("Error generating release notes:", err);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
});
|
|
@@ -215,7 +215,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
215
215
|
} finally {
|
|
216
216
|
await pg.cleanup();
|
|
217
217
|
}
|
|
218
|
-
});
|
|
218
|
+
}, { timeout: 15000 });
|
|
219
219
|
|
|
220
220
|
test("requires explicit monitoring password in non-interactive mode", async () => {
|
|
221
221
|
pg = await createTempPostgres();
|
|
@@ -239,7 +239,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
239
239
|
} finally {
|
|
240
240
|
await pg.cleanup();
|
|
241
241
|
}
|
|
242
|
-
});
|
|
242
|
+
}, { timeout: 15000 });
|
|
243
243
|
|
|
244
244
|
test(
|
|
245
245
|
"fixes slightly-off permissions idempotently",
|
|
@@ -375,6 +375,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
375
375
|
}
|
|
376
376
|
);
|
|
377
377
|
|
|
378
|
+
// 15s timeout for PostgreSQL startup + two CLI init commands in slow CI
|
|
378
379
|
test("--reset-password updates the monitoring role login password", async () => {
|
|
379
380
|
pg = await createTempPostgres();
|
|
380
381
|
|
|
@@ -405,7 +406,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
405
406
|
} finally {
|
|
406
407
|
await pg.cleanup();
|
|
407
408
|
}
|
|
408
|
-
});
|
|
409
|
+
}, { timeout: 15000 });
|
|
409
410
|
|
|
410
411
|
// 60s timeout for PostgreSQL startup + multiple SQL queries in slow CI
|
|
411
412
|
test("explain_generic validates input and prevents SQL injection", async () => {
|