Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .claude/commands/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
name: release-notes
description: Draft release notes and recommend a version bump for the next mouseterm release by analyzing all merge commits and squash-merged PRs since the last release tag. Outputs a Keep a Changelog section ready to paste into CHANGELOG.md. Used as step 2 of the release checklist in docs/specs/deploy.md.
description: Draft release notes, recommend and apply a version bump, and update CHANGELOG.md for the next mouseterm release by analyzing all merge commits and squash-merged PRs since the last release tag. Used as step 2 of the release checklist in docs/specs/deploy.md.
user-invocable: true
---

You are drafting release notes and recommending a version bump for the next mouseterm release.
You are drafting release notes, recommending and applying a version bump, and updating CHANGELOG.md for the next mouseterm release.

## 1. Gather context

Expand Down Expand Up @@ -34,7 +34,11 @@ mouseterm uses **breaking.added.bugfix** semantics (semver-shaped, but named for

Pick the highest-severity bump that any single change requires.

## 3. Edit `CHANGELOG.md`
## 3. Run the version bump

Run `./scripts/bump-version.sh X.Y.Z` with the recommended version before editing the changelog. Show the script's output so the user can review the diff stat.

## 4. Edit `CHANGELOG.md`

Edit `CHANGELOG.md` directly — insert a new section above the most recent existing release, in this exact shape:

Expand Down Expand Up @@ -65,8 +69,10 @@ Rules for the entries:

Do not ask the user to paste it themselves — make the edit. The earlier flat-bullet entries (0.8.0 and below) are legacy; do not reformat them.

## 4. Run the version bump
## 5. Finish

Do not run `website/scripts/generate-changelog.js` as part of the release-notes command. `website/src/data/changelog.json` is a gitignored generated artifact, and the website `prebuild`, `predev`, and `pretest` lifecycle scripts regenerate it from `CHANGELOG.md`.

After saving the changelog edit, run `./scripts/bump-version.sh X.Y.Z` with the recommended version. Show the script's output so the user can review the diff stat. Then remind the user of the next step:
Then remind the user of the next step:

> Review the diff, then `git commit -am 'Release vX.Y.Z'` and `git tag vX.Y.Z`.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ standalone/node_modules/
# Website
website/dist/
website/node_modules/
website/src/data/changelog.json

# OS
.DS_Store
Expand Down
22 changes: 13 additions & 9 deletions docs/specs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ Every release produces three artifact groups under one version and changelog:
Human-driven steps, in order:

1. **Update dependencies page** — run `node website/scripts/generate-deps.js` and review the diff in `website/src/data/dependencies.json`. Commit if changed.
2. **Draft release notes and pick version** — run `/release-notes` in Claude Code at the repo root. The slash command (defined in [.claude/commands/release-notes.md](../../.claude/commands/release-notes.md)) walks the merge commits and squash-merged PRs since the last tag, drafts a Keep a Changelog block, and recommends a `breaking.added.bugfix` version bump. Review and edit the output, paste it into `CHANGELOG.md` replacing the `[Unreleased]` section, and use the recommended `X.Y.Z` in the next step.

3. **Bump versions** — `./scripts/bump-version.sh X.Y.Z`. Edits the four version files in lockstep and runs `cargo check` so `Cargo.lock` follows along.
4. **Commit and tag** — `git commit -m "Release vX.Y.Z"` then `git tag vX.Y.Z`.
5. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1).
6. **Set environment variables** — copy the relevant secrets into the terminal from your password manager (see [Environment / secrets](#environment--secrets) for the list).
7. **Run local signing** — plug in the PIV USB key, then `./scripts/sign-and-deploy.sh all X.Y.Z`. The script waits for CI, downloads unsigned artifacts, signs macOS + Windows, generates the Tauri update manifest into `website/public/standalone-latest.json`, and creates the GitHub Release. Run `./scripts/sign-and-deploy.sh --help` for resume-after-failure subcommands.
8. **Deploy website** — commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live.
9. **Verify the release**
2. **Draft release notes and bump version** — run `/release-notes` in Claude Code at the repo root. The slash command (defined in [.claude/commands/release-notes.md](../../.claude/commands/release-notes.md)) walks the merge commits and squash-merged PRs since the last tag, recommends a `breaking.added.bugfix` version bump, runs `./scripts/bump-version.sh X.Y.Z`, and edits `CHANGELOG.md` for the same version. Review and edit the resulting diff if needed.
3. **Commit and tag** — `git commit -am "Release vX.Y.Z"` then `git tag vX.Y.Z`.
4. **Push** — `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1).
5. **Set environment variables** — copy the relevant secrets into the terminal from your password manager (see [Environment / secrets](#environment--secrets) for the list).
6. **Run local signing** — plug in the PIV USB key, then `./scripts/sign-and-deploy.sh all X.Y.Z`. The script waits for CI, downloads unsigned artifacts, signs macOS + Windows, generates the Tauri update manifest into `website/public/standalone-latest.json`, and creates the GitHub Release. Run `./scripts/sign-and-deploy.sh --help` for resume-after-failure subcommands.
7. **Deploy website** — commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live.
8. **Verify the release**
- Check GitHub Release assets are correct
- On a Mac: extract the `.tar.gz`, open the `.app`, confirm no Gatekeeper warnings
- On Windows: run the `.exe` installer, confirm no SmartScreen warnings
Expand Down Expand Up @@ -204,6 +202,12 @@ Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) bu

A single `CHANGELOG.md` at the repo root, following [Keep a Changelog](https://keepachangelog.com/) format. The `[Unreleased]` section is promoted to `[X.Y.Z]` at release time. The release notes include both standalone and VSCode changes in one entry.

The website changelog page imports generated data from `website/src/data/changelog.json`, but `CHANGELOG.md` is the source of truth and the JSON is gitignored. You do not normally run `website/scripts/generate-changelog.js` by hand:
- `pnpm --filter mouseterm-website build` runs it through the website `prebuild` script before Vite bundles the static site.
- `pnpm --filter mouseterm-website dev` and `pnpm --filter mouseterm-website test` also regenerate it through lifecycle scripts so clean checkouts work locally.

If you edit `CHANGELOG.md` manually outside `/release-notes` and want to preview the generated data immediately, run `node website/scripts/generate-changelog.js`. Do not commit `website/src/data/changelog.json`.

## Environment / secrets

| Secret | Where | Purpose |
Expand Down
4 changes: 3 additions & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"type": "module",
"scripts": {
"dev": "vite-react-ssg dev",
"prebuild": "node scripts/generate-deps.js",
"predev": "node scripts/generate-changelog.js",
"prebuild": "node scripts/generate-deps.js && node scripts/generate-changelog.js",
"build": "vite-react-ssg build",
"preview": "vite preview",
"pretest": "node scripts/generate-changelog.js",
"test": "vitest run"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions website/public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/changelog/after/* /changelog 200
88 changes: 88 additions & 0 deletions website/scripts/changelog-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const RELEASE_HEADING_RE = /^## \[([^\]]+)\](?: - (\d{4}-\d{2}-\d{2}))?\s*$/;
const SECTION_HEADING_RE = /^###\s+(.+?)\s*$/;
const BULLET_RE = /^(\s*)-\s+(.*)$/;

function normalizeVersion(version) {
return version.trim().replace(/^v/i, "");
}

function createRelease(rawVersion, date) {
const version = normalizeVersion(rawVersion);
return {
version,
tag: `v${version}`,
date: date ?? null,
sections: [],
};
}

function ensureSection(release, title = "Changes") {
if (!release.sections.length || release.sections[release.sections.length - 1].title !== title) {
release.sections.push({ title, items: [] });
}
return release.sections[release.sections.length - 1];
}

function pushBullet(section, indent, text) {
const item = { text: text.trim(), children: [] };
const isNested = indent.replace(/\t/g, " ").length >= 2;
const lastTopLevelItem = section.items[section.items.length - 1];

if (isNested && lastTopLevelItem) {
lastTopLevelItem.children.push(item);
return;
}

section.items.push(item);
}

function appendContinuation(section, text) {
const trimmed = text.trim();
if (!trimmed) return;

const lastTopLevelItem = section.items[section.items.length - 1];
if (lastTopLevelItem) {
lastTopLevelItem.text = `${lastTopLevelItem.text} ${trimmed}`;
return;
}

section.items.push({ text: trimmed, children: [] });
}

export function parseChangelog(markdown) {
const releases = [];
let currentRelease = null;
let currentSection = null;

for (const line of markdown.replace(/\r\n?/g, "\n").split("\n")) {
const releaseHeading = line.match(RELEASE_HEADING_RE);
if (releaseHeading) {
currentRelease = createRelease(releaseHeading[1], releaseHeading[2]);
releases.push(currentRelease);
currentSection = null;
continue;
}

if (!currentRelease) continue;

const sectionHeading = line.match(SECTION_HEADING_RE);
if (sectionHeading) {
currentSection = ensureSection(currentRelease, sectionHeading[1].trim());
continue;
}

if (!line.trim()) continue;

const bullet = line.match(BULLET_RE);
if (bullet) {
currentSection = currentSection ?? ensureSection(currentRelease);
pushBullet(currentSection, bullet[1], bullet[2]);
continue;
}

currentSection = currentSection ?? ensureSection(currentRelease);
appendContinuation(currentSection, line);
}

return { releases };
}
54 changes: 54 additions & 0 deletions website/scripts/changelog-parser.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { parseChangelog } from "./changelog-parser.js";

describe("parseChangelog", () => {
it("parses release headings, sections, and nested bullets", () => {
const parsed = parseChangelog(`# Changelog

## [0.2.0] - 2026-01-02
### Added
- First item.
- Nested detail.

### Fixed
- Second item.

## [0.1.0] - 2026-01-01
- Initial release.
`);

expect(parsed.releases).toEqual([
{
version: "0.2.0",
tag: "v0.2.0",
date: "2026-01-02",
sections: [
{
title: "Added",
items: [
{
text: "First item.",
children: [{ text: "Nested detail.", children: [] }],
},
],
},
{
title: "Fixed",
items: [{ text: "Second item.", children: [] }],
},
],
},
{
version: "0.1.0",
tag: "v0.1.0",
date: "2026-01-01",
sections: [
{
title: "Changes",
items: [{ text: "Initial release.", children: [] }],
},
],
},
]);
});
});
14 changes: 14 additions & 0 deletions website/scripts/generate-changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseChangelog } from "./changelog-parser.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "../..");
const changelogPath = resolve(repoRoot, "CHANGELOG.md");
const outPath = resolve(__dirname, "../src/data/changelog.json");

const changelog = parseChangelog(readFileSync(changelogPath, "utf-8"));

writeFileSync(outPath, JSON.stringify(changelog, null, 2) + "\n");
console.log(`Wrote ${changelog.releases.length} changelog releases to src/data/changelog.json`);
8 changes: 8 additions & 0 deletions website/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ export const routes: RouteRecord[] = [
path: "/playground",
lazy: () => import("./pages/Playground"),
},
{
path: "/changelog",
lazy: () => import("./pages/Changelog"),
},
{
path: "/changelog/after/:version",
lazy: () => import("./pages/Changelog"),
},
{
path: "/dependencies",
lazy: () => import("./pages/Dependencies"),
Expand Down
5 changes: 5 additions & 0 deletions website/src/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { forwardRef } from "react";

export const STATIC_PAGE_HEADER_STYLE: React.CSSProperties = {
background: "rgba(10, 10, 10, 0.85)",
backdropFilter: "blur(12px)",
};

const NAV_LINKS: readonly { href: string; label: string; external?: boolean; hideOnMobile?: boolean }[] = [
{ href: "/playground", label: "Playground", hideOnMobile: true },
{ href: "/#download", label: "Download", hideOnMobile: true },
Expand Down
Loading
Loading