Skip to content

Commit 542503b

Browse files
committed
ci: publish Electron desktop releases
1 parent 46c21f7 commit 542503b

5 files changed

Lines changed: 525 additions & 3 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 225 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,14 +403,190 @@ jobs:
403403
path: opencode-src/packages/opencode/dist/opencode-darwin-arm64/bin/opencode
404404
retention-days: 5
405405

406+
# Build the current upstream Electron desktop application. This intentionally
407+
# replaces the obsolete Tauri pipeline; every matrix entry is required.
408+
build-desktop:
409+
needs: fetch-version
410+
strategy:
411+
fail-fast: false
412+
matrix:
413+
include:
414+
- name: macos-arm64
415+
os: macos-15
416+
target: aarch64-apple-darwin
417+
platform_flag: --mac --arm64
418+
bun_install_flags: --os=darwin --cpu=arm64
419+
artifact: opencode-desktop-darwin-arm64
420+
required_files: latest-mac.yml *.dmg *.zip
421+
- name: macos-x64
422+
os: macos-15-intel
423+
target: x86_64-apple-darwin
424+
platform_flag: --mac --x64
425+
bun_install_flags: --os=darwin --cpu=x64
426+
artifact: opencode-desktop-darwin-x64
427+
required_files: latest-mac.yml *.dmg *.zip
428+
- name: windows-x64
429+
os: windows-latest
430+
target: x86_64-pc-windows-msvc
431+
platform_flag: --win --x64
432+
bun_install_flags: --os=win32 --cpu=x64
433+
artifact: opencode-desktop-windows-x64
434+
required_files: latest.yml *.exe *.blockmap
435+
- name: linux-x64
436+
os: ubuntu-latest
437+
target: x86_64-unknown-linux-gnu
438+
platform_flag: --linux --x64
439+
bun_install_flags: --os=linux --cpu=x64
440+
artifact: opencode-desktop-linux-x64
441+
required_files: latest-linux.yml *.AppImage *.deb
442+
- name: linux-arm64
443+
os: ubuntu-24.04-arm
444+
target: aarch64-unknown-linux-gnu
445+
platform_flag: --linux --arm64
446+
bun_install_flags: --os=linux --cpu=arm64
447+
artifact: opencode-desktop-linux-arm64
448+
required_files: latest-linux-arm64.yml *.AppImage *.deb
449+
name: desktop (${{ matrix.name }})
450+
runs-on: ${{ matrix.os }}
451+
timeout-minutes: 60
452+
steps:
453+
- name: Checkout this repo
454+
uses: actions/checkout@v4
455+
456+
- name: Clone OpenCode
457+
run: |
458+
git clone --depth 1 --branch v${{ needs.fetch-version.outputs.version }} https://github.com/anomalyco/opencode.git opencode-src
459+
shell: bash
460+
461+
- name: Setup Node
462+
uses: actions/setup-node@v4
463+
with:
464+
node-version: "24"
465+
466+
- name: Setup Bun
467+
uses: oven-sh/setup-bun@v2
468+
with:
469+
bun-version: ${{ env.BUN_VERSION }}
470+
471+
- name: Strip telemetry and prepare unsigned Electron packaging
472+
run: |
473+
bun script/strip-opencode-telemetry.ts opencode-src
474+
bun script/write-opencode-electron-release-config.ts opencode-src "${{ github.repository }}"
475+
shell: bash
476+
477+
- name: Install Electron packaging dependencies (Ubuntu)
478+
if: runner.os == 'Linux'
479+
run: |
480+
sudo apt-get update
481+
sudo apt-get install -y --no-install-recommends rpm
482+
483+
- name: Install dependencies
484+
run: |
485+
cd opencode-src
486+
if [ "${{ runner.os }}" = "Windows" ]; then
487+
bun install --linker hoisted ${{ matrix.bun_install_flags }}
488+
else
489+
bun install ${{ matrix.bun_install_flags }}
490+
fi
491+
shell: bash
492+
493+
- name: Type-check patched Electron desktop
494+
run: bun run --cwd packages/desktop typecheck
495+
working-directory: opencode-src
496+
shell: bash
497+
498+
- name: Prepare Electron desktop
499+
run: bun ./scripts/prepare.ts
500+
working-directory: opencode-src/packages/desktop
501+
shell: bash
502+
env:
503+
OPENCODE_VERSION: ${{ needs.fetch-version.outputs.version }}
504+
OPENCODE_CHANNEL: prod
505+
RUST_TARGET: ${{ matrix.target }}
506+
507+
- name: Build Electron desktop
508+
run: bun run build
509+
working-directory: opencode-src/packages/desktop
510+
shell: bash
511+
env:
512+
NODE_OPTIONS: --max-old-space-size=4096
513+
OPENCODE_CHANNEL: prod
514+
515+
- name: Package unsigned Electron desktop
516+
run: npx electron-builder ${{ matrix.platform_flag }} --publish never --config electron-builder.evil.config.ts
517+
working-directory: opencode-src/packages/desktop
518+
shell: bash
519+
timeout-minutes: 45
520+
env:
521+
OPENCODE_CHANNEL: prod
522+
CSC_IDENTITY_AUTO_DISCOVERY: "false"
523+
524+
- name: Archive macOS application bundle
525+
if: runner.os == 'macOS'
526+
run: |
527+
if [ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then
528+
app_dir="mac-arm64"
529+
archive="opencode-desktop-mac-arm64.app.tar.gz"
530+
else
531+
app_dir="mac"
532+
archive="opencode-desktop-mac-x64.app.tar.gz"
533+
fi
534+
app_path=$(find "$app_dir" -maxdepth 1 -name "*.app" -type d | head -1)
535+
if [ -z "$app_path" ]; then
536+
echo "::error::No macOS application bundle was produced"
537+
exit 1
538+
fi
539+
tar -czf "$archive" -C "$(dirname "$app_path")" "$(basename "$app_path")"
540+
working-directory: opencode-src/packages/desktop/dist
541+
shell: bash
542+
543+
- name: Verify Electron packages and updater metadata
544+
run: |
545+
cd opencode-src/packages/desktop/dist
546+
shopt -s nullglob
547+
for pattern in ${{ matrix.required_files }}; do
548+
matches=($pattern)
549+
if [ "${#matches[@]}" -eq 0 ]; then
550+
echo "::error::Missing required Electron output: $pattern"
551+
exit 1
552+
fi
553+
done
554+
find . -maxdepth 1 -type f -print | sort
555+
shell: bash
556+
557+
- name: Upload Electron desktop artifacts
558+
uses: actions/upload-artifact@v4
559+
with:
560+
name: ${{ matrix.artifact }}
561+
path: |
562+
opencode-src/packages/desktop/dist/*.dmg
563+
opencode-src/packages/desktop/dist/*.zip
564+
opencode-src/packages/desktop/dist/*.blockmap
565+
opencode-src/packages/desktop/dist/*.exe
566+
opencode-src/packages/desktop/dist/*.AppImage
567+
opencode-src/packages/desktop/dist/*.deb
568+
opencode-src/packages/desktop/dist/*.rpm
569+
opencode-src/packages/desktop/dist/*.app.tar.gz
570+
opencode-src/packages/desktop/dist/latest*.yml
571+
retention-days: 5
572+
if-no-files-found: error
573+
406574
release:
407-
needs: [fetch-version, build-linux, build-macos, build-windows]
408-
if: always() && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' && needs.build-windows.result == 'success'
575+
needs: [fetch-version, build-linux, build-macos, build-windows, build-desktop]
576+
if: always() && needs.build-linux.result == 'success' && needs.build-macos.result == 'success' && needs.build-windows.result == 'success' && needs.build-desktop.result == 'success'
409577
runs-on: ubuntu-latest
410578
permissions:
411579
contents: write
412580

413581
steps:
582+
- name: Checkout release scripts
583+
uses: actions/checkout@v4
584+
585+
- name: Setup Bun
586+
uses: oven-sh/setup-bun@v2
587+
with:
588+
bun-version: ${{ env.BUN_VERSION }}
589+
414590
- name: Download All Artifacts
415591
uses: actions/download-artifact@v4
416592
with:
@@ -462,6 +638,22 @@ jobs:
462638
463639
done
464640
641+
# Keep only distributable Electron files. Updater metadata is merged
642+
# below because macOS produces one latest-mac.yml per architecture.
643+
for dir in artifacts/opencode-desktop-*/; do
644+
[ -d "$dir" ] || continue
645+
for file in "$dir"*; do
646+
[ -f "$file" ] || continue
647+
case "$file" in
648+
*.dmg|*.zip|*.blockmap|*.exe|*.AppImage|*.deb|*.rpm|*.app.tar.gz)
649+
cp "$file" "release/$(basename "$file")"
650+
;;
651+
esac
652+
done
653+
done
654+
655+
bun script/merge-electron-updater-yml.ts artifacts release "${{ needs.fetch-version.outputs.version }}"
656+
465657
required=(
466658
opencode-linux-x64
467659
opencode-linux-x64-baseline
@@ -470,6 +662,10 @@ jobs:
470662
opencode-darwin-arm64
471663
opencode-darwin-x64
472664
opencode-windows-x64.exe
665+
latest.yml
666+
latest-mac.yml
667+
latest-linux.yml
668+
latest-linux-arm64.yml
473669
)
474670
missing=0
475671
for file in "${required[@]}"; do
@@ -482,6 +678,13 @@ jobs:
482678
exit 1
483679
fi
484680
681+
for glob in "*.dmg" "*.zip" "*.exe" "*.AppImage" "*.deb"; do
682+
if ! compgen -G "release/$glob" >/dev/null; then
683+
echo "::error::Required Electron release file matching $glob is missing"
684+
exit 1
685+
fi
686+
done
687+
485688
chmod +x release/opencode-* 2>/dev/null || true
486689
echo ""
487690
echo "Final release files:"
@@ -521,6 +724,26 @@ jobs:
521724
| macOS | Intel | `opencode-darwin-x64` |
522725
| Windows | x64 | `opencode-windows-x64.exe` |
523726
727+
### Desktop Downloads (Electron)
728+
729+
| Platform | Architecture | Files |
730+
|----------|--------------|-------|
731+
| macOS | Apple Silicon | `.dmg` / `.zip` |
732+
| macOS | Intel | `.dmg` / `.zip` |
733+
| Windows | x64 | `.exe` |
734+
| Linux | x64 | `.AppImage` / `.deb` / `.rpm` |
735+
| Linux | ARM64 | `.AppImage` / `.deb` / `.rpm` |
736+
737+
Desktop artifacts use the upstream Electron build flow. They are unsigned because this fork has no Apple or Windows signing credentials. The updater metadata (`latest*.yml`) is generated for this repository, not upstream OpenCode.
738+
739+
#### macOS: Running an unsigned app
740+
741+
After moving the app to `/Applications`, remove the quarantine attribute if macOS blocks it:
742+
743+
```bash
744+
xattr -cr /Applications/OpenCode.app
745+
```
746+
524747
### Quick Install (CLI)
525748
526749
```bash

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,21 @@ The release fails closed unless all of these checks pass:
101101
102102
## Installation
103103
104-
Current CI releases contain cross-platform CLI binaries only.
104+
### Desktop App (Electron)
105+
106+
CI follows the current upstream Electron packaging flow and releases desktop artifacts for macOS, Windows, and Linux. The generated Electron updater metadata targets this repository, so a desktop installation does not switch back to upstream OpenCode for updates.
107+
108+
| Platform | Files |
109+
| --------------------------- | ------------------------------- |
110+
| macOS Apple Silicon / Intel | `.dmg` and `.zip` |
111+
| Windows x64 | NSIS `.exe` |
112+
| Linux x64 / ARM64 | `.AppImage`, `.deb`, and `.rpm` |
113+
114+
The desktop artifacts are unsigned because this fork has no Apple or Windows signing credentials. On macOS, after copying the app to Applications, remove quarantine if Gatekeeper blocks it:
115+
116+
```bash
117+
xattr -cr /Applications/OpenCode.app
118+
```
105119

106120
### CLI Installation
107121

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import fs from "node:fs"
3+
import os from "node:os"
4+
import path from "node:path"
5+
import { pathToFileURL } from "node:url"
6+
7+
import { mergeElectronUpdaterYml } from "./merge-electron-updater-yml"
8+
import { writeElectronReleaseConfig } from "./write-opencode-electron-release-config"
9+
10+
const roots: string[] = []
11+
12+
function temporaryRoot() {
13+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "evil-opencode-electron-"))
14+
roots.push(root)
15+
return root
16+
}
17+
18+
function write(root: string, file: string, content: string) {
19+
const target = path.join(root, file)
20+
fs.mkdirSync(path.dirname(target), { recursive: true })
21+
fs.writeFileSync(target, content)
22+
}
23+
24+
function metadata(version: string, url: string) {
25+
return `version: ${version}\nfiles:\n - url: ${url}\n sha512: digest-${url}\n size: 123\nreleaseDate: '2026-07-29T00:00:00.000Z'\n`
26+
}
27+
28+
afterEach(() => {
29+
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true })
30+
})
31+
32+
describe("Electron release support", () => {
33+
test("generates an executable unsigned config that targets this fork", async () => {
34+
const root = temporaryRoot()
35+
write(
36+
root,
37+
"packages/desktop/electron-builder.config.ts",
38+
'import type { Configuration } from "electron-builder"\nexport default {}',
39+
)
40+
write(
41+
root,
42+
"packages/desktop/package.json",
43+
JSON.stringify({ scripts: { build: "electron-vite build", package: "electron-builder" } }),
44+
)
45+
46+
const output = writeElectronReleaseConfig(root, "WinMin/evil-opencode")
47+
const config = fs.readFileSync(output, "utf8")
48+
expect(config).toContain('owner: "WinMin"')
49+
expect(config).toContain('repo: "evil-opencode"')
50+
expect(config).toContain("identity: null")
51+
expect(config).toContain("notarize: false")
52+
expect(config).toContain("sign: false")
53+
54+
const generated = (await import(pathToFileURL(output).href)).default as Record<string, any>
55+
expect(generated.publish).toEqual({ provider: "github", owner: "WinMin", repo: "evil-opencode", channel: "latest" })
56+
expect(generated.mac.identity).toBeNull()
57+
expect(generated.mac.notarize).toBeFalse()
58+
expect(generated.dmg.sign).toBeFalse()
59+
})
60+
61+
test("merges multi-architecture updater metadata and rejects version drift", () => {
62+
const root = temporaryRoot()
63+
const artifacts = path.join(root, "artifacts")
64+
const release = path.join(root, "release")
65+
const version = "1.18.9"
66+
const inputs = [
67+
["opencode-desktop-windows-x64", "latest.yml", "opencode-desktop-win-x64.exe"],
68+
["opencode-desktop-darwin-x64", "latest-mac.yml", "opencode-desktop-mac-x64.zip"],
69+
["opencode-desktop-darwin-arm64", "latest-mac.yml", "opencode-desktop-mac-arm64.zip"],
70+
["opencode-desktop-linux-x64", "latest-linux.yml", "opencode-desktop-linux-x64.AppImage"],
71+
["opencode-desktop-linux-arm64", "latest-linux-arm64.yml", "opencode-desktop-linux-arm64.AppImage"],
72+
]
73+
for (const [artifact, filename, url] of inputs) write(artifacts, `${artifact}/${filename}`, metadata(version, url))
74+
75+
expect(mergeElectronUpdaterYml(artifacts, release, version)).toEqual([
76+
"latest.yml",
77+
"latest-mac.yml",
78+
"latest-linux.yml",
79+
"latest-linux-arm64.yml",
80+
])
81+
const mac = fs.readFileSync(path.join(release, "latest-mac.yml"), "utf8")
82+
expect(mac).toContain("opencode-desktop-mac-arm64.zip")
83+
expect(mac).toContain("opencode-desktop-mac-x64.zip")
84+
85+
write(artifacts, "opencode-desktop-linux-x64/latest-linux.yml", metadata("1.18.8", "stale.AppImage"))
86+
expect(() => mergeElectronUpdaterYml(artifacts, release, version)).toThrow("expected version 1.18.9")
87+
})
88+
})

0 commit comments

Comments
 (0)