From da64c1cda63fe3b76524a9d51dd18bc09a5b4976 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 8 Oct 2026 10:11:21 -0400 Subject: [PATCH] feat(release): lead V2 release notes with highlights --- script/release.ts | 70 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/script/release.ts b/script/release.ts index 03bcb02acb67..653b7d5d6e25 100755 --- a/script/release.ts +++ b/script/release.ts @@ -5,19 +5,36 @@ import { OpenCode } from "@opencode/client" import { Service } from "@opencode/client/service" import { rm } from "fs/promises" import path from "path" +import { parseArgs } from "util" const root = path.resolve(import.meta.dir, "..") const review = path.join(root, "RELEASE_REVIEW.md") const changelog = path.join(root, "UPCOMING_CHANGELOG.md") -const version = Bun.argv[2] ?? "patch" +const { values, positionals } = parseArgs({ + args: Bun.argv.slice(2), + options: { + from: { type: "string", short: "f" }, + to: { type: "string", short: "t", default: "HEAD" }, + print: { type: "boolean", default: false }, + help: { type: "boolean", short: "h", default: false }, + }, + allowPositionals: true, +}) +const version = positionals[0] ?? "patch" -if (Bun.argv.includes("--help") || Bun.argv.includes("-h")) { +if (values.help) { console.log(`Usage: ./script/release.ts [major|minor|patch|version] + ./script/release.ts --print [--from ] [--to ] Generates release notes and a public API review, opens the result in your editor, and asks for confirmation before triggering the release workflow. -The editor is selected from $VISUAL, then $EDITOR, and defaults to vim.`) +The editor is selected from $VISUAL, then $EDITOR, and defaults to vim. + +Options: + -f, --from Previous release tag (default: latest stable V2 tag) + -t, --to Ending ref (default: HEAD) + --print Print the generated review and exit without releasing`) process.exit(0) } @@ -27,8 +44,9 @@ const input = ["major", "minor", "patch"].includes(version) ? ["-f", `version=${version}`] : undefined -if (!input || Bun.argv.length > 3) { +if (!input || positionals.length > 1 || (!values.print && (values.from || values.to !== "HEAD"))) { console.error("Usage: ./script/release.ts [major|minor|patch|version]") + console.error(" ./script/release.ts --print [--from ] [--to ]") process.exit(1) } @@ -37,14 +55,18 @@ process.chdir(root) console.log("\n=== Generating changelog ===\n") await rm(review, { force: true }) await $`git fetch origin "+refs/tags/v2.*:refs/tags/v2.*"` -const base = (await $`git tag --list "v2.*" --sort=-version:refname`.text()) - .split("\n") - .find((tag) => /^v2\.\d+\.\d+$/.test(tag)) +const base = + values.from ?? + (await $`git tag --list "v2.*" --sort=-version:refname`.text()).split("\n").find((tag) => /^v2\.\d+\.\d+$/.test(tag)) if (!base) throw new Error("No stable V2 release tag was found") -console.log(`Comparing ${base}..HEAD`) -await generateReview(base) +console.log(`Comparing ${base}..${values.to}`) +await generateReview(base, values.to) if (!(await Bun.file(review).exists())) throw new Error("OpenCode did not create RELEASE_REVIEW.md") +if (values.print) { + process.stdout.write(await Bun.file(review).text()) + process.exit(0) +} const editor = Bun.spawn(["sh", "-c", 'exec ${VISUAL:-${EDITOR:-vim}} "$1"', "release", review], { cwd: root, @@ -74,7 +96,7 @@ if (answer?.trim().toLowerCase() !== "y" && answer?.trim().toLowerCase() !== "ye await $`gh workflow run publish.yml --ref v2 ${input} -f release_notes=${releaseNotes}` console.log(`Triggered the ${version} release`) -async function generateReview(base: string) { +async function generateReview(base: string, head: string) { const endpoint = await Service.ensure() const client = OpenCode.make({ baseUrl: endpoint.url, @@ -114,7 +136,7 @@ async function generateReview(base: string) { })() try { - await client.session.prompt({ sessionID: session.id, text: reviewPrompt(base) }) + await client.session.prompt({ sessionID: session.id, text: reviewPrompt(base, head) }) await completed const messages = await client.message.list({ sessionID: session.id, limit: 100, order: "desc" }) const response = messages.data.find((message) => message.type === "assistant") @@ -127,14 +149,34 @@ async function generateReview(base: string) { } } -function reviewPrompt(base: string) { +function reviewPrompt(base: string, head: string) { return `Create a concise pre-release review for a maintainer. -The previous stable V2 release is ${base}. Inspect every relevant commit and actual diff in the exact range ${base}..HEAD. +The previous stable V2 release is ${base}. Inspect every relevant commit and actual diff in the exact range ${base}..${head}. Do not compare against dev, V1 release tags, or commits outside that range. Do not rely only on commit titles. Write a user-facing changelog with sections for Core, TUI, Desktop, SDK, and Extensions, omitting empty sections and changes that are entirely internal. Group bug fixes separately from improvements. Preserve community contributor attribution when it -is available from merged pull requests. +is available from merged pull requests, but never thank authors listed in .github/TEAM_MEMBERS. + +Start the changelog with a "### Highlights" section: the few changes worth telling people about, written for humans. +Pick 2 to 5 user-visible changes a user would notice, try, or share: new capabilities, clearly better workflows, or fixes +for problems many users hit. Merge related small changes into one highlight. Skip internal work, refactors, tests, CI, +dependency bumps, broad polish without one concrete outcome, and anything not usable in this release. When nothing +qualifies, omit the section instead of padding it. Order highlights by how much users will care. + +Before writing a highlight, read its pull request with gh pr view and confirm the behavior in the diff. Name the exact +command, flag, setting, or place in the UI, and describe how to use it as the pull request does. Never invent, guess, or +embellish, and avoid vague claims like "better" or "more naturally". Write each highlight as one bullet with a few-word +Title Case title (lowercase short words like "and" or "to"), then one or two plain sentences in second person about what +you can now do and why it matters. Vary the sentence openings, and leave contributor thanks to the detailed +sections. For example: + +- **Reopen Closed Tabs**: Right-click the new-session button in the tab bar to bring back a session you closed by + mistake. ([#123](https://github.com/anomalyco/opencode/pull/123)) + +Every highlight must also appear in the detailed sections that follow. Use "###" for Highlights and each release section, +"####" for Improvements and Bug fixes, and never use "#" or "##" headings in the changelog, since each release is filed +under its own "##" heading. Put the complete editable changelog between these markers, which must each appear exactly once outside code fences: