finalize-latest-json.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 (!version) 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) {
  43. console.log("latest.json not found, skipping tauri finalization")
  44. process.exit(0)
  45. }
  46. const latestRes = await fetch(latestAsset.url, {
  47. headers: {
  48. Authorization: `token ${token}`,
  49. Accept: "application/octet-stream",
  50. },
  51. })
  52. if (!latestRes.ok) {
  53. throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`)
  54. }
  55. const latestText = new TextDecoder().decode(await latestRes.arrayBuffer())
  56. const latest = JSON.parse(latestText)
  57. const base = { ...latest }
  58. delete base.platforms
  59. const fetchSignature = async (asset: Asset) => {
  60. const res = await fetch(asset.url, {
  61. headers: {
  62. Authorization: `token ${token}`,
  63. Accept: "application/octet-stream",
  64. },
  65. })
  66. if (!res.ok) {
  67. throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`)
  68. }
  69. return Buffer.from(await res.arrayBuffer()).toString()
  70. }
  71. const entries: Record<string, { url: string; signature: string }> = {}
  72. const add = (key: string, asset: Asset, signature: string) => {
  73. if (entries[key]) return
  74. entries[key] = {
  75. url: `https://github.com/${repo}/releases/download/v${version}/${asset.name}`,
  76. signature,
  77. }
  78. }
  79. const targets = [
  80. { key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" },
  81. { key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" },
  82. { key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" },
  83. { key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" },
  84. { key: "windows-aarch64-nsis", asset: "opencode-desktop-windows-arm64.exe" },
  85. { key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" },
  86. { key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" },
  87. {
  88. key: "darwin-aarch64-app",
  89. asset: "opencode-desktop-darwin-aarch64.app.tar.gz",
  90. },
  91. ]
  92. for (const target of targets) {
  93. const asset = assetByName.get(target.asset)
  94. if (!asset) continue
  95. const sig = assetByName.get(`${target.asset}.sig`)
  96. if (!sig) continue
  97. const signature = await fetchSignature(sig)
  98. add(target.key, asset, signature)
  99. }
  100. const alias = (key: string, source: string) => {
  101. if (entries[key]) return
  102. const entry = entries[source]
  103. if (!entry) return
  104. entries[key] = entry
  105. }
  106. alias("linux-x86_64", "linux-x86_64-deb")
  107. alias("linux-aarch64", "linux-aarch64-deb")
  108. alias("windows-aarch64", "windows-aarch64-nsis")
  109. alias("windows-x86_64", "windows-x86_64-nsis")
  110. alias("darwin-x86_64", "darwin-x86_64-app")
  111. alias("darwin-aarch64", "darwin-aarch64-app")
  112. const platforms = Object.fromEntries(
  113. Object.keys(entries)
  114. .sort()
  115. .map((key) => [key, entries[key]]),
  116. )
  117. const output = {
  118. ...base,
  119. platforms,
  120. }
  121. const dir = process.env.RUNNER_TEMP ?? "/tmp"
  122. const file = `${dir}/latest.json`
  123. await Bun.write(file, JSON.stringify(output, null, 2))
  124. const tag = release.tag_name
  125. if (!tag) throw new Error("Release tag not found")
  126. if (dryRun) {
  127. console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)
  128. process.exit(0)
  129. }
  130. await $`gh release upload ${tag} ${file} --clobber --repo ${repo}`
  131. console.log(`finalized latest.json for ${tag}`)