finalize-latest-json.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env bun
  2. import { Buffer } from "node:buffer"
  3. import { $ } from "bun"
  4. const { values } = parseArgs({
  5. args: Bun.argv.slice(2),
  6. options: {
  7. "dry-run": { type: "boolean", default: false },
  8. },
  9. })
  10. const dryRun = values["dry-run"]
  11. import { parseArgs } from "node:util"
  12. const repo = process.env.GH_REPO
  13. if (!repo) throw new Error("GH_REPO is required")
  14. const releaseId = process.env.OPENCODE_RELEASE
  15. if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
  16. const version = process.env.OPENCODE_VERSION
  17. if (!releaseId) throw new Error("OPENCODE_VERSION is required")
  18. const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
  19. if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
  20. const apiHeaders = {
  21. Authorization: `token ${token}`,
  22. Accept: "application/vnd.github+json",
  23. }
  24. const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, {
  25. headers: apiHeaders,
  26. })
  27. if (!releaseRes.ok) {
  28. throw new Error(`Failed to fetch release: ${releaseRes.status} ${releaseRes.statusText}`)
  29. }
  30. type Asset = {
  31. name: string
  32. url: string
  33. }
  34. type Release = {
  35. tag_name?: string
  36. assets?: Asset[]
  37. }
  38. const release = (await releaseRes.json()) as Release
  39. const assets = release.assets ?? []
  40. const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
  41. const latestAsset = assetByName.get("latest.json")
  42. if (!latestAsset) throw new Error("latest.json asset not found")
  43. const latestRes = await fetch(latestAsset.url, {
  44. headers: {
  45. Authorization: `token ${token}`,
  46. Accept: "application/octet-stream",
  47. },
  48. })
  49. if (!latestRes.ok) {
  50. throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`)
  51. }
  52. const latestText = new TextDecoder().decode(await latestRes.arrayBuffer())
  53. const latest = JSON.parse(latestText)
  54. const base = { ...latest }
  55. delete base.platforms
  56. const fetchSignature = async (asset: Asset) => {
  57. const res = await fetch(asset.url, {
  58. headers: {
  59. Authorization: `token ${token}`,
  60. Accept: "application/octet-stream",
  61. },
  62. })
  63. if (!res.ok) {
  64. throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`)
  65. }
  66. return Buffer.from(await res.arrayBuffer()).toString()
  67. }
  68. const entries: Record<string, { url: string; signature: string }> = {}
  69. const add = (key: string, asset: Asset, signature: string) => {
  70. if (entries[key]) return
  71. entries[key] = {
  72. url: `https://github.com/${repo}/releases/download/v${version}/${asset.name}`,
  73. signature,
  74. }
  75. }
  76. const targets = [
  77. { key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" },
  78. { key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" },
  79. { key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" },
  80. { key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" },
  81. { key: "windows-aarch64-nsis", asset: "opencode-desktop-windows-arm64.exe" },
  82. { key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" },
  83. { key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" },
  84. {
  85. key: "darwin-aarch64-app",
  86. asset: "opencode-desktop-darwin-aarch64.app.tar.gz",
  87. },
  88. ]
  89. for (const target of targets) {
  90. const asset = assetByName.get(target.asset)
  91. if (!asset) continue
  92. const sig = assetByName.get(`${target.asset}.sig`)
  93. if (!sig) continue
  94. const signature = await fetchSignature(sig)
  95. add(target.key, asset, signature)
  96. }
  97. const alias = (key: string, source: string) => {
  98. if (entries[key]) return
  99. const entry = entries[source]
  100. if (!entry) return
  101. entries[key] = entry
  102. }
  103. alias("linux-x86_64", "linux-x86_64-deb")
  104. alias("linux-aarch64", "linux-aarch64-deb")
  105. alias("windows-aarch64", "windows-aarch64-nsis")
  106. alias("windows-x86_64", "windows-x86_64-nsis")
  107. alias("darwin-x86_64", "darwin-x86_64-app")
  108. alias("darwin-aarch64", "darwin-aarch64-app")
  109. const platforms = Object.fromEntries(
  110. Object.keys(entries)
  111. .sort()
  112. .map((key) => [key, entries[key]]),
  113. )
  114. const output = {
  115. ...base,
  116. platforms,
  117. }
  118. const dir = process.env.RUNNER_TEMP ?? "/tmp"
  119. const file = `${dir}/latest.json`
  120. await Bun.write(file, JSON.stringify(output, null, 2))
  121. const tag = release.tag_name
  122. if (!tag) throw new Error("Release tag not found")
  123. if (dryRun) {
  124. console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)
  125. process.exit(0)
  126. }
  127. await $`gh release upload ${tag} ${file} --clobber --repo ${repo}`
  128. console.log(`finalized latest.json for ${tag}`)