feat(coding-agent): add startup changelog display modes
(cherry picked from commit bed594fecd1eae1917f3f047f883da5ba83317f9)
This commit is contained in:
committed by
can1357
parent
091f670ea0
commit
a4dc5a094a
@@ -77,6 +77,9 @@
|
||||
- Fixed omp worktree clear prematurely deleting active task-isolation sandboxes owned by running subagents.
|
||||
- Fixed /vibe mode preventing the director from completing parent tasks after verifying worker results by keeping the built-in todo tool active.
|
||||
- Fixed numeric GitHub issue and pull request autocomplete being suppressed inside skill slash-command arguments.
|
||||
### Changed
|
||||
|
||||
- Startup release notes now default to a compact change-count summary. Use `startup.changelogMode` (`summary` | `expanded` | `hidden`) to control them; legacy `collapseChangelog` choices migrate automatically.
|
||||
|
||||
## [17.1.7] - 2026-07-27
|
||||
|
||||
|
||||
@@ -1811,17 +1811,42 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
collapseChangelog: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
"startup.changelogMode": {
|
||||
type: "enum",
|
||||
values: ["summary", "expanded", "hidden"] as const,
|
||||
default: "summary",
|
||||
ui: {
|
||||
tab: "interaction",
|
||||
group: "Startup & Updates",
|
||||
label: "Collapse Changelog",
|
||||
description: "Show condensed changelog after updates",
|
||||
label: "Startup Changelog",
|
||||
description: "Choose whether update notes start as a summary, full details, or stay hidden",
|
||||
options: [
|
||||
{
|
||||
value: "summary",
|
||||
label: "Summary",
|
||||
description: "Show release and change counts with a /changelog hint",
|
||||
},
|
||||
{
|
||||
value: "expanded",
|
||||
label: "Expanded",
|
||||
description: "Show the recent release notes in full",
|
||||
},
|
||||
{
|
||||
value: "hidden",
|
||||
label: "Hidden",
|
||||
description: "Do not show release notes on startup",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Legacy startup changelog boolean kept only as migration input for
|
||||
// startup.changelogMode; it is intentionally hidden from the settings UI.
|
||||
collapseChangelog: {
|
||||
type: "boolean",
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
"magicKeywords.enabled": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
|
||||
@@ -1346,6 +1346,31 @@ export class Settings {
|
||||
}
|
||||
delete raw.lastChangelogVersion;
|
||||
|
||||
// collapseChangelog (boolean) -> startup.changelogMode (enum). Preserve
|
||||
// every explicit legacy choice while giving new installs the schema's
|
||||
// "summary" default: true -> summary, false -> expanded. A separately
|
||||
// configured new mode always wins.
|
||||
const startupObj = isRecord(raw.startup) ? (raw.startup as Record<string, unknown>) : undefined;
|
||||
const legacyCollapseChangelog = typeof raw.collapseChangelog === "boolean" ? raw.collapseChangelog : undefined;
|
||||
const flatChangelogMode = raw["startup.changelogMode"];
|
||||
const normalizedFlatChangelogMode =
|
||||
flatChangelogMode === "summary" || flatChangelogMode === "expanded" || flatChangelogMode === "hidden"
|
||||
? flatChangelogMode
|
||||
: undefined;
|
||||
if (legacyCollapseChangelog !== undefined || normalizedFlatChangelogMode !== undefined) {
|
||||
if (!startupObj) {
|
||||
raw.startup = {};
|
||||
}
|
||||
const target = raw.startup as Record<string, unknown>;
|
||||
if (target.changelogMode === undefined) {
|
||||
target.changelogMode =
|
||||
normalizedFlatChangelogMode ??
|
||||
(legacyCollapseChangelog !== undefined ? (legacyCollapseChangelog ? "summary" : "expanded") : undefined);
|
||||
}
|
||||
}
|
||||
delete raw.collapseChangelog;
|
||||
delete raw["startup.changelogMode"];
|
||||
|
||||
// ask.timeout: ms -> seconds (if value > 1000, it's old ms format)
|
||||
if (raw.ask && typeof (raw.ask as Record<string, unknown>).timeout === "number") {
|
||||
const oldValue = (raw.ask as Record<string, unknown>).timeout as number;
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
type ScopedModel,
|
||||
} from "./config/model-resolver";
|
||||
import { ModelsConfigFile } from "./config/models-config";
|
||||
import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
|
||||
import { getDefault, type SettingPath, Settings, type SettingValue, settings } from "./config/settings";
|
||||
import { initializeWithSettings } from "./discovery";
|
||||
import {
|
||||
clearPluginRootsAndCaches,
|
||||
@@ -91,10 +91,12 @@ import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnab
|
||||
import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
|
||||
import type { LspStartupServerInfo } from "./tools";
|
||||
import {
|
||||
compareVersions,
|
||||
getChangelogPath,
|
||||
parseChangelog,
|
||||
parseChangelogVersion,
|
||||
readLastChangelogVersion,
|
||||
type StartupChangelogSelection,
|
||||
selectStartupChangelog,
|
||||
writeLastChangelogVersion,
|
||||
} from "./utils/changelog";
|
||||
@@ -415,7 +417,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
|
||||
async function runInteractiveMode(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
changelogMarkdown: string | undefined,
|
||||
startupChangelog: StartupChangelogSelection | undefined,
|
||||
notifs: (InteractiveModeNotify | null)[],
|
||||
versionCheckPromise: Promise<string | undefined>,
|
||||
initialMessages: string[],
|
||||
@@ -433,7 +435,7 @@ async function runInteractiveMode(
|
||||
const mode = new InteractiveMode(
|
||||
session,
|
||||
version,
|
||||
changelogMarkdown,
|
||||
startupChangelog,
|
||||
setExtensionUIContext,
|
||||
lspServers,
|
||||
mcpManager,
|
||||
@@ -679,7 +681,10 @@ async function resolveScopedModels(
|
||||
);
|
||||
}
|
||||
|
||||
async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
|
||||
async function getChangelogForDisplay(
|
||||
parsed: Args,
|
||||
mode: SettingValue<"startup.changelogMode">,
|
||||
): Promise<StartupChangelogSelection | undefined> {
|
||||
if (parsed.continue || parsed.resume || isForeignSessionImport(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -694,6 +699,13 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
||||
// Steady state: user already saw the current version's changelog. Skip the file read + parse.
|
||||
return undefined;
|
||||
}
|
||||
if (mode === "hidden") {
|
||||
const currentVersion = parseChangelogVersion(VERSION);
|
||||
if (currentVersion && compareVersions(currentVersion, parsedLastVersion) > 0) {
|
||||
await writeLastChangelogVersion(VERSION);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const changelogPath = getChangelogPath();
|
||||
const entries = await parseChangelog(changelogPath);
|
||||
@@ -701,11 +713,7 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
||||
if (startupChangelog.persistCurrentVersion) {
|
||||
await writeLastChangelogVersion(VERSION);
|
||||
}
|
||||
if (startupChangelog.markdown) {
|
||||
return startupChangelog.markdown;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return startupChangelog.markdown ? startupChangelog : undefined;
|
||||
}
|
||||
|
||||
const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
@@ -1667,7 +1675,12 @@ export async function runRootCommand(
|
||||
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
|
||||
} else if (isInteractive) {
|
||||
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
||||
const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
|
||||
const startupChangelog = await logger.time(
|
||||
"main:getChangelogForDisplay",
|
||||
getChangelogForDisplay,
|
||||
parsedArgs,
|
||||
settingsInstance.get("startup.changelogMode"),
|
||||
);
|
||||
|
||||
const modelScopeNotification = buildModelScopeNotification(
|
||||
scopedModels,
|
||||
@@ -1692,7 +1705,7 @@ export async function runRootCommand(
|
||||
await runInteractiveMode(
|
||||
session,
|
||||
VERSION,
|
||||
changelogMarkdown,
|
||||
startupChangelog,
|
||||
notifs,
|
||||
versionCheckPromise,
|
||||
initialArgs.messages,
|
||||
|
||||
@@ -128,6 +128,7 @@ import {
|
||||
} from "../tools/todo";
|
||||
import { vocalizer } from "../tts/vocalizer";
|
||||
import { renderTreeList } from "../tui/tree-list";
|
||||
import { formatStartupChangelogSummary, type StartupChangelogSelection } from "../utils/changelog";
|
||||
import { copyToClipboard } from "../utils/clipboard";
|
||||
import type { EventBus } from "../utils/event-bus";
|
||||
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
||||
@@ -556,7 +557,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
#cleanupUnsubscribe?: () => void;
|
||||
#signalTeardown?: SessionTeardown;
|
||||
readonly #version: string;
|
||||
readonly #changelogMarkdown: string | undefined;
|
||||
readonly #startupChangelog: StartupChangelogSelection | undefined;
|
||||
#planModePreviousTools: string[] | undefined;
|
||||
#goalModePreviousTools: string[] | undefined;
|
||||
#vibeModePreviousTools: string[] | undefined;
|
||||
@@ -664,7 +665,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
constructor(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
changelogMarkdown: string | undefined = undefined,
|
||||
startupChangelog: StartupChangelogSelection | undefined = undefined,
|
||||
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
||||
lspServers: LspStartupServerInfo[] | undefined = undefined,
|
||||
mcpManager?: MCPManager,
|
||||
@@ -676,7 +677,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.keybindings = KeybindingsManager.inMemory();
|
||||
this.agent = session.agent;
|
||||
this.#version = version;
|
||||
this.#changelogMarkdown = changelogMarkdown;
|
||||
this.#startupChangelog = startupChangelog;
|
||||
this.#toolUiContextSetter = setToolUIContext;
|
||||
this.lspServers = lspServers;
|
||||
this.mcpManager = mcpManager;
|
||||
@@ -943,19 +944,20 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
}
|
||||
|
||||
// Add changelog if provided
|
||||
if (this.#changelogMarkdown) {
|
||||
if (this.#startupChangelog && settings.get("startup.changelogMode") !== "hidden") {
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
if (settings.get("collapseChangelog")) {
|
||||
const versionMatch = this.#changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
||||
const latestVersion = versionMatch ? versionMatch[1] : this.#version;
|
||||
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
||||
this.ui.addChild(new Text(condensedText, 1, 0));
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
if (settings.get("startup.changelogMode") === "summary") {
|
||||
const summary = formatStartupChangelogSummary(this.#startupChangelog).replace(
|
||||
/\/changelog(?: full)?/g,
|
||||
command => theme.bold(command),
|
||||
);
|
||||
this.ui.addChild(new Text(summary, 1, 0));
|
||||
} else {
|
||||
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new Markdown(this.#changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new Markdown(this.#startupChangelog.markdown?.trim() ?? "", 1, 0, getMarkdownTheme()));
|
||||
}
|
||||
this.ui.addChild(new Spacer(1));
|
||||
this.ui.addChild(new DynamicBorder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,101 @@ export interface StartupChangelogSelection {
|
||||
persistCurrentVersion: boolean;
|
||||
truncated: boolean;
|
||||
selectedEntries: number;
|
||||
totalUnseenEntries: number;
|
||||
latestVersion: string | undefined;
|
||||
changeCount: number;
|
||||
categoryCounts: Record<string, number>;
|
||||
}
|
||||
|
||||
const CHANGELOG_CATEGORY_ORDER = [
|
||||
"Breaking Changes",
|
||||
"Added",
|
||||
"Changed",
|
||||
"Deprecated",
|
||||
"Removed",
|
||||
"Fixed",
|
||||
"Security",
|
||||
] as const;
|
||||
|
||||
function emptyStartupSelection(persistCurrentVersion: boolean): StartupChangelogSelection {
|
||||
return {
|
||||
markdown: undefined,
|
||||
persistCurrentVersion,
|
||||
truncated: false,
|
||||
selectedEntries: 0,
|
||||
totalUnseenEntries: 0,
|
||||
latestVersion: undefined,
|
||||
changeCount: 0,
|
||||
categoryCounts: {},
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeChangelogEntries(entries: readonly ChangelogEntry[]): {
|
||||
changeCount: number;
|
||||
categoryCounts: Record<string, number>;
|
||||
} {
|
||||
const categoryCounts: Record<string, number> = {};
|
||||
let changeCount = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
let category: string | undefined;
|
||||
for (const line of entry.content.split("\n")) {
|
||||
const heading = line.match(/^###\s+(.+?)\s*$/);
|
||||
if (heading) {
|
||||
category = heading[1];
|
||||
continue;
|
||||
}
|
||||
if (!category || !/^-\s+\S/.test(line)) continue;
|
||||
categoryCounts[category] = (categoryCounts[category] ?? 0) + 1;
|
||||
changeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return { changeCount, categoryCounts };
|
||||
}
|
||||
|
||||
function categoryLabel(category: string, count: number): string {
|
||||
if (category === "Breaking Changes") {
|
||||
return count === 1 ? "breaking change" : "breaking changes";
|
||||
}
|
||||
return category.toLowerCase();
|
||||
}
|
||||
|
||||
/** Format the compact, deterministic startup update notice. */
|
||||
export function formatStartupChangelogSummary(selection: StartupChangelogSelection): string {
|
||||
const latestVersion = selection.latestVersion;
|
||||
if (!latestVersion || selection.selectedEntries === 0) {
|
||||
return "Updated omp. Use /changelog for recent changes.";
|
||||
}
|
||||
|
||||
const releaseCount = selection.selectedEntries;
|
||||
const changeCount = selection.changeCount;
|
||||
const releaseWord = releaseCount === 1 ? "release" : "releases";
|
||||
const changeWord = changeCount === 1 ? "change" : "changes";
|
||||
const firstLine =
|
||||
releaseCount === 1
|
||||
? `Updated to v${latestVersion} · ${changeCount} ${changeWord} in 1 release`
|
||||
: `Updated to v${latestVersion} · ${changeCount} ${changeWord} across ${releaseCount} ${releaseWord}`;
|
||||
|
||||
const orderedCategories = [
|
||||
...CHANGELOG_CATEGORY_ORDER.filter(category => selection.categoryCounts[category]),
|
||||
...Object.keys(selection.categoryCounts)
|
||||
.filter(category => !(CHANGELOG_CATEGORY_ORDER as readonly string[]).includes(category))
|
||||
.sort(),
|
||||
];
|
||||
const breakdown = orderedCategories
|
||||
.map(
|
||||
category =>
|
||||
`${selection.categoryCounts[category]} ${categoryLabel(category, selection.categoryCounts[category])}`,
|
||||
)
|
||||
.join(" · ");
|
||||
const omittedReleases = selection.totalUnseenEntries - selection.selectedEntries;
|
||||
const detailHint =
|
||||
omittedReleases > 0
|
||||
? `+${omittedReleases} earlier ${omittedReleases === 1 ? "release" : "releases"} · Use /changelog full for history.`
|
||||
: "Use /changelog for details.";
|
||||
|
||||
return breakdown ? `${firstLine}\n${breakdown} · ${detailHint}` : `${firstLine}\n${detailHint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,16 +272,17 @@ export function selectStartupChangelog(
|
||||
): StartupChangelogSelection {
|
||||
const parsedLastVersion = parseChangelogVersion(lastVersion);
|
||||
if (!parsedLastVersion) {
|
||||
return { markdown: undefined, persistCurrentVersion: true, truncated: false, selectedEntries: 0 };
|
||||
return emptyStartupSelection(true);
|
||||
}
|
||||
const markerVersion = lastVersion ?? "";
|
||||
if (markerVersion === currentVersion) {
|
||||
return { markdown: undefined, persistCurrentVersion: false, truncated: false, selectedEntries: 0 };
|
||||
return emptyStartupSelection(false);
|
||||
}
|
||||
|
||||
const newEntries = getNewEntries(entries, markerVersion).slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
||||
const allNewEntries = getNewEntries(entries, markerVersion);
|
||||
const newEntries = allNewEntries.slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
||||
if (newEntries.length === 0) {
|
||||
return { markdown: undefined, persistCurrentVersion: false, truncated: false, selectedEntries: 0 };
|
||||
return emptyStartupSelection(false);
|
||||
}
|
||||
|
||||
const rendered = renderChangelogEntries(newEntries, {
|
||||
@@ -194,11 +290,16 @@ export function selectStartupChangelog(
|
||||
truncationHint: STARTUP_CHANGELOG_FULL_HINT,
|
||||
oldestFirst: false,
|
||||
});
|
||||
const summary = summarizeChangelogEntries(newEntries);
|
||||
const latestEntry = newEntries[0];
|
||||
return {
|
||||
markdown: rendered.markdown,
|
||||
persistCurrentVersion: true,
|
||||
truncated: rendered.truncated,
|
||||
selectedEntries: newEntries.length,
|
||||
totalUnseenEntries: allNewEntries.length,
|
||||
latestVersion: latestEntry ? `${latestEntry.major}.${latestEntry.minor}.${latestEntry.patch}` : undefined,
|
||||
...summary,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { removeWithRetries, VERSION } from "@oh-my-pi/pi-utils";
|
||||
import { Settings } from "../../src/config/settings";
|
||||
import {
|
||||
type ChangelogEntry,
|
||||
formatStartupChangelogSummary,
|
||||
getNewEntries,
|
||||
parseChangelog,
|
||||
RECENT_CHANGELOG_ENTRY_LIMIT,
|
||||
@@ -44,6 +46,25 @@ function release(major: number, minor: number, patch: number, body: string): Cha
|
||||
return { major, minor, patch, content };
|
||||
}
|
||||
|
||||
describe("startup changelog mode settings", () => {
|
||||
test("defaults to a summary", () => {
|
||||
expect(Settings.isolated().get("startup.changelogMode")).toBe("summary");
|
||||
});
|
||||
|
||||
test("preserves legacy collapsed and expanded choices", () => {
|
||||
expect(Settings.isolated({ collapseChangelog: true }).get("startup.changelogMode")).toBe("summary");
|
||||
expect(Settings.isolated({ collapseChangelog: false }).get("startup.changelogMode")).toBe("expanded");
|
||||
});
|
||||
|
||||
test("an explicit new mode wins over the legacy boolean", () => {
|
||||
const settings = Settings.isolated({
|
||||
collapseChangelog: false,
|
||||
"startup.changelogMode": "hidden",
|
||||
});
|
||||
expect(settings.get("startup.changelogMode")).toBe("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
async function withTempAgentDir<T>(callback: (agentDir: string) => Promise<T>): Promise<T> {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-changelog-marker-"));
|
||||
try {
|
||||
@@ -106,6 +127,10 @@ describe("selectStartupChangelog", () => {
|
||||
expect(selection.persistCurrentVersion).toBe(true);
|
||||
expect(selection.truncated).toBe(false);
|
||||
expect(selection.selectedEntries).toBe(RECENT_CHANGELOG_ENTRY_LIMIT);
|
||||
expect(selection.totalUnseenEntries).toBe(5);
|
||||
expect(selection.latestVersion).toBe("1.0.5");
|
||||
expect(selection.changeCount).toBe(3);
|
||||
expect(selection.categoryCounts).toEqual({ Added: 3 });
|
||||
expect(selection.markdown?.match(/## \[(\d+\.\d+\.\d+)\]/)?.[1]).toBe("1.0.5");
|
||||
expect(selection.markdown).toContain("## [1.0.5]");
|
||||
expect(selection.markdown).toContain("## [1.0.4]");
|
||||
@@ -153,6 +178,41 @@ describe("selectStartupChangelog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatStartupChangelogSummary", () => {
|
||||
test("summarizes selected releases and points to omitted history", () => {
|
||||
const selection = selectStartupChangelog(
|
||||
[
|
||||
release(2, 0, 0, "### Added\n\n- First addition.\n- Second addition.\n\n### Fixed\n\n- A fix."),
|
||||
release(1, 9, 0, "### Changed\n\n- A behavior change."),
|
||||
release(1, 8, 0, "### Security\n\n- A security improvement."),
|
||||
release(1, 7, 0, "### Fixed\n\n- An earlier fix."),
|
||||
release(1, 6, 0, "### Added\n\n- Already seen."),
|
||||
],
|
||||
"1.6.0",
|
||||
"2.0.0",
|
||||
);
|
||||
|
||||
expect(formatStartupChangelogSummary(selection)).toBe(
|
||||
[
|
||||
"Updated to v2.0.0 · 5 changes across 3 releases",
|
||||
"2 added · 1 changed · 1 fixed · 1 security · +1 earlier release · Use /changelog full for history.",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the recent-details hint when every unseen release is represented", () => {
|
||||
const selection = selectStartupChangelog(
|
||||
[release(2, 0, 0, "### Breaking Changes\n\n- Removed the old wire format.")],
|
||||
"1.0.0",
|
||||
"2.0.0",
|
||||
);
|
||||
|
||||
expect(formatStartupChangelogSummary(selection)).toBe(
|
||||
["Updated to v2.0.0 · 1 change in 1 release", "1 breaking change · Use /changelog for details."].join("\n"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChangelog", () => {
|
||||
test("reads current source release data and filters versions newer than the previous release", async () => {
|
||||
const entries = await parseChangelog(undefined);
|
||||
|
||||
Reference in New Issue
Block a user