mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
A stable body aggregates the changelog since the previous STABLE release, not since the last beta; in-beta-only bugfixes are excluded by the curator (the draft lists every candidate with its source section); the small-fixes filler line is legal only when the range carries user-visible work not itemised in the body — a single-issue hotfix ships without it, and body bullets must link their issues so the rule stays checkable. scripts/release-notes.mjs prints the aggregation draft and verifies docs/RELEASE-NOTES.md (npm run release:notes -- <tag> [--verify]); the verifier rejects the v1.68.1-style empty filler by execution. STATUS.md release mechanics updated in the same commit. Issue: #328 User-Visible: no
117 lines
5.2 KiB
JavaScript
117 lines
5.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import {
|
|
aggregateItems, compareVersions, isStable, issuesOf, parseChangelog,
|
|
previousStableTag, sectionsInRange, verifyReleaseNotes, visibleIssuesInRange,
|
|
} from '../scripts/release-notes.mjs';
|
|
|
|
// #328: правила тела стабильного релиза, решение владельца 2026-08-27.
|
|
|
|
test('versions compare with prereleases below their release', () => {
|
|
assert.ok(compareVersions('v1.68.0-beta.4', 'v1.68.0') < 0);
|
|
assert.ok(compareVersions('v1.68.1', 'v1.68.0') > 0);
|
|
assert.ok(compareVersions('v1.68.0-beta.2', 'v1.68.0-beta.10') < 0);
|
|
assert.equal(isStable('v1.68.0'), true);
|
|
assert.equal(isStable('v1.68.0-rc.1'), false);
|
|
assert.equal(previousStableTag('v1.69.0',
|
|
['v1.68.0', 'v1.68.1', 'v1.69.0-beta.1', 'v1.67.0']), 'v1.68.1');
|
|
assert.equal(previousStableTag('v1.0.0', ['v1.0.0-beta.1']), null);
|
|
});
|
|
|
|
const CHANGELOG = `# Changelog
|
|
|
|
## Unreleased
|
|
|
|
- New drawing tool ([#400](https://github.com/x/y/issues/400)).
|
|
|
|
## v1.69.0-beta.2 — 2026-09-01
|
|
|
|
- Reworded drawing tool ([#400](https://github.com/x/y/issues/400)).
|
|
- Fix a beta-only crash ([#401](https://github.com/x/y/issues/401)).
|
|
|
|
## v1.69.0-beta.1 — 2026-08-30
|
|
|
|
- New drawing tool, first wording ([#400](https://github.com/x/y/issues/400)).
|
|
|
|
## v1.68.1 — 2026-08-27
|
|
|
|
- Stable hotfix ([#390](https://github.com/x/y/issues/390)).
|
|
`;
|
|
|
|
test('the stable range takes every section after the previous stable and dedupes by issue', () => {
|
|
const sections = sectionsInRange(parseChangelog(CHANGELOG), 'v1.68.1', 'v1.69.0');
|
|
assert.deepEqual(sections.map((section) => section.title.split(' ')[0]),
|
|
['Unreleased', 'v1.69.0-beta.2', 'v1.69.0-beta.1']);
|
|
const items = aggregateItems(sections);
|
|
// #400 появляется трижды — выигрывает самая новая формулировка (Unreleased),
|
|
// #401 входит один раз; хотфикс #390 предыдущего стабильного не входит.
|
|
assert.deepEqual(items.map(({ item }) => issuesOf(item)[0]), [400, 401]);
|
|
assert.match(items[0].item, /New drawing tool/);
|
|
});
|
|
|
|
const gitStub = (visibleIssues) => (args) => {
|
|
assert.equal(args[0], 'log');
|
|
return visibleIssues.map((issue) =>
|
|
`feat: something\n\nIssue: #${issue}\nUser-Visible: yes\n\x1e`).join('')
|
|
+ 'chore: infra\n\nIssue: #999\nUser-Visible: no\n\x1e';
|
|
};
|
|
|
|
test('visible issues come only from User-Visible commits', () => {
|
|
const issues = visibleIssuesInRange('a..b', gitStub([400, 401]));
|
|
assert.deepEqual([...issues].sort(), [400, 401]);
|
|
});
|
|
|
|
const notesFor = (body) => `<!-- release: v1.69.0 -->\n\n## Основное\n\n${body}\n`;
|
|
|
|
test('an empty filler line is an error; a justified one passes (#328 rule 3)', () => {
|
|
const base = {
|
|
tag: 'v1.69.0',
|
|
changelogRu: CHANGELOG, changelogEn: CHANGELOG,
|
|
tags: ['v1.68.0', 'v1.68.1'],
|
|
};
|
|
// Хотфикс одной задачи: всё упомянуто, приписка запрещена.
|
|
const empty = verifyReleaseNotes({
|
|
...base,
|
|
notes: notesFor('- Одна задача ([#400](https://github.com/x/y/issues/400)).\n- Мелкие исправления и улучшения.\n- Small fixes and improvements.'),
|
|
gitRunner: gitStub([400]),
|
|
});
|
|
assert.equal(empty.errors.length, 1, JSON.stringify(empty.errors));
|
|
assert.match(empty.errors[0], /приписка.*пустая/);
|
|
|
|
// Есть непопавшая в тело user-visible работа — приписка законна.
|
|
const justified = verifyReleaseNotes({
|
|
...base,
|
|
notes: notesFor('- Одна задача ([#400](https://github.com/x/y/issues/400)).\n- Мелкие исправления и улучшения.\n\n## Highlights\n\n- One item ([#400](https://github.com/x/y/issues/400)).\n- Small fixes and improvements.'),
|
|
gitRunner: gitStub([400, 401]),
|
|
});
|
|
assert.deepEqual(justified.errors, []);
|
|
|
|
// Пункты без ссылок при наличии приписки — непроверяемо, ошибка.
|
|
const unlinkable = verifyReleaseNotes({
|
|
...base,
|
|
notes: notesFor('- Просто текст без ссылок.\n- Мелкие исправления и улучшения.\n- Small fixes and improvements.'),
|
|
gitRunner: gitStub([400]),
|
|
});
|
|
assert.equal(unlinkable.errors.length, 1, JSON.stringify(unlinkable.errors));
|
|
assert.match(unlinkable.errors[0], /не ссылаются на issues/);
|
|
|
|
// Issue вне диапазона и ченджлога — тело шире релиза.
|
|
const foreign = verifyReleaseNotes({
|
|
...base,
|
|
notes: notesFor('- Чужая задача ([#777](https://github.com/x/y/issues/777)).'),
|
|
gitRunner: gitStub([400]),
|
|
});
|
|
assert.equal(foreign.errors.length, 1, JSON.stringify(foreign.errors));
|
|
assert.match(foreign.errors[0], /#777/);
|
|
|
|
// Скрытая работа без приписки — предупреждение, не ошибка.
|
|
const missing = verifyReleaseNotes({
|
|
...base,
|
|
notes: notesFor('- Одна задача ([#400](https://github.com/x/y/issues/400)).'),
|
|
gitRunner: gitStub([400, 401]),
|
|
});
|
|
assert.deepEqual(missing.errors, []);
|
|
assert.equal(missing.warnings.length, 1);
|
|
});
|