Files
Trilium/scripts/update-version.ts
T
Elian DoranandClaude Opus 5 da3561485d feat(release): derive the Flathub metainfo releases from the release tags
The metainfo's `<releases>` decides the version Flathub displays, and the
manifest installs that file from the tagged checkout, so the entry has to
be right in the commit that gets tagged. Nothing wrote it: the block was
hand-maintained and had stood at 0.105.0. Now that release-flathub.yml
opens a packaging pull request for every stable release, that would have
published each one while the store still advertised the same version.

The list is derived rather than accumulated. `git for-each-ref` already
holds both fields — the tags date v0.105.0 to 2026-08-19, exactly the
date the block carried by hand — so the newest five `vX.Y.Z` tags are
rendered fresh on every run, `rc`, `beta` and the two-component `v0.48`
tags filtered out. A version being prepared has no tag yet and is dated
today until its own tag supplies the date, which means a re-run after
tagging settles on the tag date instead of drifting to the day of the
re-run. Backfill comes free: the first run fills four historical entries.

The release notes were the other candidate and cannot serve. They carry
no date — the files open with a bare `# vX.Y.Z` — and the directory also
holds images, a template and legacy names.

A checkout with no tags throws rather than truncating the block to a
single entry. Safe to fail hard: no workflow runs `chore:update-version`,
only `update-nightly-version`, so this is always a human in a full clone.

The script gains the run-as-script guard its siblings use, so the spec
can import the helpers without main() rewriting the tree.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:32:04 +03:00

113 lines
4.3 KiB
TypeScript

/**
* @module
*
* This script synchronizes the `package.json` version of the monorepo (root `package.json`)
* into the apps, so that it is properly displayed. It also regenerates the Flathub
* metainfo's `<releases>`, which decides the version Flathub displays: the manifest
* installs that file from the tagged checkout, so the entry has to be in the commit that
* gets tagged.
*/
import { execFileSync } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { fileURLToPath } from "url";
const SCRIPT_PATH = fileURLToPath(import.meta.url);
const ROOT = join(SCRIPT_PATH, "..", "..");
const METAINFO_PATH = join(ROOT, "apps", "desktop", "flatpak", "org.triliumnotes.Trilium.metainfo.xml");
const RELEASE_URL = "https://github.com/TriliumNext/Trilium/releases/tag/v";
const MAX_RELEASE_ENTRIES = 5;
export interface Release {
version: string;
date: string;
}
export function main() {
const version = getVersion(join(ROOT, "package.json"));
for (const appName of ["server", "client", "standalone", "desktop", "edit-docs"]) {
patchPackageJson(join(ROOT, "apps", appName, "package.json"), version);
}
for (const packageName of ["commons", "pdfjs-viewer", "trilium-core"]) {
patchPackageJson(join(ROOT, "packages", packageName, "package.json"), version);
}
const tags = parseReleaseTags(git("for-each-ref", "--sort=-creatordate",
"--format=%(refname:short)\t%(creatordate:short)", "refs/tags/v*"));
if (tags.length === 0) {
throw new Error("Found no release tags to date the metainfo entries with.");
}
const releases = selectReleases(tags, version, new Date().toISOString().slice(0, 10));
writeFileSync(METAINFO_PATH,
renderMetainfoReleases(readFileSync(METAINFO_PATH, "utf-8"), releases));
}
/**
* Reads `git for-each-ref` output, keeping the `vX.Y.Z` tags in the order given. The rc
* and beta tags are releases Flathub never sees, and `v0.48`-style tags predate the
* three-part scheme.
*/
export function parseReleaseTags(output: string): Release[] {
const releases: Release[] = [];
for (const line of output.split("\n")) {
const tag = /^v(\d+\.\d+\.\d+)\t(\d{4}-\d{2}-\d{2})$/.exec(line.trim());
if (tag) {
releases.push({ version: tag[1], date: tag[2] });
}
}
return releases;
}
/**
* The newest MAX_RELEASE_ENTRIES releases, newest first. A version being prepared has no
* tag yet, so it is dated `today` until the tag it is about to get supplies the date.
*/
export function selectReleases(tags: Release[], version: string, today: string): Release[] {
const releases = tags.some((it) => it.version === version)
? tags : [ { version, date: today }, ...tags ];
return releases.slice(0, MAX_RELEASE_ENTRIES);
}
/** Replaces the metainfo's `<releases>`, keeping the indentation the document uses. */
export function renderMetainfoReleases(metainfo: string, releases: Release[]): string {
const block = /([ \t]*)<releases>\n[\s\S]*?[ \t]*<\/releases>/;
if (!block.test(metainfo)) {
throw new Error("Found no <releases> block in the metainfo.");
}
return metainfo.replace(block, (_, indent: string) => {
const entries = releases.map(({ version, date }) =>
`${indent} <release version="${version}" date="${date}">\n`
+ `${indent} <url type="details">${RELEASE_URL}${version}</url>\n`
+ `${indent} </release>\n`);
return `${indent}<releases>\n${entries.join("")}${indent}</releases>`;
});
}
function patchPackageJson(packageJsonPath: string, version: string) {
// Read the version from package.json and process it.
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
// Write the adjusted version back in.
packageJson.version = version;
const formattedJson = JSON.stringify(packageJson, null, 2);
writeFileSync(packageJsonPath, formattedJson);
}
function getVersion(packageJsonPath: string) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.version;
}
function git(...args: string[]): string {
return execFileSync("git", args, { cwd: ROOT }).toString();
}
// Only when run as a script — the pure helpers above are imported by the spec.
if (process.argv[1] === SCRIPT_PATH) {
main();
}