mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 18:35:17 +00:00
Every release entry in CHANGELOG.md now leads with highlights: an unheaded list above the first `###`. The app's What's new sheet, the website changelog and the GitHub release body show the highlights and fold the sections, the full record, behind one control. An entry without highlights keeps its sections in view. docs/release.md carries the writing rules. Installed apps fetch CHANGELOG.md from main and parse it with the parser they shipped with, so the two heading rules are a compatibility surface: `##` starts a release, `###` starts a section, and headings inside fenced code are text. The app, website and release-script parsers now split an entry the same way and each runs the shared cases in scripts/test-support/changelog-structure.json. The release-script tests, which no CI job ran before, are part of "Validate CI contracts". Adds the 0.16.0-beta.1 entry (en and zh-CN) ahead of the release.
350 lines
12 KiB
JavaScript
350 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import {
|
|
getFdroidVersionCodes,
|
|
getNativeReleaseVersion,
|
|
} from "../packages/app/native-release-version.js";
|
|
import { formatFdroidChangelog, syncFdroidChangelogs } from "./sync-fdroid-changelogs.mjs";
|
|
|
|
const CHANGELOG_CHARACTER_LIMIT = 500;
|
|
|
|
function withTempRepo(fn, { changelog, version = "0.2.3" }) {
|
|
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-fdroid-changelogs-"));
|
|
try {
|
|
writeFileSync(path.join(tempDir, "package.json"), JSON.stringify({ version }));
|
|
writeFileSync(path.join(tempDir, "CHANGELOG.md"), changelog);
|
|
fn(tempDir);
|
|
} finally {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
function readChangelogFile(dir, versionCode, locale = "en-US") {
|
|
return readFileSync(
|
|
path.join(dir, "fastlane", "metadata", "android", locale, "changelogs", `${versionCode}.txt`),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
// The whole point of the shared module: these are the numbers the APKs actually
|
|
// ship with, so the filenames have to track them exactly.
|
|
test("derives one version code per published ABI", () => {
|
|
assert.equal(getNativeReleaseVersion("0.2.2").androidVersionCode, 2002);
|
|
assert.deepEqual(getFdroidVersionCodes("0.2.2"), [
|
|
{ abi: "armeabi-v7a", versionCode: 20021 },
|
|
{ abi: "arm64-v8a", versionCode: 20022 },
|
|
{ abi: "x86", versionCode: 20023 },
|
|
{ abi: "x86_64", versionCode: 20024 },
|
|
]);
|
|
});
|
|
|
|
test("does not write or validate F-Droid changelogs for beta releases", () => {
|
|
withTempRepo(
|
|
(dir) => {
|
|
const result = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.deepEqual(result, {
|
|
contents: null,
|
|
version: "0.2.3-beta.1",
|
|
versionCodes: [],
|
|
written: [],
|
|
});
|
|
assert.equal(existsSync(path.join(dir, "fastlane")), false);
|
|
},
|
|
{ changelog: "", version: "0.2.3-beta.1" },
|
|
);
|
|
});
|
|
|
|
test("rejects versions that would collide or overflow", () => {
|
|
assert.throws(() => getNativeReleaseVersion("0.1000.0"), /collision-free/);
|
|
assert.throws(() => getNativeReleaseVersion("not-a-version"), /unsupported version/);
|
|
assert.throws(() => getNativeReleaseVersion("210.0.0"), /out of range/);
|
|
});
|
|
|
|
test("writes an identical changelog for every ABI version code", () => {
|
|
const changelog = "# Changelog\n\n## 0.2.3 - 2026-07-27\n\n### Fixed\n\n- Something broke.\n";
|
|
withTempRepo(
|
|
(dir) => {
|
|
const result = syncFdroidChangelogs([], { cwd: dir });
|
|
|
|
assert.equal(result.written.length, 4);
|
|
for (const { versionCode } of getFdroidVersionCodes("0.2.3")) {
|
|
assert.equal(readChangelogFile(dir, versionCode), result.contents);
|
|
}
|
|
assert.match(result.contents, /- Something broke\./);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|
|
|
|
test("writes the four stable 0.5.0 changelog files", () => {
|
|
const changelog = "## 0.5.0 - 2026-08-21\n\n### Added\n\n- Stable F-Droid metadata.\n";
|
|
withTempRepo(
|
|
(dir) => {
|
|
const result = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.deepEqual(
|
|
result.written.map((entry) => entry.replace(/ \(.+\)$/, "")),
|
|
[
|
|
"fastlane/metadata/android/en-US/changelogs/50001.txt",
|
|
"fastlane/metadata/android/en-US/changelogs/50002.txt",
|
|
"fastlane/metadata/android/en-US/changelogs/50003.txt",
|
|
"fastlane/metadata/android/en-US/changelogs/50004.txt",
|
|
],
|
|
);
|
|
},
|
|
{ changelog, version: "0.5.0" },
|
|
);
|
|
});
|
|
|
|
test("strips PR references and contributor attributions", () => {
|
|
const changelog = [
|
|
"## 0.2.3 - 2026-07-27",
|
|
"",
|
|
"### Added",
|
|
"",
|
|
"- Support for `GitLab` merge requests ([#1913](https://x/1913) by [@nllptrx](https://y))",
|
|
"",
|
|
].join("\n");
|
|
|
|
withTempRepo(
|
|
(dir) => {
|
|
const { contents } = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.equal(contents.includes("#1913"), false);
|
|
assert.equal(contents.includes("nllptrx"), false);
|
|
assert.equal(contents.includes("`"), false);
|
|
assert.match(contents, /- Support for GitLab merge requests\n/);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|
|
|
|
test("stays inside the 500 character limit and keeps the full-notes link", () => {
|
|
const bullets = Array.from(
|
|
{ length: 40 },
|
|
(_, index) => `- Shipped improvement number ${index}.`,
|
|
);
|
|
const changelog = ["## 0.2.3 - 2026-07-27", "", "### Added", "", ...bullets, ""].join("\n");
|
|
|
|
withTempRepo(
|
|
(dir) => {
|
|
const { contents } = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.ok(
|
|
contents.length <= CHANGELOG_CHARACTER_LIMIT,
|
|
`expected <= ${CHANGELOG_CHARACTER_LIMIT}, got ${contents.length}`,
|
|
);
|
|
assert.match(contents, /Full notes: https:\/\/paseo\.sh\/changelog\n$/);
|
|
// Truncation happens at a bullet boundary, never mid-sentence.
|
|
assert.equal(contents.includes("Shipped improvement number 0."), true);
|
|
assert.equal(/- Shipped improvement number \d+\.\.\.$/m.test(contents), false);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|
|
|
|
test("never leaves an orphan section heading when bullets get cut", () => {
|
|
const filler = Array.from({ length: 20 }, (_, index) => `- Improvement ${index} with some text.`);
|
|
const changelog = [
|
|
"## 0.2.3 - 2026-07-27",
|
|
"",
|
|
"### Improved",
|
|
"",
|
|
...filler,
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- A late fix that will not fit.",
|
|
"",
|
|
].join("\n");
|
|
|
|
withTempRepo(
|
|
(dir) => {
|
|
const { contents } = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.equal(contents.includes("Fixed"), false);
|
|
assert.equal(contents.trimEnd().endsWith("Improved"), false);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|
|
|
|
test("salvages a truncated first bullet rather than emitting only a link", () => {
|
|
const contents = formatFdroidChangelog(["### Added", `- ${"x".repeat(600)}`]);
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
assert.match(contents, /^- x+\.\.\.\n\nFull notes:/);
|
|
});
|
|
|
|
// Releases use blockquotes for action-required notices. Dropping one silently is
|
|
// worse than dropping any bullet, so these pin the behaviour.
|
|
test("preserves a blockquoted notice ahead of the bullets", () => {
|
|
const contents = formatFdroidChangelog([
|
|
"> **Important update notice**",
|
|
">",
|
|
"> If you installed Paseo Desktop 0.1.108, you need to [reinstall manually](https://paseo.sh/download).",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- Desktop no longer gets stuck connecting.",
|
|
]);
|
|
|
|
assert.match(
|
|
contents,
|
|
/^Important update notice\nIf you installed Paseo Desktop 0\.1\.108, you need to reinstall manually\.\n\nFixed\n- Desktop no longer gets stuck connecting\./,
|
|
);
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
});
|
|
|
|
test("keeps the notice whole and cuts bullets when the budget runs out", () => {
|
|
const notice = `- ${"y".repeat(10)}`;
|
|
const bullets = Array.from({ length: 30 }, () => `- ${"b".repeat(60)}`);
|
|
const contents = formatFdroidChangelog([
|
|
"> Reinstall Paseo manually before updating, the old updater cannot install this build.",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
...bullets,
|
|
notice,
|
|
]);
|
|
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
assert.match(
|
|
contents,
|
|
/^Reinstall Paseo manually before updating, the old updater cannot install this build\.\n/,
|
|
);
|
|
// The notice survived intact, so some bullets must have been dropped.
|
|
assert.ok(contents.split("\n").filter((line) => line.startsWith("- b")).length < bullets.length);
|
|
});
|
|
|
|
test("truncates an over-long notice instead of dropping it", () => {
|
|
const contents = formatFdroidChangelog([`> ${"n".repeat(900)}`, "", "### Fixed", "", "- A fix."]);
|
|
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
assert.match(contents, /^n+\.\.\.\n\nFull notes:/);
|
|
assert.equal(contents.includes("A fix."), false);
|
|
});
|
|
|
|
test("captures multiple blockquotes and blockquotes that follow sections", () => {
|
|
const contents = formatFdroidChangelog([
|
|
"> First notice.",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- A fix.",
|
|
"",
|
|
"> Second notice.",
|
|
]);
|
|
|
|
assert.match(contents, /^First notice\.\n\nSecond notice\.\n\nFixed\n- A fix\./);
|
|
});
|
|
|
|
test("renders the real 0.1.109 notice, which is the entry's whole point", () => {
|
|
const changelog = [
|
|
"## 0.1.109 - 2026-07-16",
|
|
"",
|
|
"> **Important update notice**",
|
|
">",
|
|
"> If you installed Paseo Desktop 0.1.108, you need to [download and reinstall Paseo manually](https://paseo.sh/download) to get this fix. The bug in 0.1.108 prevents its automatic updater from installing 0.1.109. Users on 0.1.107 or earlier can update normally.",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- Paseo Desktop no longer gets stuck connecting or loses native window controls after updating ([#2111](https://github.com/getpaseo/paseo/pull/2111) by [@cleiter](https://github.com/cleiter))",
|
|
"",
|
|
].join("\n");
|
|
|
|
withTempRepo(
|
|
(dir) => {
|
|
const { contents } = syncFdroidChangelogs([], { cwd: dir });
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
assert.match(contents, /^Important update notice\n/);
|
|
assert.match(contents, /download and reinstall Paseo manually/);
|
|
assert.match(contents, /Users on 0\.1\.107 or earlier can update normally\./);
|
|
assert.match(contents, /\nFixed\n- Paseo Desktop no longer gets stuck connecting/);
|
|
assert.equal(contents.includes("#2111"), false);
|
|
},
|
|
{ changelog, version: "0.1.109" },
|
|
);
|
|
});
|
|
|
|
test("preserves the standalone 0.1.96 Android notice", () => {
|
|
const changelog = readFileSync(path.resolve("CHANGELOG.md"), "utf8");
|
|
const entry = changelog.match(/## 0\.1\.96 - 2026-06-13\n([\s\S]*?)(?=\n## |$)/)?.[1];
|
|
assert.ok(entry);
|
|
|
|
const contents = formatFdroidChangelog(entry.split("\n"));
|
|
assert.match(contents, /This release only fixes an Android issue/);
|
|
assert.equal(contents.includes("_This release"), false);
|
|
});
|
|
|
|
test("leads with an entry's highlights and spends the budget on them first", () => {
|
|
const highlight = (name) =>
|
|
`- **${name}:** ${"A highlight a user would name first. ".repeat(4).trim()} ([#1](https://github.com/getpaseo/paseo/pull/1))`;
|
|
const contents = formatFdroidChangelog(
|
|
[
|
|
highlight("First"),
|
|
highlight("Second"),
|
|
highlight("Third"),
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- Fixed a crash",
|
|
],
|
|
{ limit: CHANGELOG_CHARACTER_LIMIT },
|
|
);
|
|
|
|
assert.match(contents, /^- First: A highlight a user would name first\./);
|
|
assert.match(contents, /\n- Second: /);
|
|
assert.equal(contents.includes("Fixed a crash"), false);
|
|
assert.ok(contents.length <= CHANGELOG_CHARACTER_LIMIT);
|
|
});
|
|
|
|
test("--check fails when the generated files are missing or stale", () => {
|
|
const changelog = "## 0.2.3 - 2026-07-27\n\n### Fixed\n\n- Something broke.\n";
|
|
withTempRepo(
|
|
(dir) => {
|
|
assert.throws(() => syncFdroidChangelogs(["--check"], { cwd: dir }), /out of date/);
|
|
|
|
syncFdroidChangelogs([], { cwd: dir });
|
|
assert.doesNotThrow(() => syncFdroidChangelogs(["--check"], { cwd: dir }));
|
|
|
|
const [{ versionCode }] = getFdroidVersionCodes("0.2.3");
|
|
writeFileSync(
|
|
path.join(
|
|
dir,
|
|
"fastlane",
|
|
"metadata",
|
|
"android",
|
|
"en-US",
|
|
"changelogs",
|
|
`${versionCode}.txt`,
|
|
),
|
|
"drifted\n",
|
|
);
|
|
assert.throws(() => syncFdroidChangelogs(["--check"], { cwd: dir }), /out of date/);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|
|
|
|
test("fails loudly when the release has no changelog entry", () => {
|
|
const changelog = "## 0.2.2 - 2026-07-25\n\n### Fixed\n\n- Older release.\n";
|
|
withTempRepo(
|
|
(dir) => {
|
|
assert.throws(() => syncFdroidChangelogs([], { cwd: dir }), /No CHANGELOG\.md entry/);
|
|
},
|
|
{ changelog, version: "0.2.3" },
|
|
);
|
|
});
|
|
|
|
test("honours a non-default locale directory", () => {
|
|
const changelog = "## 0.2.3 - 2026-07-27\n\n### Fixed\n\n- Something broke.\n";
|
|
withTempRepo(
|
|
(dir) => {
|
|
mkdirSync(path.join(dir, "fastlane", "metadata", "android", "ja"), { recursive: true });
|
|
syncFdroidChangelogs(["--locale", "ja"], { cwd: dir });
|
|
const [{ versionCode }] = getFdroidVersionCodes("0.2.3");
|
|
assert.match(readChangelogFile(dir, versionCode, "ja"), /- Something broke\./);
|
|
},
|
|
{ changelog },
|
|
);
|
|
});
|