Compare commits

...

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 365 additions and 174 deletions

View File

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

View File

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

View File

@@ -2,57 +2,94 @@
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { Effect } from "effect"
import { fileURLToPath } from "url"
console.log("=== publishing ===\n")
const tag = `v${Script.version}`
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 = 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`
await import(`../packages/sdk/js/script/build.ts`)
const readText = (path: string) => Effect.promise(() => Bun.file(path).text())
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) {
if (!Script.preview) {
await $`git commit -am "release: v${Script.version}"`
await $`git tag v${Script.version}`
await $`git fetch origin`
await $`git cherry-pick HEAD..origin/dev`.nothrow()
await $`git push origin HEAD --tags --no-verify --force-with-lease`
await new Promise((resolve) => setTimeout(resolve, 5_000))
const hasChanges = shell(() => $`git diff --quiet && git diff --cached --quiet`.nothrow()).pipe(
Effect.map((result) => result.exitCode !== 0),
)
const releaseTagExists = shell(() => $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).pipe(
Effect.map((result) => result.exitCode === 0),
)
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`)
await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`)
yield* prepareReleaseFiles
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")
await import(`../packages/opencode/script/publish.ts`)
yield* log("\n=== cli ===\n")
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")
await import(`../packages/sdk/js/script/publish.ts`)
if (Script.release) {
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")
await import(`../packages/plugin/script/publish.ts`)
if (Script.release && !Script.preview) {
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))
process.chdir(dir)

View File

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