@releasekit/notes 0.2.0-next.0 → 0.2.0-next.10
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/dist/{chunk-NHDLQLG2.js → chunk-ACXCEHQT.js} +177 -55
- package/dist/cli.cjs +239 -108
- package/dist/cli.js +11 -2
- package/dist/index.cjs +212 -90
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -256,6 +256,10 @@ function writeMarkdown(outputPath, contexts, config, dryRun) {
|
|
|
256
256
|
if (!fs2.existsSync(dir)) {
|
|
257
257
|
fs2.mkdirSync(dir, { recursive: true });
|
|
258
258
|
}
|
|
259
|
+
if (outputPath === "-") {
|
|
260
|
+
process.stdout.write(content);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
259
263
|
if (config.updateStrategy === "prepend" && fs2.existsSync(outputPath) && contexts.length === 1) {
|
|
260
264
|
const firstContext = contexts[0];
|
|
261
265
|
if (firstContext) {
|
|
@@ -307,7 +311,7 @@ function writeJson(outputPath, contexts, dryRun) {
|
|
|
307
311
|
// src/core/pipeline.ts
|
|
308
312
|
import * as fs8 from "fs";
|
|
309
313
|
import * as path6 from "path";
|
|
310
|
-
import { debug, info as info4, success as success4, warn as
|
|
314
|
+
import { debug, info as info4, success as success4, warn as warn3 } from "@releasekit/core";
|
|
311
315
|
|
|
312
316
|
// src/llm/defaults.ts
|
|
313
317
|
var LLM_DEFAULTS = {
|
|
@@ -516,15 +520,26 @@ Entries:
|
|
|
516
520
|
Output only valid JSON, nothing else:`;
|
|
517
521
|
function buildCustomCategorizePrompt(categories) {
|
|
518
522
|
const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
|
|
523
|
+
const developerCategory = categories.find((c) => c.name === "Developer");
|
|
524
|
+
let scopeInstructions = "";
|
|
525
|
+
if (developerCategory) {
|
|
526
|
+
const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
|
|
527
|
+
if (scopeMatch?.[1]) {
|
|
528
|
+
const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
529
|
+
if (scopes.length > 0) {
|
|
530
|
+
scopeInstructions = `
|
|
531
|
+
|
|
532
|
+
For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
519
537
|
return `You are categorizing changelog entries for a software release.
|
|
520
538
|
|
|
521
|
-
Given the following entries, group them into the specified categories. Only use the categories listed below
|
|
539
|
+
Given the following entries, group them into the specified categories. Only use the categories listed below in this exact order:
|
|
522
540
|
|
|
523
541
|
Categories:
|
|
524
|
-
${categoryList}
|
|
525
|
-
|
|
526
|
-
For entries in categories that involve internal/developer changes, set a "scope" field on those entries with a short subcategory label (e.g., "CI", "Dependencies", "Testing", "Code Quality", "Build System").
|
|
527
|
-
|
|
542
|
+
${categoryList}${scopeInstructions}
|
|
528
543
|
Output a JSON object with two fields:
|
|
529
544
|
- "categories": an object where keys are category names and values are arrays of entry indices (0-based)
|
|
530
545
|
- "scopes": an object where keys are entry indices (as strings) and values are scope labels
|
|
@@ -556,7 +571,8 @@ async function categorizeEntries(provider, entries, context) {
|
|
|
556
571
|
entries[idx] = { ...entries[idx], scope };
|
|
557
572
|
}
|
|
558
573
|
}
|
|
559
|
-
for (const [category,
|
|
574
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
575
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
560
576
|
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
561
577
|
if (categoryEntries.length > 0) {
|
|
562
578
|
result.push({ category, entries: categoryEntries });
|
|
@@ -564,7 +580,8 @@ async function categorizeEntries(provider, entries, context) {
|
|
|
564
580
|
}
|
|
565
581
|
} else {
|
|
566
582
|
const categoryMap = parsed;
|
|
567
|
-
for (const [category,
|
|
583
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
584
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
568
585
|
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
569
586
|
if (categoryEntries.length > 0) {
|
|
570
587
|
result.push({ category, entries: categoryEntries });
|
|
@@ -622,6 +639,113 @@ async function enhanceEntries(provider, entries, context, concurrency = LLM_DEFA
|
|
|
622
639
|
return results;
|
|
623
640
|
}
|
|
624
641
|
|
|
642
|
+
// src/llm/tasks/enhance-and-categorize.ts
|
|
643
|
+
import { warn as warn2 } from "@releasekit/core";
|
|
644
|
+
|
|
645
|
+
// src/utils/retry.ts
|
|
646
|
+
function sleep(ms) {
|
|
647
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
648
|
+
}
|
|
649
|
+
async function withRetry(fn, options = {}) {
|
|
650
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
651
|
+
const initialDelay = options.initialDelay ?? 1e3;
|
|
652
|
+
const maxDelay = options.maxDelay ?? 3e4;
|
|
653
|
+
const backoffFactor = options.backoffFactor ?? 2;
|
|
654
|
+
let lastError;
|
|
655
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
656
|
+
try {
|
|
657
|
+
return await fn();
|
|
658
|
+
} catch (error) {
|
|
659
|
+
lastError = error;
|
|
660
|
+
if (attempt < maxAttempts - 1) {
|
|
661
|
+
const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
|
|
662
|
+
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
663
|
+
await sleep(Math.max(0, base + jitter));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
throw lastError;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/llm/tasks/enhance-and-categorize.ts
|
|
671
|
+
function buildPrompt(entries, categories, style) {
|
|
672
|
+
const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
|
|
673
|
+
const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
|
|
674
|
+
const categorySection = categories ? `Categories (use ONLY these):
|
|
675
|
+
${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
|
|
676
|
+
return `You are generating release notes for a software project. Given the following changelog entries, do two things:
|
|
677
|
+
|
|
678
|
+
1. **Rewrite** each entry as a clear, user-friendly description
|
|
679
|
+
2. **Categorize** each entry into the appropriate category
|
|
680
|
+
|
|
681
|
+
Style guidelines:
|
|
682
|
+
- ${styleText}
|
|
683
|
+
- Be concise (1 short sentence per entry)
|
|
684
|
+
- Focus on what changed, not implementation details
|
|
685
|
+
|
|
686
|
+
${categorySection}
|
|
687
|
+
|
|
688
|
+
${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
|
|
689
|
+
|
|
690
|
+
Entries:
|
|
691
|
+
${entriesText}
|
|
692
|
+
|
|
693
|
+
Output a JSON object with:
|
|
694
|
+
- "entries": array of objects, one per input entry (same order), each with: { "description": "rewritten text", "category": "CategoryName", "scope": "optional subcategory label or null" }
|
|
695
|
+
|
|
696
|
+
Output only valid JSON, nothing else:`;
|
|
697
|
+
}
|
|
698
|
+
async function enhanceAndCategorize(provider, entries, context) {
|
|
699
|
+
if (entries.length === 0) {
|
|
700
|
+
return { enhancedEntries: [], categories: [] };
|
|
701
|
+
}
|
|
702
|
+
const retryOpts = LLM_DEFAULTS.retry;
|
|
703
|
+
try {
|
|
704
|
+
return await withRetry(async () => {
|
|
705
|
+
const prompt = buildPrompt(entries, context.categories, context.style);
|
|
706
|
+
const response = await provider.complete(prompt);
|
|
707
|
+
const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
708
|
+
const parsed = JSON.parse(cleaned);
|
|
709
|
+
if (!Array.isArray(parsed.entries)) {
|
|
710
|
+
throw new Error('Response missing "entries" array');
|
|
711
|
+
}
|
|
712
|
+
const enhancedEntries = entries.map((original, i) => {
|
|
713
|
+
const result = parsed.entries[i];
|
|
714
|
+
if (!result) return original;
|
|
715
|
+
return {
|
|
716
|
+
...original,
|
|
717
|
+
description: result.description || original.description,
|
|
718
|
+
scope: result.scope || original.scope
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
const categoryMap = /* @__PURE__ */ new Map();
|
|
722
|
+
for (let i = 0; i < parsed.entries.length; i++) {
|
|
723
|
+
const result = parsed.entries[i];
|
|
724
|
+
const category = result?.category || "General";
|
|
725
|
+
const entry = enhancedEntries[i];
|
|
726
|
+
if (!entry) continue;
|
|
727
|
+
if (!categoryMap.has(category)) {
|
|
728
|
+
categoryMap.set(category, []);
|
|
729
|
+
}
|
|
730
|
+
categoryMap.get(category).push(entry);
|
|
731
|
+
}
|
|
732
|
+
const categories = [];
|
|
733
|
+
for (const [category, catEntries] of categoryMap) {
|
|
734
|
+
categories.push({ category, entries: catEntries });
|
|
735
|
+
}
|
|
736
|
+
return { enhancedEntries, categories };
|
|
737
|
+
}, retryOpts);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
warn2(
|
|
740
|
+
`Combined enhance+categorize failed after ${retryOpts.maxAttempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
741
|
+
);
|
|
742
|
+
return {
|
|
743
|
+
enhancedEntries: entries,
|
|
744
|
+
categories: [{ category: "General", entries }]
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
625
749
|
// src/llm/tasks/release-notes.ts
|
|
626
750
|
var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
|
|
627
751
|
|
|
@@ -1089,40 +1213,23 @@ function renderTemplate(templatePath, context, engine) {
|
|
|
1089
1213
|
return renderComposable(templatePath, context, engine);
|
|
1090
1214
|
}
|
|
1091
1215
|
|
|
1092
|
-
// src/
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
async function withRetry(fn, options = {}) {
|
|
1097
|
-
const maxAttempts = options.maxAttempts ?? 3;
|
|
1098
|
-
const initialDelay = options.initialDelay ?? 1e3;
|
|
1099
|
-
const maxDelay = options.maxDelay ?? 3e4;
|
|
1100
|
-
const backoffFactor = options.backoffFactor ?? 2;
|
|
1101
|
-
let lastError;
|
|
1102
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1103
|
-
try {
|
|
1104
|
-
return await fn();
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
lastError = error;
|
|
1107
|
-
if (attempt < maxAttempts - 1) {
|
|
1108
|
-
const base = Math.min(initialDelay * backoffFactor ** attempt, maxDelay);
|
|
1109
|
-
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
1110
|
-
await sleep(Math.max(0, base + jitter));
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1216
|
+
// src/core/pipeline.ts
|
|
1217
|
+
function extractVersionFromTag(tag) {
|
|
1218
|
+
if (tag.includes("@") && !tag.startsWith("@")) {
|
|
1219
|
+
return tag;
|
|
1113
1220
|
}
|
|
1114
|
-
|
|
1221
|
+
return tag.replace(/^v/, "");
|
|
1115
1222
|
}
|
|
1116
|
-
|
|
1117
|
-
// src/core/pipeline.ts
|
|
1118
1223
|
function generateCompareUrl(repoUrl, from, to) {
|
|
1224
|
+
const fromVersion = extractVersionFromTag(from);
|
|
1225
|
+
const toVersion = extractVersionFromTag(to);
|
|
1119
1226
|
if (/gitlab\.com/i.test(repoUrl)) {
|
|
1120
|
-
return `${repoUrl}/-/compare/${
|
|
1227
|
+
return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
|
|
1121
1228
|
}
|
|
1122
1229
|
if (/bitbucket\.org/i.test(repoUrl)) {
|
|
1123
|
-
return `${repoUrl}/branches/compare/${
|
|
1230
|
+
return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
|
|
1124
1231
|
}
|
|
1125
|
-
return `${repoUrl}/compare/${
|
|
1232
|
+
return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
|
|
1126
1233
|
}
|
|
1127
1234
|
function createTemplateContext(pkg) {
|
|
1128
1235
|
const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
|
|
@@ -1181,10 +1288,30 @@ async function processWithLLM(context, config) {
|
|
|
1181
1288
|
};
|
|
1182
1289
|
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1183
1290
|
info4(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1184
|
-
if (tasks.enhance) {
|
|
1185
|
-
info4("Enhancing entries with LLM...");
|
|
1186
|
-
|
|
1187
|
-
|
|
1291
|
+
if (tasks.enhance && tasks.categorize) {
|
|
1292
|
+
info4("Enhancing and categorizing entries with LLM...");
|
|
1293
|
+
const result = await enhanceAndCategorize(provider, context.entries, llmContext);
|
|
1294
|
+
enhanced.entries = result.enhancedEntries;
|
|
1295
|
+
enhanced.categories = {};
|
|
1296
|
+
for (const cat of result.categories) {
|
|
1297
|
+
enhanced.categories[cat.category] = cat.entries;
|
|
1298
|
+
}
|
|
1299
|
+
info4(`Enhanced ${enhanced.entries.length} entries into ${result.categories.length} categories`);
|
|
1300
|
+
} else {
|
|
1301
|
+
if (tasks.enhance) {
|
|
1302
|
+
info4("Enhancing entries with LLM...");
|
|
1303
|
+
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1304
|
+
info4(`Enhanced ${enhanced.entries.length} entries`);
|
|
1305
|
+
}
|
|
1306
|
+
if (tasks.categorize) {
|
|
1307
|
+
info4("Categorizing entries with LLM...");
|
|
1308
|
+
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1309
|
+
enhanced.categories = {};
|
|
1310
|
+
for (const cat of categorized) {
|
|
1311
|
+
enhanced.categories[cat.category] = cat.entries;
|
|
1312
|
+
}
|
|
1313
|
+
info4(`Created ${categorized.length} categories`);
|
|
1314
|
+
}
|
|
1188
1315
|
}
|
|
1189
1316
|
if (tasks.summarize) {
|
|
1190
1317
|
info4("Summarizing entries with LLM...");
|
|
@@ -1193,25 +1320,16 @@ async function processWithLLM(context, config) {
|
|
|
1193
1320
|
info4("Summary generated successfully");
|
|
1194
1321
|
debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1195
1322
|
} else {
|
|
1196
|
-
|
|
1323
|
+
warn3("Summary generation returned empty result");
|
|
1197
1324
|
}
|
|
1198
1325
|
}
|
|
1199
|
-
if (tasks.categorize) {
|
|
1200
|
-
info4("Categorizing entries with LLM...");
|
|
1201
|
-
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1202
|
-
enhanced.categories = {};
|
|
1203
|
-
for (const cat of categorized) {
|
|
1204
|
-
enhanced.categories[cat.category] = cat.entries;
|
|
1205
|
-
}
|
|
1206
|
-
info4(`Created ${categorized.length} categories`);
|
|
1207
|
-
}
|
|
1208
1326
|
if (tasks.releaseNotes) {
|
|
1209
1327
|
info4("Generating release notes with LLM...");
|
|
1210
1328
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1211
1329
|
if (enhanced.releaseNotes) {
|
|
1212
1330
|
info4("Release notes generated successfully");
|
|
1213
1331
|
} else {
|
|
1214
|
-
|
|
1332
|
+
warn3("Release notes generation returned empty result");
|
|
1215
1333
|
}
|
|
1216
1334
|
}
|
|
1217
1335
|
return {
|
|
@@ -1220,8 +1338,8 @@ async function processWithLLM(context, config) {
|
|
|
1220
1338
|
enhanced
|
|
1221
1339
|
};
|
|
1222
1340
|
} catch (error) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1341
|
+
warn3(`LLM processing failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1342
|
+
warn3("Falling back to raw entries");
|
|
1225
1343
|
return context;
|
|
1226
1344
|
}
|
|
1227
1345
|
}
|
|
@@ -1254,6 +1372,10 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1254
1372
|
info4("--- End Preview ---");
|
|
1255
1373
|
return;
|
|
1256
1374
|
}
|
|
1375
|
+
if (outputPath === "-") {
|
|
1376
|
+
process.stdout.write(result.content);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1257
1379
|
const dir = path6.dirname(outputPath);
|
|
1258
1380
|
if (!fs8.existsSync(dir)) {
|
|
1259
1381
|
fs8.mkdirSync(dir, { recursive: true });
|
|
@@ -1292,17 +1414,17 @@ async function runPipeline(input, config, dryRun) {
|
|
|
1292
1414
|
}
|
|
1293
1415
|
const firstContext = contexts[0];
|
|
1294
1416
|
if (!firstContext) {
|
|
1295
|
-
|
|
1417
|
+
warn3("No context available for GitHub release");
|
|
1296
1418
|
break;
|
|
1297
1419
|
}
|
|
1298
1420
|
const repoUrl = firstContext.repoUrl;
|
|
1299
1421
|
if (!repoUrl) {
|
|
1300
|
-
|
|
1422
|
+
warn3("No repo URL available, cannot create GitHub release");
|
|
1301
1423
|
break;
|
|
1302
1424
|
}
|
|
1303
1425
|
const parsed = parseRepoUrl(repoUrl);
|
|
1304
1426
|
if (!parsed) {
|
|
1305
|
-
|
|
1427
|
+
warn3(`Could not parse repo URL: ${repoUrl}`);
|
|
1306
1428
|
break;
|
|
1307
1429
|
}
|
|
1308
1430
|
await createGitHubRelease(firstContext, {
|