From 49d63d457cc73a4c3c21ad3ea98527574646febe Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 25 Mar 2026 14:00:43 +0800 Subject: [PATCH] ci: only build electron desktop --- .github/workflows/publish.yml | 147 +---------- .../desktop/scripts/finalize-latest-json.ts | 231 ++++++++++-------- 2 files changed, 129 insertions(+), 249 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b425b32a58..edb1e34990 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -102,150 +102,6 @@ jobs: outputs: version: ${{ needs.version.outputs.version }} - build-tauri: - needs: - - build-cli - - version - continue-on-error: false - strategy: - fail-fast: false - matrix: - settings: - - host: macos-latest - target: x86_64-apple-darwin - - host: macos-latest - target: aarch64-apple-darwin - # github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain - - host: windows-2025 - target: aarch64-pc-windows-msvc - - host: blacksmith-4vcpu-windows-2025 - target: x86_64-pc-windows-msvc - - host: blacksmith-4vcpu-ubuntu-2404 - target: x86_64-unknown-linux-gnu - - host: blacksmith-8vcpu-ubuntu-2404-arm - target: aarch64-unknown-linux-gnu - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v3 - with: - fetch-tags: true - - - uses: apple-actions/import-codesign-certs@v2 - if: ${{ runner.os == 'macOS' }} - with: - keychain: build - p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - - name: Verify Certificate - if: ${{ runner.os == 'macOS' }} - run: | - CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application") - CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') - echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV - echo "Certificate imported." - - - name: Setup Apple API Key - if: ${{ runner.os == 'macOS' }} - run: | - echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - - uses: ./.github/actions/setup-bun - - - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Cache apt packages - if: contains(matrix.settings.host, 'ubuntu') - uses: actions/cache@v4 - with: - path: ~/apt-cache - key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-${{ hashFiles('.github/workflows/publish.yml') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.settings.target }}-apt- - - - name: install dependencies (ubuntu only) - if: contains(matrix.settings.host, 'ubuntu') - run: | - mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache - sudo apt-get update - sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - sudo chmod -R a+rw ~/apt-cache - - - name: install Rust stable - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.settings.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: packages/desktop/src-tauri - shared-key: ${{ matrix.settings.target }} - - - name: Prepare - run: | - cd packages/desktop - bun ./scripts/prepare.ts - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - - - name: Resolve tauri portable SHA - if: contains(matrix.settings.host, 'ubuntu') - run: echo "TAURI_PORTABLE_SHA=$(git ls-remote https://github.com/tauri-apps/tauri.git refs/heads/feat/truly-portable-appimage | cut -f1)" >> "$GITHUB_ENV" - - # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - - name: Install tauri-cli from portable appimage branch - uses: taiki-e/cache-cargo-install-action@v3 - if: contains(matrix.settings.host, 'ubuntu') - with: - tool: tauri-cli - git: https://github.com/tauri-apps/tauri - # branch: feat/truly-portable-appimage - rev: ${{ env.TAURI_PORTABLE_SHA }} - - - name: Show tauri-cli version - if: contains(matrix.settings.host, 'ubuntu') - run: cargo tauri --version - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Build and upload artifacts - uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a - timeout-minutes: 60 - with: - projectPath: packages/desktop - uploadWorkflowArtifacts: true - tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} - args: --target ${{ matrix.settings.target }} --config ${{ (github.ref_name == 'beta' && './src-tauri/tauri.beta.conf.json') || './src-tauri/tauri.prod.conf.json' }} --verbose - updaterJsonPreferNsis: true - releaseId: ${{ needs.version.outputs.release }} - tagName: ${{ needs.version.outputs.tag }} - releaseDraft: true - releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext] - repo: ${{ (github.ref_name == 'beta' && 'opencode-beta') || '' }} - releaseCommitish: ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} - APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8 - build-electron: needs: - build-cli @@ -373,7 +229,6 @@ jobs: needs: - version - build-cli - - build-tauri - build-electron runs-on: blacksmith-4vcpu-ubuntu-2404 steps: @@ -445,3 +300,5 @@ jobs: GH_REPO: ${{ needs.version.outputs.repo }} NPM_CONFIG_PROVENANCE: false LATEST_YML_DIR: /tmp/latest-yml + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} diff --git a/packages/desktop/scripts/finalize-latest-json.ts b/packages/desktop/scripts/finalize-latest-json.ts index a2b95d2c47..33b3974b23 100644 --- a/packages/desktop/scripts/finalize-latest-json.ts +++ b/packages/desktop/scripts/finalize-latest-json.ts @@ -1,7 +1,8 @@ #!/usr/bin/env bun -import { Buffer } from "node:buffer" import { $ } from "bun" +import path from "node:path" +import { parseArgs } from "node:util" const { values } = parseArgs({ args: Bun.argv.slice(2), @@ -12,145 +13,167 @@ const { values } = parseArgs({ const dryRun = values["dry-run"] -import { parseArgs } from "node:util" - const repo = process.env.GH_REPO if (!repo) throw new Error("GH_REPO is required") -const releaseId = process.env.OPENCODE_RELEASE -if (!releaseId) throw new Error("OPENCODE_RELEASE is required") - const version = process.env.OPENCODE_VERSION -if (!releaseId) throw new Error("OPENCODE_VERSION is required") +if (!version) throw new Error("OPENCODE_VERSION is required") + +const dir = process.env.LATEST_YML_DIR +if (!dir) throw new Error("LATEST_YML_DIR is required") +const root = dir const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required") -const apiHeaders = { - Authorization: `token ${token}`, - Accept: "application/vnd.github+json", -} - -const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, { - headers: apiHeaders, -}) - -if (!releaseRes.ok) { - throw new Error(`Failed to fetch release: ${releaseRes.status} ${releaseRes.statusText}`) -} - -type Asset = { - name: string +type Item = { url: string } -type Release = { - tag_name?: string - assets?: Asset[] +type Yml = { + version: string + files: Item[] } -const release = (await releaseRes.json()) as Release -const assets = release.assets ?? [] -const assetByName = new Map(assets.map((asset) => [asset.name, asset])) +function parse(text: string): Yml { + const lines = text.split("\n") + let version = "" + const files: Item[] = [] + let url = "" -const latestAsset = assetByName.get("latest.json") -if (!latestAsset) throw new Error("latest.json asset not found") + const flush = () => { + if (!url) return + files.push({ url }) + url = "" + } -const latestRes = await fetch(latestAsset.url, { - headers: { - Authorization: `token ${token}`, - Accept: "application/octet-stream", - }, -}) + for (const line of lines) { + const trim = line.trim() + if (line.startsWith("version:")) { + version = line.slice("version:".length).trim() + continue + } + if (trim.startsWith("- url:")) { + flush() + url = trim.slice("- url:".length).trim() + continue + } + const indented = line.startsWith(" ") || line.startsWith("\t") + if (!indented) flush() + } + flush() -if (!latestRes.ok) { - throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`) + return { version, files } } -const latestText = new TextDecoder().decode(await latestRes.arrayBuffer()) -const latest = JSON.parse(latestText) -const base = { ...latest } -delete base.platforms +async function read(sub: string, file: string) { + const item = Bun.file(path.join(root, sub, file)) + if (!(await item.exists())) return undefined + return parse(await item.text()) +} -const fetchSignature = async (asset: Asset) => { - const res = await fetch(asset.url, { - headers: { - Authorization: `token ${token}`, - Accept: "application/octet-stream", - }, +function pick(list: Item[], exts: string[]) { + for (const ext of exts) { + const found = list.find((item) => item.url.split("?")[0]?.toLowerCase().endsWith(ext)) + if (found) return found.url + } +} + +function link(raw: string) { + if (raw.startsWith("https://") || raw.startsWith("http://")) return raw + return `https://github.com/${repo}/releases/download/v${version}/${raw}` +} + +function lkey(arch: string, raw: string | undefined) { + if (!raw) return + const low = raw.split("?")[0]?.toLowerCase() ?? "" + if (low.endsWith(".deb")) return `linux-${arch}-deb` + if (low.endsWith(".rpm")) return `linux-${arch}-rpm` + if (low.endsWith(".appimage")) return `linux-${arch}-appimage` +} + +async function sign(url: string, key: string) { + const res = await fetch(url, { + headers: { Authorization: `token ${token}` }, }) + if (!res.ok) throw new Error(`Failed to fetch file: ${res.status} ${res.statusText}`) - if (!res.ok) { - throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`) - } - - return Buffer.from(await res.arrayBuffer()).toString() + const name = decodeURIComponent(new URL(url).pathname.split("/").pop() ?? key) + const tmp = process.env.RUNNER_TEMP ?? "/tmp" + const file = path.join(tmp, name) + await Bun.write(file, await res.arrayBuffer()) + const out = await $`bunx @tauri-apps/cli signer sign ${file}`.text() + return out.trim() } -const entries: Record = {} -const add = (key: string, asset: Asset, signature: string) => { - if (entries[key]) return - entries[key] = { - url: `https://github.com/${repo}/releases/download/v${version}/${asset.name}`, - signature, - } +const add = async (data: Record, key: string, raw: string | undefined) => { + if (!raw) return + if (data[key]) return + const url = link(raw) + data[key] = { url, signature: await sign(url, key) } } -const targets = [ - { key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" }, - { key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" }, - { key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" }, - { key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" }, - { key: "windows-aarch64-nsis", asset: "opencode-desktop-windows-arm64.exe" }, - { key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" }, - { key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" }, - { - key: "darwin-aarch64-app", - asset: "opencode-desktop-darwin-aarch64.app.tar.gz", - }, -] - -for (const target of targets) { - const asset = assetByName.get(target.asset) - if (!asset) continue - - const sig = assetByName.get(`${target.asset}.sig`) - if (!sig) continue - - const signature = await fetchSignature(sig) - add(target.key, asset, signature) +const alias = (data: Record, key: string, src: string) => { + if (data[key]) return + if (!data[src]) return + data[key] = data[src] } -const alias = (key: string, source: string) => { - if (entries[key]) return - const entry = entries[source] - if (!entry) return - entries[key] = entry -} +const winx = await read("latest-yml-x86_64-pc-windows-msvc", "latest.yml") +const wina = await read("latest-yml-aarch64-pc-windows-msvc", "latest.yml") +const macx = await read("latest-yml-x86_64-apple-darwin", "latest-mac.yml") +const maca = await read("latest-yml-aarch64-apple-darwin", "latest-mac.yml") +const linx = await read("latest-yml-x86_64-unknown-linux-gnu", "latest-linux.yml") +const lina = await read("latest-yml-aarch64-unknown-linux-gnu", "latest-linux-arm64.yml") -alias("linux-x86_64", "linux-x86_64-deb") -alias("linux-aarch64", "linux-aarch64-deb") -alias("windows-aarch64", "windows-aarch64-nsis") -alias("windows-x86_64", "windows-x86_64-nsis") -alias("darwin-x86_64", "darwin-x86_64-app") -alias("darwin-aarch64", "darwin-aarch64-app") +const yver = winx?.version ?? wina?.version ?? macx?.version ?? maca?.version ?? linx?.version ?? lina?.version +if (yver && yver !== version) throw new Error(`latest.yml version mismatch: expected ${version}, got ${yver}`) + +const out: Record = {} + +const winxexe = pick(winx?.files ?? [], [".exe"]) +const winaexe = pick(wina?.files ?? [], [".exe"]) +const macxzip = pick(macx?.files ?? [], [".zip", ".dmg"]) +const macazip = pick(maca?.files ?? [], [".zip", ".dmg"]) +const linxapp = pick(linx?.files ?? [], [".appimage", ".deb", ".rpm"]) +const linaapp = pick(lina?.files ?? [], [".appimage", ".deb", ".rpm"]) +const linxkey = lkey("x86_64", linxapp) +const linakey = lkey("aarch64", linaapp) + +await add(out, "windows-x86_64-nsis", winxexe) +await add(out, "windows-aarch64-nsis", winaexe) +await add(out, "darwin-x86_64-app", macxzip) +await add(out, "darwin-aarch64-app", macazip) +if (linxkey) await add(out, linxkey, linxapp) +if (linakey) await add(out, linakey, linaapp) + +alias(out, "windows-x86_64", "windows-x86_64-nsis") +alias(out, "windows-aarch64", "windows-aarch64-nsis") +alias(out, "darwin-x86_64", "darwin-x86_64-app") +alias(out, "darwin-aarch64", "darwin-aarch64-app") +if (linxkey) alias(out, "linux-x86_64", linxkey) +if (linakey) alias(out, "linux-aarch64", linakey) const platforms = Object.fromEntries( - Object.keys(entries) + Object.keys(out) .sort() - .map((key) => [key, entries[key]]), + .map((key) => [key, out[key]]), ) -const output = { - ...base, + +if (!Object.keys(platforms).length) throw new Error("No updater files found in latest.yml artifacts") + +const data = { + version, + notes: "", + pub_date: new Date().toISOString(), platforms, } -const dir = process.env.RUNNER_TEMP ?? "/tmp" -const file = `${dir}/latest.json` -await Bun.write(file, JSON.stringify(output, null, 2)) +const tmp = process.env.RUNNER_TEMP ?? "/tmp" +const file = path.join(tmp, "latest.json") +await Bun.write(file, JSON.stringify(data, null, 2)) -const tag = release.tag_name -if (!tag) throw new Error("Release tag not found") +const tag = `v${version}` if (dryRun) { console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)