Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
Kit Langton
3496b9caf5 refactor(release): polish Effect publish scripts
Use Effect.ensuring to restore temporary package manifests on failure and tidy the opencode packager with a small parallel hash helper so the stacked Effect rewrite reads cleaner without changing behavior.
2026-04-17 23:00:25 -04:00
Kit Langton
7f4dc4a249 refactor(release): rewrite publish scripts with Effect
Refactor the detached release flow and retry-safe package publishers into Effect.gen pipelines while preserving the stacked branch behavior from the release-fix PR.
2026-04-17 22:54:35 -04:00
Kit Langton
421f891e6b refactor(release): simplify detached publish flow
Keep the detached release commit and retry-safe package publishes, but drop the draft and tag reuse logic so the release workflow stays focused on preventing dev rewrites while syncing version files back onto the latest dev tip.
2026-04-17 22:43:22 -04:00
Kit Langton
325d199089
Merge branch 'dev' into kit/fix-release-publish 2026-04-17 22:27:32 -04:00
Kit Langton
c79859be43 fix(release): make publish reruns safe
Reuse the existing draft release and tagged snapshot, skip already-published packages on retry, and sync dev before undrafting so release reruns stop rewriting history and can recover cleanly from partial failures.
2026-04-17 22:18:33 -04:00
Kit Langton
f5cb737440 fix: stop rewriting dev during release publish 2026-04-16 20:54:20 -04:00
5 changed files with 373 additions and 182 deletions

View file

@ -1,5 +1,7 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { $ } from "bun" import { $ } from "bun"
import { Effect } from "effect"
import pkg from "../package.json" import pkg from "../package.json"
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
@ -7,72 +9,107 @@ import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url)) const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir) process.chdir(dir)
const binaries: Record<string, string> = {} const published = (name: string, version: string) =>
for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) { Effect.promise(() => $`npm view ${name}@${version} version`.nothrow()).pipe(
const pkg = await Bun.file(`./dist/${filepath}`).json() Effect.map((result) => result.exitCode === 0),
binaries[pkg.name] = pkg.version )
const sha256 = (path: string) =>
Effect.promise(() => $`sha256sum ${path} | cut -d' ' -f1`.text()).pipe(Effect.map((text) => text.trim()))
const publishPackage = (dir: string, name: string, version: string) =>
Effect.gen(function* () {
if (yield* published(name, version)) {
console.log(`already published ${name}@${version}`)
return
}
if (process.platform !== "win32") yield* Effect.promise(() => $`chmod -R 755 .`.cwd(dir))
yield* Effect.promise(() => $`bun pm pack`.cwd(dir))
yield* Effect.promise(() => $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir))
})
const binaryVersion = (value: unknown) => {
if (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string" &&
"version" in value &&
typeof value.version === "string"
) {
return value
}
throw new Error("invalid dist package manifest")
} }
console.log("binaries", binaries)
const version = Object.values(binaries)[0]
await $`mkdir -p ./dist/${pkg.name}` const ensureVersion = (value: unknown) => {
await $`cp -r ./bin ./dist/${pkg.name}/bin` if (typeof value === "string") return value
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs` throw new Error("missing dist package version")
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text()) }
await Bun.file(`./dist/${pkg.name}/package.json`).write( const program = Effect.gen(function* () {
const binaries: Record<string, string> = Object.fromEntries(
yield* Effect.promise(async () =>
Array.fromAsync(new Bun.Glob("*/package.json").scan({ cwd: "./dist" }), async (filepath) => {
const current = binaryVersion(await Bun.file(`./dist/${filepath}`).json())
return [current.name, current.version] as const
}),
),
)
console.log("binaries", binaries)
const version = ensureVersion(Object.values(binaries)[0])
yield* Effect.promise(() => $`mkdir -p ./dist/${pkg.name}`)
yield* Effect.promise(() => $`cp -r ./bin ./dist/${pkg.name}/bin`)
yield* Effect.promise(() => $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`)
yield* Effect.promise(async () =>
Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text()),
)
yield* Effect.promise(() =>
Bun.write(
`./dist/${pkg.name}/package.json`,
JSON.stringify( JSON.stringify(
{ {
name: pkg.name + "-ai", name: pkg.name + "-ai",
bin: { bin: { [pkg.name]: `./bin/${pkg.name}` },
[pkg.name]: `./bin/${pkg.name}`, scripts: { postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs" },
}, version,
scripts: {
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
},
version: version,
license: pkg.license, license: pkg.license,
optionalDependencies: binaries, optionalDependencies: binaries,
}, },
null, null,
2, 2,
), ),
) ),
)
const tasks = Object.entries(binaries).map(async ([name]) => { yield* Effect.all(Object.entries(binaries).map(([name, version]) => publishPackage(`./dist/${name}`, name, version)))
if (process.platform !== "win32") { yield* publishPackage(`./dist/${pkg.name}`, `${pkg.name}-ai`, version)
await $`chmod -R 755 .`.cwd(`./dist/${name}`)
}
await $`bun pm pack`.cwd(`./dist/${name}`)
await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(`./dist/${name}`)
})
await Promise.all(tasks)
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${Script.channel}`
const image = "ghcr.io/anomalyco/opencode" const image = "ghcr.io/anomalyco/opencode"
const platforms = "linux/amd64,linux/arm64" const tags = [`${image}:${version}`, `${image}:${Script.channel}`]
const tags = [`${image}:${version}`, `${image}:${Script.channel}`] yield* Effect.promise(
const tagFlags = tags.flatMap((t) => ["-t", t]) () => $`docker buildx build --platform linux/amd64,linux/arm64 ${tags.flatMap((t) => ["-t", t])} --push .`,
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .` )
// registries if (Script.preview) return
if (!Script.preview) {
// Calculate SHA values
const arm64Sha = await $`sha256sum ./dist/opencode-linux-arm64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const x64Sha = await $`sha256sum ./dist/opencode-linux-x64.tar.gz | cut -d' ' -f1`.text().then((x) => x.trim())
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const [pkgver, _subver = ""] = Script.version.split(/(-.*)/, 2) const [arm64Sha, x64Sha, macX64Sha, macArm64Sha] = yield* Effect.all([
sha256("./dist/opencode-linux-arm64.tar.gz"),
sha256("./dist/opencode-linux-x64.tar.gz"),
sha256("./dist/opencode-darwin-x64.zip"),
sha256("./dist/opencode-darwin-arm64.zip"),
])
const [pkgver, subver = ""] = Script.version.split(/(-.*)/, 2)
// arch
const binaryPkgbuild = [ const binaryPkgbuild = [
"# Maintainer: dax", "# Maintainer: dax",
"# Maintainer: adam", "# Maintainer: adam",
"", "",
"pkgname='opencode-bin'", "pkgname='opencode-bin'",
`pkgver=${pkgver}`, `pkgver=${pkgver}`,
`_subver=${_subver}`, `_subver=${subver}`,
"options=('!debug' '!strip')", "options=('!debug' '!strip')",
"pkgrel=1", "pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'", "pkgdesc='The AI coding agent built for the terminal.'",
@ -85,7 +122,6 @@ if (!Script.preview) {
"", "",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`, `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
`sha256sums_aarch64=('${arm64Sha}')`, `sha256sums_aarch64=('${arm64Sha}')`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`, `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
`sha256sums_x86_64=('${x64Sha}')`, `sha256sums_x86_64=('${x64Sha}')`,
"", "",
@ -95,36 +131,40 @@ if (!Script.preview) {
"", "",
].join("\n") ].join("\n")
for (const [pkg, pkgbuild] of [["opencode-bin", binaryPkgbuild]]) { yield* Effect.promise(async () => {
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
try { try {
await $`rm -rf ./dist/aur-${pkg}` await $`rm -rf ./dist/aur-opencode-bin`
await $`git clone ssh://aur@aur.archlinux.org/${pkg}.git ./dist/aur-${pkg}` await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
await $`cd ./dist/aur-${pkg} && git checkout master` await $`cd ./dist/aur-opencode-bin && git checkout master`
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild) await Bun.write(`./dist/aur-opencode-bin/PKGBUILD`, binaryPkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO` await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO` await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"` if ((await $`cd ./dist/aur-opencode-bin && git diff --cached --quiet`.nothrow()).exitCode === 0) return
await $`cd ./dist/aur-${pkg} && git push` await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${Script.version}"`
break await $`cd ./dist/aur-opencode-bin && git push`
} catch { return
continue } catch {}
}
} }
})
const token = process.env.GITHUB_TOKEN
if (!token) {
console.error("GITHUB_TOKEN is required to update homebrew tap")
process.exit(1)
} }
// Homebrew formula
const homebrewFormula = [ const homebrewFormula = [
"# typed: false", "# typed: false",
"# frozen_string_literal: true", "# frozen_string_literal: true",
"", "",
"# This file was generated by GoReleaser. DO NOT EDIT.", "# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula", "class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`, ' desc "The AI coding agent built for the terminal."',
` homepage "https://github.com/anomalyco/opencode"`, ' homepage "https://github.com/anomalyco/opencode"',
` version "${Script.version.split("-")[0]}"`, ` version "${Script.version.split("-")[0]}"`,
"", "",
` depends_on "ripgrep"`, ' depends_on "ripgrep"',
"", "",
" on_macos do", " on_macos do",
" if Hardware::CPU.intel?", " if Hardware::CPU.intel?",
@ -166,16 +206,15 @@ if (!Script.preview) {
"", "",
].join("\n") ].join("\n")
const token = process.env.GITHUB_TOKEN yield* Effect.promise(async () => {
if (!token) {
console.error("GITHUB_TOKEN is required to update homebrew tap")
process.exit(1)
}
const tap = `https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap` await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap` await $`git clone https://x-access-token:${token}@github.com/anomalyco/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula) await Bun.write("./dist/homebrew-tap/opencode.rb", homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb` await $`cd ./dist/homebrew-tap && git add opencode.rb`
if ((await $`cd ./dist/homebrew-tap && git diff --cached --quiet`.nothrow()).exitCode === 0) return
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"` await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push` await $`cd ./dist/homebrew-tap && git push`
} })
})
await Effect.runPromise(program)

View file

@ -1,22 +1,76 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { $ } from "bun" import { $ } from "bun"
import { Effect } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url)) const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir) process.chdir(dir)
await $`bun tsc` type PackageJson = {
const pkg = await import("../package.json").then((m) => m.default) name: string
const original = JSON.parse(JSON.stringify(pkg)) version: string
for (const [key, value] of Object.entries(pkg.exports)) { exports: Record<string, string>
const file = value.replace("./src/", "./dist/").replace(".ts", "")
// @ts-ignore
pkg.exports[key] = {
import: file + ".js",
types: file + ".d.ts",
}
} }
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
await $`bun pm pack && npm publish *.tgz --tag ${Script.channel} --access public` const packageJson = (value: unknown) => {
await Bun.write("package.json", JSON.stringify(original, null, 2)) if (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string" &&
"version" in value &&
typeof value.version === "string" &&
"exports" in value &&
typeof value.exports === "object" &&
value.exports !== null
) {
return {
name: value.name,
version: value.version,
exports: Object.fromEntries(
Object.entries(value.exports).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
),
}
}
throw new Error("invalid plugin package manifest")
}
const published = (name: string, version: string) =>
Effect.promise(() => $`npm view ${name}@${version} version`.nothrow()).pipe(
Effect.map((result) => result.exitCode === 0),
)
const withPackageJson = (
pkg: PackageJson,
next: { name: string; version: string; exports: Record<string, { import: string; types: string }> },
) =>
Effect.promise(() => Bun.write("package.json", JSON.stringify(next, null, 2))).pipe(
Effect.zipRight(Effect.promise(() => $`bun pm pack && npm publish *.tgz --tag ${Script.channel} --access public`)),
Effect.ensuring(Effect.promise(() => Bun.write("package.json", JSON.stringify(pkg, null, 2)))),
)
const program = Effect.gen(function* () {
yield* Effect.promise(() => $`bun tsc`)
const pkg = packageJson(yield* Effect.promise(() => import("../package.json").then((m) => m.default)))
if (yield* published(pkg.name, pkg.version)) {
console.log(`already published ${pkg.name}@${pkg.version}`)
return
}
const next = {
...pkg,
exports: Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
const file = value.replace("./src/", "./dist/").replace(".ts", "")
return [key, { import: file + ".js", types: file + ".d.ts" }]
}),
),
}
yield* withPackageJson(pkg, next)
})
await Effect.runPromise(program)

View file

@ -2,30 +2,71 @@
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { $ } from "bun" import { $ } from "bun"
import { Effect } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
const dir = fileURLToPath(new URL("..", import.meta.url)) const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir) process.chdir(dir)
const pkg = (await import("../package.json").then((m) => m.default)) as { const packageJson = (value: unknown) => {
exports: Record<string, string | object> if (
typeof value === "object" &&
value !== null &&
"name" in value &&
typeof value.name === "string" &&
"version" in value &&
typeof value.version === "string" &&
"exports" in value &&
typeof value.exports === "object" &&
value.exports !== null
) {
return {
name: value.name,
version: value.version,
exports: value.exports,
}
}
throw new Error("invalid sdk package manifest")
} }
const original = JSON.parse(JSON.stringify(pkg))
function transformExports(exports: Record<string, string | object>) { const published = (name: string, version: string) =>
for (const [key, value] of Object.entries(exports)) { Effect.promise(() => $`npm view ${name}@${version} version`.nothrow()).pipe(
if (typeof value === "object" && value !== null) { Effect.map((result) => result.exitCode === 0),
transformExports(value as Record<string, string | object>) )
} else if (typeof value === "string") {
const withPackageJson = (pkg: ReturnType<typeof packageJson>, next: ReturnType<typeof packageJson>) =>
Effect.promise(() => Bun.write("package.json", JSON.stringify(next, null, 2))).pipe(
Effect.zipRight(Effect.promise(() => $`bun pm pack`)),
Effect.zipRight(Effect.promise(() => $`npm publish *.tgz --tag ${Script.channel} --access public`)),
Effect.ensuring(Effect.promise(() => Bun.write("package.json", JSON.stringify(pkg, null, 2)))),
)
function transformExports(exports: Record<string, unknown>) {
return Object.fromEntries(
Object.entries(exports).map(([key, value]) => {
if (typeof value === "string") {
const file = value.replace("./src/", "./dist/").replace(".ts", "") const file = value.replace("./src/", "./dist/").replace(".ts", "")
exports[key] = { return [key, { import: file + ".js", types: file + ".d.ts" }]
import: file + ".js",
types: file + ".d.ts",
}
}
} }
if (typeof value === "object" && value !== null && !Array.isArray(value)) return [key, transformExports(value)]
return [key, value]
}),
)
} }
transformExports(pkg.exports)
await Bun.write("package.json", JSON.stringify(pkg, null, 2)) const program = Effect.gen(function* () {
await $`bun pm pack` const pkg = packageJson(yield* Effect.promise(() => import("../package.json").then((m) => m.default)))
await $`npm publish *.tgz --tag ${Script.channel} --access public` if (yield* published(pkg.name, pkg.version)) {
await Bun.write("package.json", JSON.stringify(original, null, 2)) console.log(`already published ${pkg.name}@${pkg.version}`)
return
}
const next = {
...pkg,
exports: transformExports(pkg.exports),
}
yield* withPackageJson(pkg, next)
})
await Effect.runPromise(program)

View file

@ -2,57 +2,94 @@
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { $ } from "bun" import { $ } from "bun"
import { Effect } from "effect"
import { fileURLToPath } from "url" import { fileURLToPath } from "url"
console.log("=== publishing ===\n") console.log("=== publishing ===\n")
const tag = `v${Script.version}`
const pkgjsons = await Array.fromAsync( const pkgjsons = await Array.fromAsync(
new Bun.Glob("**/package.json").scan({ new Bun.Glob("**/package.json").scan({
absolute: true, absolute: true,
}), }),
).then((arr) => arr.filter((x) => !x.includes("node_modules") && !x.includes("dist"))) ).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 = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url)) const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url))
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` const readText = (path: string) => Effect.promise(() => Bun.file(path).text())
await import(`../packages/sdk/js/script/build.ts`) const writeText = (path: string, value: string) => Effect.promise(() => Bun.write(path, value))
const shell = <A>(run: () => Promise<A>) => Effect.promise(run)
const log = (message: string) => Effect.sync(() => console.log(message))
if (Script.release) { const hasChanges = shell(() => $`git diff --quiet && git diff --cached --quiet`.nothrow()).pipe(
if (!Script.preview) { Effect.map((result) => result.exitCode !== 0),
await $`git commit -am "release: v${Script.version}"` )
await $`git tag v${Script.version}`
await $`git fetch origin` const releaseTagExists = shell(() => $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).pipe(
await $`git cherry-pick HEAD..origin/dev`.nothrow() Effect.map((result) => result.exitCode === 0),
await $`git push origin HEAD --tags --no-verify --force-with-lease` )
await new Promise((resolve) => setTimeout(resolve, 5_000))
const prepareReleaseFiles = Effect.gen(function* () {
yield* Effect.forEach(pkgjsons, (file) =>
Effect.gen(function* () {
const next = (yield* readText(file)).replaceAll(/"version": "[^"]+"/g, `"version": "${Script.version}"`)
yield* log(`updated: ${file}`)
yield* writeText(file, next)
}),
)
const nextToml = (yield* readText(extensionToml))
.replace(/^version = "[^"]+"/m, `version = "${Script.version}"`)
.replaceAll(/releases\/download\/v[^/]+\//g, `releases/download/v${Script.version}/`)
yield* log(`updated: ${extensionToml}`)
yield* writeText(extensionToml, nextToml)
yield* shell(() => $`bun install`)
yield* shell(() => $`./packages/sdk/js/script/build.ts`)
})
const program = Effect.gen(function* () {
if (Script.release && !Script.preview) {
yield* shell(() => $`git fetch origin --tags`)
yield* shell(() => $`git switch --detach`)
} }
await import(`../packages/desktop/scripts/finalize-latest-json.ts`) yield* prepareReleaseFiles
await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`)
await $`gh release edit v${Script.version} --draft=false --repo ${process.env.GH_REPO}` if (Script.release && !Script.preview) {
} if (yield* releaseTagExists) yield* log(`release tag ${tag} already exists, skipping tag creation`)
else {
yield* shell(() => $`git commit -am "release: ${tag}"`)
yield* shell(() => $`git tag ${tag}`)
yield* shell(() => $`git push origin refs/tags/${tag} --no-verify`)
yield* shell(() => new Promise((resolve) => setTimeout(resolve, 5_000)))
}
}
console.log("\n=== cli ===\n") yield* log("\n=== cli ===\n")
await import(`../packages/opencode/script/publish.ts`) yield* shell(() => import(`../packages/opencode/script/publish.ts`))
yield* log("\n=== sdk ===\n")
yield* shell(() => import(`../packages/sdk/js/script/publish.ts`))
yield* log("\n=== plugin ===\n")
yield* shell(() => import(`../packages/plugin/script/publish.ts`))
console.log("\n=== sdk ===\n") if (Script.release) {
await import(`../packages/sdk/js/script/publish.ts`) yield* shell(() => import(`../packages/desktop/scripts/finalize-latest-json.ts`))
yield* shell(() => import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`))
}
console.log("\n=== plugin ===\n") if (Script.release && !Script.preview) {
await import(`../packages/plugin/script/publish.ts`) yield* shell(() => $`git fetch origin`)
yield* shell(() => $`git checkout -B dev origin/dev`)
yield* prepareReleaseFiles
if (yield* hasChanges) {
yield* shell(() => $`git commit -am "sync release versions for v${Script.version}"`)
yield* shell(() => $`git push origin HEAD:dev --no-verify`)
} else yield* log(`dev already synced for ${tag}`)
}
if (Script.release) yield* shell(() => $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}`)
})
await Effect.runPromise(program)
const dir = fileURLToPath(new URL("..", import.meta.url)) const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir) process.chdir(dir)

View file

@ -2,35 +2,55 @@
import { Script } from "@opencode-ai/script" import { Script } from "@opencode-ai/script"
import { $ } from "bun" import { $ } from "bun"
import { Effect } from "effect"
const output = [`version=${Script.version}`] const tag = `v${Script.version}`
const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim()
const betaPreview = Script.preview && Script.channel === "beta"
if (!Script.preview) { const changelog = Effect.promise(() => $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()))
const sha = process.env.GITHUB_SHA ?? (await $`git rev-parse HEAD`.text()).trim() const readNotes = Effect.promise(() => Bun.file(`${process.cwd()}/UPCOMING_CHANGELOG.md`).text()).pipe(
await $`bun script/changelog.ts --to ${sha}`.cwd(process.cwd()) Effect.catchAll(() => Effect.succeed("No notable changes")),
const file = `${process.cwd()}/UPCOMING_CHANGELOG.md` )
const body = await Bun.file(file) const writeOutput = (lines: ReadonlyArray<string>) =>
.text() process.env.GITHUB_OUTPUT
.catch(() => "No notable changes") ? Effect.promise(() => Bun.write(process.env.GITHUB_OUTPUT!, lines.join("\n")))
const dir = process.env.RUNNER_TEMP ?? "/tmp" : Effect.void
const notesFile = `${dir}/opencode-release-notes.txt`
await Bun.write(notesFile, body) const createRelease = (notesFile?: string) => {
await $`gh release create v${Script.version} -d --title "v${Script.version}" --notes-file ${notesFile}` if (!notesFile && betaPreview) {
const release = await $`gh release view v${Script.version} --json tagName,databaseId`.json() return Effect.promise(
output.push(`release=${release.databaseId}`) () => $`gh release create ${tag} -d --target ${sha} --title ${tag} --repo ${process.env.GH_REPO}`,
output.push(`tag=${release.tagName}`) )
} else if (Script.channel === "beta") { }
await $`gh release create v${Script.version} -d --title "v${Script.version}" --repo ${process.env.GH_REPO}` if (notesFile)
const release = return Effect.promise(() => $`gh release create ${tag} -d --target ${sha} --title ${tag} --notes-file ${notesFile}`)
await $`gh release view v${Script.version} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json() return Effect.void
output.push(`release=${release.databaseId}`)
output.push(`tag=${release.tagName}`)
} }
output.push(`repo=${process.env.GH_REPO}`) const viewRelease = betaPreview
? Effect.promise(() => $`gh release view ${tag} --json tagName,databaseId --repo ${process.env.GH_REPO}`.json())
: Effect.promise(() => $`gh release view ${tag} --json tagName,databaseId`.json())
if (process.env.GITHUB_OUTPUT) { const output = Effect.gen(function* () {
await Bun.write(process.env.GITHUB_OUTPUT, output.join("\n")) const lines = [`version=${Script.version}`]
}
process.exit(0) if (!Script.preview) {
yield* changelog
const body = yield* readNotes
const notesFile = `${process.env.RUNNER_TEMP ?? "/tmp"}/opencode-release-notes.txt`
yield* Effect.promise(() => Bun.write(notesFile, body))
yield* createRelease(notesFile)
const release = yield* viewRelease
lines.push(`release=${release.databaseId}`, `tag=${release.tagName}`)
} else if (Script.channel === "beta") {
yield* createRelease()
const release = yield* viewRelease
lines.push(`release=${release.databaseId}`, `tag=${release.tagName}`)
}
lines.push(`repo=${process.env.GH_REPO}`)
yield* writeOutput(lines)
})
await Effect.runPromise(output)