feat(updates): add release control service
This commit is contained in:
parent
c2351e308f
commit
c944048bcf
11 changed files with 755 additions and 0 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import { Script } from "@opencode-ai/script"
|
||||
import { $ } from "bun"
|
||||
import { fileURLToPath } from "url"
|
||||
import { UpdateArtifact } from "./update-artifact"
|
||||
|
||||
console.log("=== publishing ===\n")
|
||||
|
||||
|
|
@ -82,4 +83,13 @@ if (Script.release && !Script.preview) {
|
|||
|
||||
if (Script.release) {
|
||||
await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}`
|
||||
const repo = process.env.GH_REPO
|
||||
if (!repo) throw new Error("GH_REPO is required")
|
||||
await UpdateArtifact.publish({
|
||||
channel: Script.channel,
|
||||
name: "desktop",
|
||||
distribution: "github",
|
||||
version: Script.version,
|
||||
metadata: await UpdateArtifact.desktopMetadata(Script.version, repo),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
104
script/update-artifact.ts
Normal file
104
script/update-artifact.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
type Artifact = {
|
||||
channel: string
|
||||
name: string
|
||||
distribution: string
|
||||
version: string
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
type DesktopFile = {
|
||||
url: string
|
||||
sha512: string
|
||||
size: number
|
||||
blockMapSize?: number
|
||||
}
|
||||
|
||||
export namespace UpdateArtifact {
|
||||
export async function publish(artifact: Artifact) {
|
||||
if (process.env.GITHUB_ACTIONS !== "true") {
|
||||
console.log("skipped update artifact publication outside GitHub Actions")
|
||||
return
|
||||
}
|
||||
const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL
|
||||
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
||||
if (!requestURL || !requestToken) throw new Error("GitHub Actions OIDC is unavailable")
|
||||
|
||||
const url = new URL(requestURL)
|
||||
url.searchParams.set("audience", "https://update.opencode.ai")
|
||||
const tokenResponse = await fetch(url, { headers: { Authorization: `Bearer ${requestToken}` } })
|
||||
if (!tokenResponse.ok) throw new Error(`Failed to request GitHub OIDC token: ${tokenResponse.status}`)
|
||||
const token: unknown = await tokenResponse.json()
|
||||
if (!isRecord(token) || typeof token.value !== "string") throw new Error("GitHub OIDC response did not include a token")
|
||||
|
||||
const response = await fetch("https://update.opencode.ai/api/publish", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.value}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(artifact),
|
||||
})
|
||||
if (response.ok) return
|
||||
throw new Error(`Failed to publish update artifact: ${response.status} ${await response.text()}`)
|
||||
}
|
||||
|
||||
export async function desktopMetadata(version: string, repo: string) {
|
||||
const directory = process.env.RUNNER_TEMP ?? "/tmp"
|
||||
const entries = await Promise.all(
|
||||
[
|
||||
["desktop.yml", "latest.yml"],
|
||||
["desktop-mac.yml", "latest-mac.yml"],
|
||||
["desktop-linux.yml", "latest-linux.yml"],
|
||||
["desktop-linux-arm64.yml", "latest-linux-arm64.yml"],
|
||||
].map(async ([name, source]) => {
|
||||
const file = Bun.file(`${directory}/${source}`)
|
||||
if (!(await file.exists())) return
|
||||
return [name, parseDesktop(await file.text(), version, repo)] as const
|
||||
}),
|
||||
)
|
||||
const manifests = Object.fromEntries(entries.filter((entry) => entry !== undefined))
|
||||
if (!Object.keys(manifests).length) throw new Error("No desktop update metadata found")
|
||||
return { manifests }
|
||||
}
|
||||
}
|
||||
|
||||
function parseDesktop(content: string, version: string, repo: string) {
|
||||
const lines = content.split("\n")
|
||||
const found = lines.find((line) => line.startsWith("version:"))?.slice("version:".length).trim()
|
||||
if (found !== version) throw new Error(`Desktop metadata version mismatch: expected ${version}, got ${found}`)
|
||||
const releaseDate = lines
|
||||
.find((line) => line.startsWith("releaseDate:"))
|
||||
?.slice("releaseDate:".length)
|
||||
.trim()
|
||||
.replace(/^['"]|['"]$/g, "")
|
||||
if (!releaseDate) throw new Error("Desktop metadata did not include a release date")
|
||||
|
||||
const files: DesktopFile[] = []
|
||||
lines.forEach((line) => {
|
||||
const value = line.trim()
|
||||
if (value.startsWith("- url:")) {
|
||||
const name = value.slice("- url:".length).trim()
|
||||
files.push({
|
||||
url: name.startsWith("http")
|
||||
? name
|
||||
: `https://github.com/${repo}/releases/download/v${version}/${encodeURIComponent(name)}`,
|
||||
sha512: "",
|
||||
size: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
const current = files.at(-1)
|
||||
if (!current) return
|
||||
if (value.startsWith("sha512:")) current.sha512 = value.slice("sha512:".length).trim()
|
||||
if (value.startsWith("size:")) current.size = Number(value.slice("size:".length).trim())
|
||||
if (value.startsWith("blockMapSize:")) current.blockMapSize = Number(value.slice("blockMapSize:".length).trim())
|
||||
})
|
||||
if (!files.length || files.some((file) => !file.sha512 || !file.size)) {
|
||||
throw new Error("Desktop metadata contained an incomplete file")
|
||||
}
|
||||
return { files, releaseDate }
|
||||
}
|
||||
|
||||
function isRecord(input: unknown): input is Record<string, unknown> {
|
||||
return typeof input === "object" && input !== null && !Array.isArray(input)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue