2
0

finalize-latest-json.ts 4.1 KB

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