type Artifact = { channel: string name: string distribution: string version: string metadata: Record } 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 { return typeof input === "object" && input !== null && !Array.isArray(input) }