#!/usr/bin/env bun import { Buffer } from "node:buffer" import { $ } from "bun" const { values } = parseArgs({ args: Bun.argv.slice(2), options: { "dry-run": { type: "boolean", default: false }, }, }) const dryRun = values["dry-run"] import { parseArgs } from "node:util" const repo = process.env.GH_REPO if (!repo) throw new Error("GH_REPO is required") const releaseId = process.env.OPENCODE_RELEASE if (!releaseId) throw new Error("OPENCODE_RELEASE is required") const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required") const apiHeaders = { Authorization: `token ${token}`, Accept: "application/vnd.github+json", } const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, { headers: apiHeaders, }) if (!releaseRes.ok) { throw new Error(`Failed to fetch release: ${releaseRes.status} ${releaseRes.statusText}`) } type Asset = { name: string url: string browser_download_url: string } type Release = { tag_name?: string assets?: Asset[] } const release = (await releaseRes.json()) as Release const assets = release.assets ?? [] const assetByName = new Map(assets.map((asset) => [asset.name, asset])) const latestAsset = assetByName.get("latest.json") if (!latestAsset) throw new Error("latest.json asset not found") const latestRes = await fetch(latestAsset.url, { headers: { Authorization: `token ${token}`, Accept: "application/octet-stream", }, }) if (!latestRes.ok) { throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`) } const latestText = new TextDecoder().decode(await latestRes.arrayBuffer()) const latest = JSON.parse(latestText) const base = { ...latest } delete base.platforms const fetchSignature = async (asset: Asset) => { const res = await fetch(asset.url, { headers: { Authorization: `token ${token}`, Accept: "application/octet-stream", }, }) if (!res.ok) { throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`) } return Buffer.from(await res.arrayBuffer()).toString() } const entries: Record = {} const add = (key: string, asset: Asset, signature: string) => { if (entries[key]) return entries[key] = { url: asset.browser_download_url, signature, } } const targets = [ { key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" }, { key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" }, { key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" }, { key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" }, { key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" }, { key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" }, { key: "darwin-aarch64-app", asset: "opencode-desktop-darwin-aarch64.app.tar.gz", }, ] for (const target of targets) { const asset = assetByName.get(target.asset) if (!asset) continue const sig = assetByName.get(`${target.asset}.sig`) if (!sig) continue const signature = await fetchSignature(sig) add(target.key, asset, signature) } const alias = (key: string, source: string) => { if (entries[key]) return const entry = entries[source] if (!entry) return entries[key] = entry } alias("linux-x86_64", "linux-x86_64-deb") alias("linux-aarch64", "linux-aarch64-deb") alias("windows-x86_64", "windows-x86_64-nsis") alias("darwin-x86_64", "darwin-x86_64-app") alias("darwin-aarch64", "darwin-aarch64-app") const platforms = Object.fromEntries( Object.keys(entries) .sort() .map((key) => [key, entries[key]]), ) const output = { ...base, platforms, } const dir = process.env.RUNNER_TEMP ?? "/tmp" const file = `${dir}/latest.json` await Bun.write(file, JSON.stringify(output, null, 2)) const tag = release.tag_name if (!tag) throw new Error("Release tag not found") 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}`)