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.
This commit is contained in:
+66
-65
@@ -2,93 +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")))
|
||||
|
||||
const extensionToml = fileURLToPath(new URL("../packages/extensions/zed/extension.toml", import.meta.url))
|
||||
|
||||
async function hasChanges() {
|
||||
return (await $`git diff --quiet && git diff --cached --quiet`.nothrow()).exitCode !== 0
|
||||
}
|
||||
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))
|
||||
|
||||
async function releaseTagExists() {
|
||||
return (await $`git rev-parse -q --verify refs/tags/${tag}`.nothrow()).exitCode === 0
|
||||
}
|
||||
const hasChanges = shell(() => $`git diff --quiet && git diff --cached --quiet`.nothrow()).pipe(
|
||||
Effect.map((result) => result.exitCode !== 0),
|
||||
)
|
||||
|
||||
async function prepareReleaseFiles() {
|
||||
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 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`)
|
||||
}
|
||||
|
||||
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)
|
||||
yield* prepareReleaseFiles
|
||||
|
||||
await $`bun install`
|
||||
await $`./packages/sdk/js/script/build.ts`
|
||||
}
|
||||
|
||||
if (Script.release && !Script.preview) {
|
||||
await $`git fetch origin --tags`
|
||||
await $`git switch --detach`
|
||||
}
|
||||
|
||||
await prepareReleaseFiles()
|
||||
|
||||
if (Script.release && !Script.preview) {
|
||||
if (await releaseTagExists()) {
|
||||
console.log(`release tag ${tag} already exists, skipping tag creation`)
|
||||
} else {
|
||||
await $`git commit -am "release: ${tag}"`
|
||||
await $`git tag ${tag}`
|
||||
await $`git push origin refs/tags/${tag} --no-verify`
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
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`)
|
||||
|
||||
console.log("\n=== plugin ===\n")
|
||||
await import(`../packages/plugin/script/publish.ts`)
|
||||
|
||||
if (Script.release) {
|
||||
await import(`../packages/desktop/scripts/finalize-latest-json.ts`)
|
||||
await import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`)
|
||||
}
|
||||
|
||||
if (Script.release && !Script.preview) {
|
||||
await $`git fetch origin`
|
||||
await $`git checkout -B dev origin/dev`
|
||||
await prepareReleaseFiles()
|
||||
if (await hasChanges()) {
|
||||
await $`git commit -am "sync release versions for v${Script.version}"`
|
||||
await $`git push origin HEAD:dev --no-verify`
|
||||
} else {
|
||||
console.log(`dev already synced for ${tag}`)
|
||||
if (Script.release) {
|
||||
yield* shell(() => import(`../packages/desktop/scripts/finalize-latest-json.ts`))
|
||||
yield* shell(() => import(`../packages/desktop-electron/scripts/finalize-latest-yml.ts`))
|
||||
}
|
||||
}
|
||||
|
||||
if (Script.release) {
|
||||
await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}`
|
||||
}
|
||||
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)
|
||||
|
||||
+45
-25
@@ -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) {
|
||||
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 --target ${sha} --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 --target ${sha} --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)
|
||||
|
||||
Reference in New Issue
Block a user