2025-07-25 03:51:33 +08:00
# ! / u s r / b i n / e n v b u n
import { $ } from "bun"
2025-12-20 07:46:36 +08:00
import { createOpencode } from "@opencode-ai/sdk"
2025-10-15 02:34:19 +08:00
import { Script } from "@opencode-ai/script"
2025-09-10 15:36:42 +08:00
2025-12-20 07:46:36 +08:00
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 } `
}
const rootDir = file . split ( "/" ) [ 0 ]
if ( rootDir && ! rootDir . includes ( "." ) ) return rootDir
return "other"
}
2025-12-19 08:49:37 +08:00
2025-11-22 12:08:35 +08:00
console . log ( "=== publishing ===\n" )
if ( ! Script . preview ) {
2025-12-10 10:57:03 +08:00
const previous = await fetch ( "https://registry.npmjs.org/opencode-ai/latest" )
2025-11-22 12:08:35 +08:00
. then ( ( res ) = > {
if ( ! res . ok ) throw new Error ( res . statusText )
return res . json ( )
} )
. then ( ( data : any ) = > data . version )
2025-12-20 07:46:36 +08:00
// 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 [ ] > ( )
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 ) )
}
}
// 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 ( )
2025-11-22 12:08:35 +08:00
console . log ( "generating changelog since " + previous )
2025-12-20 07:46:36 +08:00
const raw = await opencode . client . session
. prompt ( {
path : {
id : session.data ! . id ,
} ,
body : {
model : {
providerID : "opencode" ,
modelID : "gemini-3-flash" ,
} ,
parts : [
{
type : "text" ,
text : `
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 .
- 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 .
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
- 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 ( notes . length > 0 ) notes . push ( "" )
notes . push ( line )
} else if ( line . startsWith ( "- " ) ) {
notes . push ( line )
}
}
2025-11-22 12:08:35 +08:00
console . log ( "---- Generated Changelog ----" )
2025-12-20 07:46:36 +08:00
console . log ( notes . join ( "\n" ) )
2025-11-22 12:08:35 +08:00
console . log ( "-----------------------------" )
2025-12-20 07:46:36 +08:00
opencode . server . close ( )
if ( contributors . size > 0 ) {
notes . push ( "" )
notes . push ( ` **Thank you to ${ contributors . size } community contributor ${ contributors . size > 1 ? "s" : "" } :** ` )
for ( const username of contributors . keys ( ) ) {
notes . push ( ` - @ ${ username } ` )
}
}
2025-11-22 12:08:35 +08:00
}
const pkgjsons = await Array . fromAsync (
new Bun . Glob ( "**/package.json" ) . scan ( {
absolute : true ,
} ) ,
) . then ( ( arr ) = > arr . filter ( ( x ) = > ! x . includes ( "node_modules" ) && ! x . includes ( "dist" ) ) )
for ( const file of pkgjsons ) {
let pkg = await Bun . file ( file ) . text ( )
pkg = pkg . replaceAll ( /"version": "[^"]+"/g , ` "version": " ${ Script . version } " ` )
console . log ( "updated:" , file )
await Bun . file ( file ) . write ( pkg )
}
const extensionToml = new URL ( "../packages/extensions/zed/extension.toml" , import . meta . url ) . pathname
let toml = await Bun . file ( extensionToml ) . text ( )
toml = toml . replace ( /^version = "[^"]+"/m , ` version = " ${ Script . version } " ` )
toml = toml . replaceAll ( /releases\/download\/v[^/]+\//g , ` releases/download/v ${ Script . version } / ` )
console . log ( "updated:" , extensionToml )
await Bun . file ( extensionToml ) . write ( toml )
await $ ` bun install `
console . log ( "\n=== opencode ===\n" )
await import ( ` ../packages/opencode/script/publish.ts ` )
console . log ( "\n=== sdk ===\n" )
await import ( ` ../packages/sdk/js/script/publish.ts ` )
console . log ( "\n=== plugin ===\n" )
await import ( ` ../packages/plugin/script/publish.ts ` )
const dir = new URL ( ".." , import . meta . url ) . pathname
process . chdir ( dir )
2025-10-15 02:34:19 +08:00
if ( ! Script . preview ) {
await $ ` git commit -am "release: v ${ Script . version } " `
await $ ` git tag v ${ Script . version } `
2025-10-02 13:19:44 +08:00
await $ ` git fetch origin `
await $ ` git cherry-pick HEAD..origin/dev ` . nothrow ( )
2025-10-17 03:36:12 +08:00
await $ ` git push origin HEAD --tags --no-verify --force-with-lease `
2025-11-03 09:55:20 +08:00
await new Promise ( ( resolve ) = > setTimeout ( resolve , 5 _000 ) )
2025-12-20 07:46:36 +08:00
await $ ` gh release create v ${ Script . version } -d --title "v ${ Script . version } " --notes ${ notes . join ( "\n" ) || "No notable changes" } ./packages/opencode/dist/*.zip ./packages/opencode/dist/*.tar.gz `
2025-12-12 00:41:52 +08:00
const release = await $ ` gh release view v ${ Script . version } --json id,tagName ` . json ( )
if ( process . env . GITHUB_OUTPUT ) {
await Bun . write ( process . env . GITHUB_OUTPUT , ` releaseId= ${ release . id } \ ntagName= ${ release . tagName } \ n ` )
}
2025-07-31 13:00:29 +08:00
}