Skip to content
Merged
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
70 changes: 56 additions & 14 deletions script/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tag>] [--to <ref>]
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 <tag> Previous release tag (default: latest stable V2 tag)
-t, --to <ref> Ending ref (default: HEAD)
--print Print the generated review and exit without releasing`)
process.exit(0)
}

Expand All @@ -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 <tag>] [--to <ref>]")
process.exit(1)
}

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down
Loading