feat(cli): show changelog entries before running upgrades

This commit is contained in:
2026-04-16 13:09:21 +00:00
parent 6b2fa65611
commit ba4e56338c
8 changed files with 493 additions and 897 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## 2026-04-16 - 5.11.0 - feat(cli)
show changelog entries before running upgrades
- fetch and render changelog entries between the installed and latest versions during the upgrade flow
- add upgrade changelog parsing helper with tests for version filtering and grouped version ranges
- document that the upgrade command displays release notes before installing
## 2026-04-16 - 5.10.0 - feat(cli,snmp)
fix APC runtime unit defaults and add interactive action editing
+3
View File
@@ -65,5 +65,8 @@
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"devDependencies": {
"@git.zone/tsdeno": "^1.3.1"
},
"dependencies": {
"@push.rocks/smartchangelog": "^0.1.0"
}
}
+390 -894
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -787,6 +787,10 @@ Any UPS supported by [NUT (Network UPS Tools)](https://networkupstools.org/) —
sudo nupst upgrade
```
The built-in upgrade command checks the latest published release, fetches `changelog.md` from
`main`, and shows the release notes for the versions between your installed version and the target
version before running the installer.
### Re-run Installer
```bash
+34
View File
@@ -42,6 +42,7 @@ import { createInitialUpsStatus } from '../ts/ups-status.ts';
import { MigrationV4_2ToV4_3 } from '../ts/migrations/migration-v4.2-to-v4.3.ts';
import { MigrationV4_3ToV4_4 } from '../ts/migrations/migration-v4.3-to-v4.4.ts';
import { ActionHandler } from '../ts/cli/action-handler.ts';
import { renderUpgradeChangelog } from '../ts/upgrade-changelog.ts';
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
const testQenv = new qenv.Qenv('./', '.nogit/');
@@ -817,6 +818,39 @@ Deno.test('convertRuntimeValueToMinutes: APC and explicit overrides convert corr
assertEquals(convertRuntimeValueToMinutes({ upsModel: 'apc', runtimeUnit: 'minutes' }, 12), 12);
});
Deno.test('renderUpgradeChangelog: renders only versions between current and latest', () => {
const changelogMarkdown = `# Changelog
## 2026-04-16 - 5.10.0 - feat(cli,snmp)
fix APC runtime unit defaults and add interactive action editing
- correct APC runtime handling
## 2026-04-16 - 5.8.0 - feat(systemd)
improve service status reporting with structured systemctl data
- switch status collection to systemctl show
`;
const rendered = renderUpgradeChangelog(changelogMarkdown, '5.8.0', '5.10.0');
assert(rendered.includes('5.10.0 - feat(cli,snmp)'));
assert(rendered.includes('fix APC runtime unit defaults and add interactive action editing'));
assert(!rendered.includes('5.8.0 - feat(systemd)'));
});
Deno.test('renderUpgradeChangelog: includes grouped version ranges when they intersect', () => {
const changelogMarkdown = `# Changelog
## 2020-06-01 - 4.0.3-4.0.5 - core
Grouped maintenance releases with repeated core update work.
- 4.0.5 introduced a breaking change by switching core packaging behavior toward ESM compatibility
`;
const rendered = renderUpgradeChangelog(changelogMarkdown, '4.0.4', '4.0.5');
assert(rendered.includes('4.0.3-4.0.5 - core'));
});
// -----------------------------------------------------------------------------
// Migration Tests
// -----------------------------------------------------------------------------
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '5.10.0',
version: '5.11.0',
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
}
+38 -2
View File
@@ -1,13 +1,14 @@
import process from 'node:process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { execFileSync, execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import { theme } from '../colors.ts';
import { PAUSE } from '../constants.ts';
import type { IPauseState } from '../pause-state.ts';
import * as helpers from '../helpers/index.ts';
import { renderUpgradeChangelog } from '../upgrade-changelog.ts';
/**
* Class for handling service-related CLI commands
@@ -270,7 +271,7 @@ export class ServiceHandler {
// Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
const response = execSync(`curl -sSL ${apiUrl}`).toString();
const response = this.fetchRemoteText(apiUrl);
const release = JSON.parse(response);
const latestVersion = release.tag_name; // e.g., "v4.0.7"
@@ -294,6 +295,7 @@ export class ServiceHandler {
}
logger.info(`New version available: ${latestVersion}`);
this.showUpgradeChangelog(normalizedCurrent, normalizedLatest);
logger.dim('Downloading and installing...');
console.log('');
@@ -321,6 +323,40 @@ export class ServiceHandler {
}
}
private fetchRemoteText(url: string): string {
return execFileSync('curl', ['-fsSL', url], {
encoding: 'utf8',
});
}
private showUpgradeChangelog(currentVersion: string, latestVersion: string): void {
const changelogUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/changelog.md';
try {
const changelogMarkdown = this.fetchRemoteText(changelogUrl);
const renderedChanges = renderUpgradeChangelog(
changelogMarkdown,
currentVersion,
latestVersion,
);
if (!renderedChanges) {
return;
}
logger.info(`What's changed:`);
logger.log('');
for (const line of renderedChanges.split('\n')) {
logger.log(line);
}
logger.log('');
} catch (error) {
logger.warn('Could not load changelog for this upgrade. Continuing anyway.');
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
logger.log('');
}
}
/**
* Completely uninstall NUPST from the system
*/
+16
View File
@@ -0,0 +1,16 @@
import { SmartChangelog } from 'npm:@push.rocks/smartchangelog@^0.1.0';
export const renderUpgradeChangelog = (
changelogMarkdown: string,
currentVersion: string,
latestVersion: string,
): string => {
const changelog = SmartChangelog.fromMarkdown(changelogMarkdown);
const entries = changelog.getEntriesBetween(currentVersion, latestVersion);
if (entries.length === 0) {
return '';
}
return entries.map((entry) => entry.toCliString()).join('\n\n');
};