From abe0bf3c13c2d0f4eac7d8defa7c42ef8ef217d8 Mon Sep 17 00:00:00 2001 From: Ben Senescu <44480372+bensenescu@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:53:36 -0400 Subject: [PATCH] chore: prepare 0.0.3 release notes (#37) --- .opencode/command/release-notes.md | 1 + knip.jsonc | 1 + package.json | 2 +- release-notes/v0.0.3.md | 14 ++++ scripts/release-notes.mjs | 105 ++++++++++++++++++++++++++++- 5 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 release-notes/v0.0.3.md diff --git a/.opencode/command/release-notes.md b/.opencode/command/release-notes.md index 55382f8..11eb917 100644 --- a/.opencode/command/release-notes.md +++ b/.opencode/command/release-notes.md @@ -26,6 +26,7 @@ Rules: After reviewing the generated notes: - confirm the `package.json` version matches the intended release tag when one is provided +- sanity-check that the notes only cover changes since the previous release - tighten wording only when it improves clarity - preserve the existing section structure unless there is a strong reason to merge sections - suggest saving the finalized notes to `release-notes/v.md` when a version is known diff --git a/knip.jsonc b/knip.jsonc index 28f0d09..64dbca7 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -11,6 +11,7 @@ ], "project": ["**/*.{js,mjs,ts,tsx}", "!src/routeTree.gen.ts", "!web/**"], "ignore": ["drizzle-prod.config.ts"], + "ignoreBinaries": ["tsx"], // Disable Drizzle plugin - it tries to load drizzle.config.ts which imports cloudflare:workers "drizzle": false, "ignoreDependencies": [ diff --git a/package.json b/package.json index 8c1c3db..1299a55 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-seo", "private": true, "sideEffects": false, - "version": "0.0.2", + "version": "0.0.3", "type": "module", "scripts": { "dev": "AUTH_MODE=local_noauth vite dev", diff --git a/release-notes/v0.0.3.md b/release-notes/v0.0.3.md new file mode 100644 index 0000000..46bc0af --- /dev/null +++ b/release-notes/v0.0.3.md @@ -0,0 +1,14 @@ +## Added + +- Added the Backlinks page. + +## Fixed + +- Renamed from OpenRank back to OpenSEO. +- Fixed website styling issues. + +## Docs + +- Clarified how to update self-hosted Docker instances. + +Full Changelog: https://github.com/bensenescu/open-seo/compare/v0.0.2...HEAD diff --git a/scripts/release-notes.mjs b/scripts/release-notes.mjs index 54341fb..d31f589 100644 --- a/scripts/release-notes.mjs +++ b/scripts/release-notes.mjs @@ -3,10 +3,10 @@ // @ts-check import { execFileSync } from "node:child_process"; +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { parseArgs } from "node:util"; -import { writeFileSync } from "node:fs"; const argv = process.argv.slice(2); const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv; @@ -54,6 +54,107 @@ function getLatestSemverTag() { ); } +/** @typedef {{ major: number, minor: number, patch: number }} Semver */ +/** @typedef {{ tag: string, version: Semver, source: "tag" | "release-notes" }} VersionCandidate */ + +/** @param {string} value */ +function parseSemver(value) { + const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/); + if (!match) return null; + + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + }; +} + +/** @param {Semver} left @param {Semver} right */ +function compareSemver(left, right) { + if (left.major !== right.major) return left.major - right.major; + if (left.minor !== right.minor) return left.minor - right.minor; + return left.patch - right.patch; +} + +/** @param {unknown} value @returns {value is Record} */ +function isRecord(value) { + return Boolean(value) && typeof value === "object"; +} + +function getPackageVersion() { + /** @type {unknown} */ + const packageJson = JSON.parse(readFileSync("package.json", "utf8")); + if (!isRecord(packageJson)) return null; + const version = packageJson.version; + return typeof version === "string" ? version : null; +} + +function getReleaseNoteVersions() { + try { + return readdirSync("release-notes").flatMap((name) => { + const version = name.match( + /^v(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\.md$/, + )?.[1]; + return version ? [version] : []; + }); + } catch { + return []; + } +} + +/** @param {string[]} versionValues @param {VersionCandidate["source"]} source */ +function collectVersionCandidates(versionValues, source) { + return versionValues.flatMap((value) => { + const version = parseSemver(value); + if (!version) return []; + return [{ tag: source === "tag" ? value : `v${value}`, version, source }]; + }); +} + +function getDefaultFromTag() { + /** @type {string | null} */ + const currentVersion = getPackageVersion(); + const parsedCurrentVersion = currentVersion + ? parseSemver(currentVersion) + : null; + if (!parsedCurrentVersion) return getLatestSemverTag(); + + const tagCandidates = collectVersionCandidates( + git(["tag", "--sort=-version:refname"]) + .split("\n") + .map((tag) => tag.trim()) + .filter(Boolean), + "tag", + ); + + const releaseNoteCandidates = collectVersionCandidates( + getReleaseNoteVersions(), + "release-notes", + ); + + const candidates = [...tagCandidates, ...releaseNoteCandidates] + .filter((entry) => compareSemver(entry.version, parsedCurrentVersion) < 0) + .reduce((sorted, entry) => { + const insertAt = sorted.findIndex( + (candidate) => compareSemver(entry.version, candidate.version) > 0, + ); + if (insertAt === -1) { + sorted.push(entry); + } else { + sorted.splice(insertAt, 0, entry); + } + return sorted; + }, /** @type {VersionCandidate[]} */ ([])); + + const bestVersion = candidates[0]?.tag; + if (!bestVersion) return getLatestSemverTag(); + + const matchingTag = tagCandidates.find((entry) => entry.tag === bestVersion); + if (matchingTag) return matchingTag.tag; + + return getLatestSemverTag(); +} + function getOriginRepo() { const remote = git(["remote", "get-url", "origin"]); const match = remote.match(/github\.com[:/]([^/]+\/[^/.]+)(?:\.git)?$/); @@ -164,7 +265,7 @@ function buildNotes({ from, to, repo }) { return lines.join("\n").trim(); } -const from = values.from ?? getLatestSemverTag(); +const from = values.from ?? getDefaultFromTag(); const to = values.to; const repo = values.repo ?? getOriginRepo(); const notes = buildNotes({ from, to, repo });