Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220#!/usr/bin/env bun
import { $ } from "bun"import path from "node:path"import { parseArgs } from "node:util"
const { values } = parseArgs({ args: Bun.argv.slice(2), options: { "dry-run": { type: "boolean", default: false }, },})
const dryRun = values["dry-run"]
const repo = process.env.GH_REPOif (!repo) throw new Error("GH_REPO is required")
const releaseId = process.env.OPENCODE_RELEASEif (!releaseId) throw new Error("OPENCODE_RELEASE is required")
const version = process.env.OPENCODE_VERSIONif (!version) throw new Error("OPENCODE_VERSION is required")
const dir = process.env.LATEST_YML_DIRif (!dir) throw new Error("LATEST_YML_DIR is required")const root = dir
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKENif (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
const rel = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, { headers: { Authorization: `token ${token}`, Accept: "application/vnd.github+json", },})
if (!rel.ok) { throw new Error(`Failed to fetch release: ${rel.status} ${rel.statusText}`)}
type Asset = { name: string url: string}
type Release = { assets?: Asset[]}
const assets = ((await rel.json()) as Release).assets ?? []const amap = new Map(assets.map((item) => [item.name, item]))
type Item = { url: string}
type Yml = { version: string files: Item[]}
function parse(text: string): Yml { const lines = text.split("\n") let version = "" const files: Item[] = [] let url = ""
const flush = () => { if (!url) return files.push({ url }) url = "" }
for (const line of lines) { const trim = line.trim() if (line.startsWith("version:")) { version = line.slice("version:".length).trim() continue } if (trim.startsWith("- url:")) { flush() url = trim.slice("- url:".length).trim() continue } const indented = line.startsWith(" ") || line.startsWith("\t") if (!indented) flush() } flush()
return { version, files }}
async function read(sub: string, file: string) { const item = Bun.file(path.join(root, sub, file)) if (!(await item.exists())) return undefined return parse(await item.text())}
function pick(list: Item[], exts: string[]) { for (const ext of exts) { const found = list.find((item) => item.url.split("?")[0]?.toLowerCase().endsWith(ext)) if (found) return found.url }}
function link(raw: string) { if (raw.startsWith("https://") || raw.startsWith("http://")) return raw return `https://github.com/${repo}/releases/download/v${version}/${raw}`}
async function sign(url: string, key: string) { const name = decodeURIComponent(new URL(url).pathname.split("/").pop() ?? key) const asset = amap.get(name) const res = await fetch(asset?.url ?? url, { headers: { Authorization: `token ${token}`, ...(asset ? { Accept: "application/octet-stream" } : {}), }, }) if (!res.ok) { throw new Error(`Failed to fetch file ${name}: ${res.status} ${res.statusText} (${asset?.url ?? url})`) }
const tmp = process.env.RUNNER_TEMP ?? "/tmp" const file = path.join(tmp, name) await Bun.write(file, await res.arrayBuffer()) await $`bunx @tauri-apps/cli signer sign ${file}` const sigFile = Bun.file(`${file}.sig`) if (!(await sigFile.exists())) throw new Error(`Signature file not found for ${name}`) return (await sigFile.text()).trim()}
const add = async (data: Record<string, { url: string; signature: string }>, key: string, raw: string | undefined) => { if (!raw) return if (data[key]) return const url = link(raw) data[key] = { url, signature: await sign(url, key) }}
const alias = (data: Record<string, { url: string; signature: string }>, key: string, src: string) => { if (data[key]) return if (!data[src]) return data[key] = data[src]}
const winx = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml")const wina = await read("latest-yml-aarch64-pc-windows-msvc", "latest.yml")const macx = await read("latest-yml-x86_64-apple-darwin", "latest-mac.yml")const maca = await read("latest-yml-aarch64-apple-darwin", "latest-mac.yml")const linx = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml")const lina = await read("latest-yml-aarch64-unknown-linux-gnu", "latest-linux-arm64.yml")
const yver = winx?.version ?? wina?.version ?? macx?.version ?? maca?.version ?? linx?.version ?? lina?.versionif (yver && yver !== version) throw new Error(`latest.yml version mismatch: expected ${version}, got ${yver}`)
const out: Record<string, { url: string; signature: string }> = {}
const winxexe = pick(winx?.files ?? [], [".exe"])const winaexe = pick(wina?.files ?? [], [".exe"])
const macxTarGz = "opencode-desktop-mac-x64.app.tar.gz"const macaTarGz = "opencode-desktop-mac-arm64.app.tar.gz"
const linxDeb = pick(linx?.files ?? [], [".deb"])const linxRpm = pick(linx?.files ?? [], [".rpm"])const linxAppImage = pick(linx?.files ?? [], [".appimage"])const linaDeb = pick(lina?.files ?? [], [".deb"])const linaRpm = pick(lina?.files ?? [], [".rpm"])const linaAppImage = pick(lina?.files ?? [], [".appimage"])
await add(out, "windows-x86_64-nsis", winxexe)await add(out, "windows-aarch64-nsis", winaexe)await add(out, "darwin-x86_64-app", macxTarGz)await add(out, "darwin-aarch64-app", macaTarGz)
await add(out, "linux-x86_64-deb", linxDeb)await add(out, "linux-x86_64-rpm", linxRpm)await add(out, "linux-x86_64-appimage", linxAppImage)await add(out, "linux-aarch64-deb", linaDeb)await add(out, "linux-aarch64-rpm", linaRpm)await add(out, "linux-aarch64-appimage", linaAppImage)
alias(out, "windows-x86_64", "windows-x86_64-nsis")alias(out, "windows-aarch64", "windows-aarch64-nsis")alias(out, "darwin-x86_64", "darwin-x86_64-app")alias(out, "darwin-aarch64", "darwin-aarch64-app")alias(out, "linux-x86_64", "linux-x86_64-deb")alias(out, "linux-aarch64", "linux-aarch64-deb")
const platforms = Object.fromEntries( Object.keys(out) .sort() .map((key) => [key, out[key]]),)
if (!Object.keys(platforms).length) throw new Error("No updater files found in latest.yml artifacts")
const data = { version, notes: "", pub_date: new Date().toISOString(), platforms,}
const tmp = process.env.RUNNER_TEMP ?? "/tmp"const file = path.join(tmp, "latest.json")await Bun.write(file, JSON.stringify(data, null, 2))
const tag = `v${version}`
if (dryRun) { console.log(`dry-run: wrote latest.json for ${tag} to ${file}`) process.exit(0)}await $`gh release upload ${tag} ${file} --clobber --repo ${repo}`
console.log(`finalized latest.json for ${tag}`)