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
196 changes: 137 additions & 59 deletions script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ import { Script } from "@opencode-ai/script"

const notes = [] as string[]

const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]

function getAreaFromPath(file: string): string {
if (file.startsWith("packages/")) {
const parts = file.replace("packages/", "").split("/")
if (parts[0] === "extensions" && parts[1]) return `extensions/${parts[1]}`
return parts[0] || "other"
}
if (file.startsWith("sdks/")) {
const name = file.replace("sdks/", "").split("/")[0] || "other"
return `extensions/${name}`
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code maps "sdks/" paths to "extensions/{name}" (line 30), but the AI prompt categorizes "sdk" area as part of the "SDK" category (line 130), while "extensions/*" are in the "Extensions" category (line 131). This inconsistency means changes in "sdks/" will be incorrectly categorized as Extensions instead of SDK, which could confuse the changelog grouping.

Suggested change
return `extensions/${name}`
return `sdks/${name}`

Copilot uses AI. Check for mistakes.
}
const rootDir = file.split("/")[0]
if (rootDir && !rootDir.includes(".")) return rootDir
return "other"
}

console.log("=== publishing ===\n")

if (!Script.preview) {
Expand All @@ -16,13 +44,59 @@ if (!Script.preview) {
})
.then((data: any) => data.version)

const log =
await $`git log v${previous}..HEAD --oneline --format="%h %s" -- packages/opencode packages/sdk packages/plugin packages/tauri packages/desktop`.text()
// Fetch commit authors from GitHub API (hash -> login)
const compare =
await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {sha: .sha, login: .author.login, message: .commit.message}'`.text()
const authorByHash = new Map<string, string>()
const contributors = new Map<string, string[]>()

const commits = log
.split("\n")
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:)/i))
.join("\n")
for (const line of compare.split("\n").filter(Boolean)) {
const { sha, login, message } = JSON.parse(line) as { sha: string; login: string | null; message: string }
const shortHash = sha.slice(0, 7)
if (login) authorByHash.set(shortHash, login)

const title = message.split("\n")[0] || ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue
if (login && !team.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}

// Batch-fetch files for all commits (hash -> areas)
const diffLog = await $`git log v${previous}..HEAD --name-only --format="%h"`.text()
const areasByHash = new Map<string, Set<string>>()
let currentHash: string | null = null

for (const rawLine of diffLog.split("\n")) {
const line = rawLine.trim()
if (!line) continue
if (/^[0-9a-f]{7}$/i.test(line)) {
currentHash = line
if (!areasByHash.has(currentHash)) areasByHash.set(currentHash, new Set())
continue
}
if (currentHash) {
areasByHash.get(currentHash)!.add(getAreaFromPath(line))
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-nullish assertion operator (!) is used without prior validation. If areasByHash.get(currentHash) returns undefined (which shouldn't happen given the logic, but could in edge cases), this will throw a runtime error. Consider using optional chaining or an existence check for defensive programming.

Suggested change
areasByHash.get(currentHash)!.add(getAreaFromPath(line))
let areas = areasByHash.get(currentHash)
if (!areas) {
areas = new Set<string>()
areasByHash.set(currentHash, areas)
}
areas.add(getAreaFromPath(line))

Copilot uses AI. Check for mistakes.
}
}

// Build commit lines with author and areas
const log = await $`git log v${previous}..HEAD --oneline --format="%h %s"`.text()
const commitLines = log.split("\n").filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:|release:)/i))

const commitsWithMeta = commitLines
.map((line) => {
const hash = line.split(" ")[0]
if (!hash) return null
const author = authorByHash.get(hash)
const authorStr = author ? ` [author: ${author}]` : ""
const areas = areasByHash.get(hash)
const areaStr = areas && areas.size > 0 ? ` [areas: ${[...areas].join(", ")}]` : " [areas: other]"
return `${line}${authorStr}${areaStr}`
})
.filter(Boolean) as string[]
const commits = commitsWithMeta.join("\n")

const opencode = await createOpencode()
const session = await opencode.client.session.create()
Expand All @@ -35,37 +109,72 @@ if (!Script.preview) {
body: {
model: {
providerID: "opencode",
modelID: "claude-haiku-4-5",
modelID: "gemini-3-flash",
},
parts: [
{
type: "text",
text: `
Analyze these commits and generate a changelog of all notable user facing changes.

Commits between ${previous} and HEAD:
${commits}

- Do NOT make general statements about "improvements", be very specific about what was changed.
- Do NOT include any information about code changes if they do not affect the user facing changes.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG)
- If a commit was made and then reverted do not include it in the changelog. If the commits only include a revert but not the original commit, then include the revert in the changelog.

IMPORTANT: ONLY return a bulleted list of changes, do not include any other information. Do not include a preamble like "Based on my analysis..."

<example>
- Added ability to @ mention agents
- Fixed a bug where the TUI would render improperly on some terminals
</example>
`,
Analyze these commits and generate a changelog of all notable user facing changes, grouped by area.

Each commit below includes:
- [author: username] showing the GitHub username of the commit author
- [areas: ...] showing which areas of the codebase were modified

Commits between ${previous} and HEAD:
${commits}

Group the changes into these categories based on the [areas: ...] tags (omit any category with no changes):
- **TUI**: Changes to "opencode" area (the terminal/CLI interface)
- **Desktop**: Changes to "desktop" or "tauri" areas (the desktop application)
- **SDK**: Changes to "sdk" or "plugin" areas (the SDK and plugin system)
- **Extensions**: Changes to "extensions/zed", "extensions/vscode", or "github" areas (editor extensions and GitHub Action)
- **Other**: Any user-facing changes that don't fit the above categories

Excluded areas (omit these entirely unless they contain user-facing changes like refactors that may affect behavior):
- "nix", "infra", "script" - CI/build infrastructure
- "ui", "docs", "web", "console", "enterprise", "function", "util", "identity", "slack" - internal packages

Rules:
- Use the [areas: ...] tags to determine the correct category. If a commit touches multiple areas, put it in the most relevant user-facing category.
- ONLY include commits that have user-facing impact. Omit purely internal changes (CI, build scripts, internal tooling).
- However, DO include refactors that touch user-facing code - refactors can introduce bugs or change behavior.
Comment on lines +136 to +141
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt instructs to exclude "ui", "docs", "web", "console", "enterprise", "function", "util", "identity", "slack" areas as "internal packages". However, at least some of these appear to be user-facing: "docs" likely contains documentation that users read, and "web"/"console" might be user-facing interfaces. Additionally, the Instructions state these should be "omitted entirely unless they contain user-facing changes", but changes to documentation or web UI are inherently user-facing. This could result in important user-visible changes being omitted from release notes.

Suggested change
- "ui", "docs", "web", "console", "enterprise", "function", "util", "identity", "slack" - internal packages
Rules:
- Use the [areas: ...] tags to determine the correct category. If a commit touches multiple areas, put it in the most relevant user-facing category.
- ONLY include commits that have user-facing impact. Omit purely internal changes (CI, build scripts, internal tooling).
- However, DO include refactors that touch user-facing code - refactors can introduce bugs or change behavior.
- "enterprise", "function", "util", "identity", "slack" - internal tooling/packages that are not directly user-facing
Rules:
- Use the [areas: ...] tags to determine the correct category. If a commit touches multiple areas, put it in the most relevant user-facing category.
- ONLY include commits that have user-facing impact. Omit purely internal changes (CI, build scripts, internal tooling).
- However, DO include refactors that touch user-facing code - refactors can introduce bugs or change behavior.
- Changes to documentation ("docs") or user interfaces ("ui", "web", "console") are generally user-facing and SHOULD be included when they affect the user experience.

Copilot uses AI. Check for mistakes.
- Do NOT make general statements about "improvements", be very specific about what was changed.
- For commits that are already well-written and descriptive, avoid rewording them. Simply capitalize the first letter, fix any misspellings, and ensure proper English grammar.
- DO NOT read any other commits than the ones listed above (THIS IS IMPORTANT TO AVOID DUPLICATING THINGS IN OUR CHANGELOG).
- If a commit was made and then reverted do not include it in the changelog. If the commits only include a revert but not the original commit, then include the revert in the changelog.
- Omit categories that have no changes.
- For community contributors: if the [author: username] is NOT in the team list, add (@username) at the end of the changelog entry. This is REQUIRED for all non-team contributors.
- The team members are: ${team.join(", ")}. Do NOT add @ mentions for team members.
Comment on lines +147 to +148
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt instructions include "DO NOT add @ mentions for team members" but also require "(@username) at the end of the changelog entry" for non-team contributors. However, the team list uses different formats: some entries have "[bot]" suffix ("opencode-agent[bot]") while the GitHub API returns login names. If a bot account's login is returned without the "[bot]" suffix by the API, it might not match the team list correctly and could be incorrectly treated as a community contributor.

Copilot uses AI. Check for mistakes.

IMPORTANT: ONLY return the grouped changelog, do not include any other information. Do not include a preamble like "Based on my analysis..." or "Here is the changelog..."

<example>
## TUI
- Added experimental support for the Ty language server (@OpeOginni)
- Added /fork slash command for keyboard-friendly session forking (@ariane-emory)
- Increased retry attempts for failed requests
- Fixed model validation before executing slash commands (@devxoul)

## Desktop
- Added shell mode support
- Fixed prompt history navigation and optimistic prompt duplication
- Disabled pinch-to-zoom on Linux (@Brendonovich)

## Extensions
Comment on lines +153 to +164
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example in the prompt shows "## TUI", "## Desktop", and "## Extensions" sections, but the category definition on line 132 says "Other" with bold formatting. This inconsistency in formatting (## vs **) could cause the AI model to produce inconsistent output, making the parsing logic on lines 174-179 fail to correctly identify category headers.

Suggested change
## TUI
- Added experimental support for the Ty language server (@OpeOginni)
- Added /fork slash command for keyboard-friendly session forking (@ariane-emory)
- Increased retry attempts for failed requests
- Fixed model validation before executing slash commands (@devxoul)
## Desktop
- Added shell mode support
- Fixed prompt history navigation and optimistic prompt duplication
- Disabled pinch-to-zoom on Linux (@Brendonovich)
## Extensions
**TUI**
- Added experimental support for the Ty language server (@OpeOginni)
- Added /fork slash command for keyboard-friendly session forking (@ariane-emory)
- Increased retry attempts for failed requests
- Fixed model validation before executing slash commands (@devxoul)
**Desktop**
- Added shell mode support
- Fixed prompt history navigation and optimistic prompt duplication
- Disabled pinch-to-zoom on Linux (@Brendonovich)
**Extensions**

Copilot uses AI. Check for mistakes.
- Added OIDC_BASE_URL support for custom GitHub App installations (@elithrar)
</example>
`,
},
],
},
})
.then((x) => x.data?.parts?.find((y) => y.type === "text")?.text)
for (const line of raw?.split("\n") ?? []) {
if (line.startsWith("- ")) {
if (line.startsWith("## ")) {
if (notes.length > 0) notes.push("")
notes.push(line)
} else if (line.startsWith("- ")) {
notes.push(line)
}
}
Expand All @@ -74,42 +183,11 @@ if (!Script.preview) {
console.log("-----------------------------")
opencode.server.close()

// Get contributors
const team = [
"actions-user",
"opencode",
"rekram1-node",
"thdxr",
"kommander",
"jayair",
"fwang",
"adamdotdevin",
"iamdavidhill",
"opencode-agent[bot]",
]
const compare =
await $`gh api "/repos/sst/opencode/compare/v${previous}...HEAD" --jq '.commits[] | {login: .author.login, message: .commit.message}'`.text()
const contributors = new Map<string, string[]>()

for (const line of compare.split("\n").filter(Boolean)) {
const { login, message } = JSON.parse(line) as { login: string | null; message: string }
const title = message.split("\n")[0] ?? ""
if (title.match(/^(ignore:|test:|chore:|ci:|release:)/i)) continue

if (login && !team.includes(login)) {
if (!contributors.has(login)) contributors.set(login, [])
contributors.get(login)?.push(title)
}
}

if (contributors.size > 0) {
notes.push("")
notes.push(`**Thank you to ${contributors.size} community contributor${contributors.size > 1 ? "s" : ""}:**`)
for (const [username, userCommits] of contributors) {
notes.push(`- @${username}:`)
for (const commit of userCommits) {
notes.push(` - ${commit}`)
}
for (const username of contributors.keys()) {
notes.push(`- @${username}`)
}
}
}
Expand Down
Loading