Files
iptvnator/tools/release/nightly-version.test.mjs
T
4grayandClaude Fable 5.1 6f7973a9fb feat(updater): nightly builds and a stable/nightly update channel (#1608)
* feat(updater): nightly builds and a stable/nightly update channel

Every master push publishes its artifacts as a prerelease of
4gray/iptvnator-nightly instead of the rolling test-master draft, with a
version of <next patch>-nightly.<commit date>.<run number> applied in
every build job. Settings → About gains an Update channel switch;
AppUpdateService re-points electron-updater per check (feed repository,
allowPrerelease, channel name, allowDowngrade reset) and reads release
notes from the repository the requested version belongs to. Channel
switches are forward-only: a nightly build stays until a newer stable
release exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(updater): compute the nightly version once and keep re-runs safe

Review follow-ups: the nightly version is resolved by a leading job and
handed to every build job, and the patch is bumped only when the base
tag already exists so the release-cut window stays below the imminent
release. A re-run never deletes a published nightly; only a draft left
by a failed run is replaced. Typed update-status literals in the
remaining specs carry the new channel fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(packaging): expect the nightly-version prerequisite in the build workflow graph

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 17:49:22 +02:00

220 lines
7.3 KiB
JavaScript

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
applyNightlyVersion,
buildNightlyVersion,
isNightlyVersion,
parseArguments,
parseBooleanFlag,
} from './nightly-version.mjs';
describe('buildNightlyVersion', () => {
it('bumps the patch and appends the nightly identifiers', () => {
assert.equal(
buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260915',
runNumber: '1234',
}),
'0.23.1-nightly.20260915.1234'
);
});
it('orders newer nightlies above older ones and below the next stable', () => {
const semverOrder = (left, right) => {
// Mirror semver precedence closely enough for these shapes:
// numeric identifiers compare numerically, a release outranks
// its prereleases.
const parse = (value) => {
const [core, prerelease = ''] = value.split('-');
return {
core: core.split('.').map(Number),
prerelease: prerelease ? prerelease.split('.') : [],
};
};
const a = parse(left);
const b = parse(right);
for (let index = 0; index < 3; index += 1) {
if (a.core[index] !== b.core[index]) {
return a.core[index] - b.core[index];
}
}
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
return b.prerelease.length - a.prerelease.length;
}
for (let index = 0; index < a.prerelease.length; index += 1) {
const x = a.prerelease[index];
const y = b.prerelease[index];
if (x === y) continue;
return /^\d+$/.test(x) && /^\d+$/.test(y)
? Number(x) - Number(y)
: x < y
? -1
: 1;
}
return 0;
};
const older = buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260915',
runNumber: 1234,
});
const sameDayLater = buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260915',
runNumber: 1240,
});
const nextDay = buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260916',
runNumber: 1241,
});
assert.ok(semverOrder(older, '0.23.0') > 0);
assert.ok(semverOrder(sameDayLater, older) > 0);
assert.ok(semverOrder(nextDay, sameDayLater) > 0);
assert.ok(semverOrder(nextDay, '0.23.1') < 0);
assert.ok(semverOrder(nextDay, '0.24.0') < 0);
});
it('keeps the base patch while the base version is not tagged yet', () => {
// The release-cut commit bumps package.json to 0.23.1 before the
// v0.23.1 tag exists: the nightly must sit BELOW 0.23.1 so the
// imminent stable release is still offered.
assert.equal(
buildNightlyVersion({
baseVersion: '0.23.1',
date: '20260915',
runNumber: 1240,
baseReleased: false,
}),
'0.23.1-nightly.20260915.1240'
);
assert.equal(
buildNightlyVersion({
baseVersion: '0.23.1',
date: '20260915',
runNumber: 1241,
baseReleased: true,
}),
'0.23.2-nightly.20260915.1241'
);
});
it('rejects a base version that is already a prerelease', () => {
assert.throws(
() =>
buildNightlyVersion({
baseVersion: '0.23.1-nightly.20260915.1',
date: '20260915',
runNumber: 2,
}),
/released X\.Y\.Z version/
);
});
it('rejects malformed dates and run numbers', () => {
assert.throws(
() =>
buildNightlyVersion({
baseVersion: '0.23.0',
date: '2026-09-15',
runNumber: 1,
}),
/YYYYMMDD/
);
assert.throws(
() =>
buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260915',
runNumber: undefined,
}),
/positive integer/
);
assert.throws(
() =>
buildNightlyVersion({
baseVersion: '0.23.0',
date: '20260915',
runNumber: '0',
}),
/positive integer/
);
});
});
describe('applyNightlyVersion', () => {
it('replaces only the version line and keeps the file formatting', () => {
const source =
'{\n "name": "iptvnator",\n "version": "0.23.0",\n "engines": {\n "node": "22"\n }\n}\n';
assert.equal(
applyNightlyVersion(source, '0.23.1-nightly.20260915.7'),
'{\n "name": "iptvnator",\n "version": "0.23.1-nightly.20260915.7",\n "engines": {\n "node": "22"\n }\n}\n'
);
});
it('fails when the version line is missing', () => {
assert.throws(
() => applyNightlyVersion('{ "name": "x" }', '1.0.1-nightly.1.1'),
/no "version" line/
);
});
});
describe('explicit version input', () => {
it('accepts only the shape the script itself produces', () => {
assert.equal(isNightlyVersion('0.23.1-nightly.20260915.1234'), true);
assert.equal(isNightlyVersion('0.23.1'), false);
assert.equal(isNightlyVersion('0.23.1-beta.1'), false);
assert.equal(isNightlyVersion('0.23.1-nightly.2026915.1'), false);
assert.equal(isNightlyVersion(undefined), false);
});
it('parses the base-released flag strictly', () => {
assert.equal(parseBooleanFlag('true', '--base-released'), true);
assert.equal(parseBooleanFlag('false', '--base-released'), false);
assert.throws(
() => parseBooleanFlag('yes', '--base-released'),
/--base-released must be "true" or "false"/
);
});
});
describe('parseArguments', () => {
it('reads the flags and ignores a bare separator', () => {
assert.deepEqual(
parseArguments([
'--',
'--apply',
'--base',
'0.23.0',
'--date',
'20260915',
'--run-number',
'12',
'--base-released',
'false',
'--version',
'0.23.0-nightly.20260915.12',
]),
{
apply: true,
base: '0.23.0',
date: '20260915',
runNumber: '12',
baseReleased: 'false',
version: '0.23.0-nightly.20260915.12',
}
);
});
it('reports usage for unknown flags and missing values', () => {
assert.equal(parseArguments(['--unknown']), null);
assert.equal(parseArguments(['--base']), null);
assert.equal(parseArguments(['--base', '--apply']), null);
});
});