Skip to content

Commit 03de7f8

Browse files
committed
chore: customize fork release workflows
1 parent 626a488 commit 03de7f8

4 files changed

Lines changed: 286 additions & 2 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: sync-upstream
2+
3+
on:
4+
schedule:
5+
- cron: "17 2 * * *"
6+
workflow_dispatch:
7+
8+
concurrency: ${{ github.workflow }}-${{ github.ref }}
9+
10+
permissions:
11+
contents: write
12+
actions: write
13+
14+
jobs:
15+
sync:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
ref: zexi/dev
22+
23+
- name: Configure git
24+
run: |
25+
git config user.name "github-actions[bot]"
26+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
27+
28+
- name: Rebase zexi/dev onto upstream dev
29+
run: |
30+
git remote add upstream https://github.com/anomalyco/opencode.git
31+
git fetch upstream dev
32+
git rebase upstream/dev
33+
git push --force-with-lease origin HEAD:zexi/dev
34+
35+
- name: Keep only fork workflows enabled
36+
env:
37+
GH_TOKEN: ${{ github.token }}
38+
GH_REPO: ${{ github.repository }}
39+
run: |
40+
allowlist='
41+
.github/workflows/sync-upstream.yml
42+
.github/workflows/zexi-electron.yml
43+
'
44+
45+
gh workflow list -R "$GH_REPO" --limit 200 --json path \
46+
| jq -r '.[].path' \
47+
| while read -r workflow; do
48+
if printf '%s\n' "$allowlist" | sed 's/^ *//' | grep -Fxq "$workflow"; then
49+
gh workflow enable "$workflow" -R "$GH_REPO" || true
50+
else
51+
gh workflow disable "$workflow" -R "$GH_REPO" || true
52+
fi
53+
done
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
name: zexi-electron
2+
3+
on:
4+
push:
5+
branches:
6+
- zexi/dev
7+
workflow_dispatch:
8+
9+
concurrency: ${{ github.workflow }}-${{ github.ref }}
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
build-electron:
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
settings:
20+
- name: mac-arm64
21+
host: macos-26
22+
target: aarch64-apple-darwin
23+
platform_flag: --mac --arm64
24+
bun_install_flags: --os=darwin --cpu=arm64
25+
- name: windows-x64
26+
host: windows-2025
27+
target: x86_64-pc-windows-msvc
28+
platform_flag: --win
29+
bun_install_flags: ""
30+
runs-on: ${{ matrix.settings.host }}
31+
env:
32+
OPENCODE_CHANNEL: prod
33+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
34+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
35+
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
36+
APPLE_API_KEY_PATH: ${{ secrets.APPLE_API_KEY_PATH }}
37+
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
38+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
39+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
40+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
41+
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
42+
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
43+
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- uses: apple-actions/import-codesign-certs@v2
48+
if: runner.os == 'macOS' && env.APPLE_CERTIFICATE != '' && env.APPLE_CERTIFICATE_PASSWORD != ''
49+
with:
50+
keychain: build
51+
p12-file-base64: ${{ env.APPLE_CERTIFICATE }}
52+
p12-password: ${{ env.APPLE_CERTIFICATE_PASSWORD }}
53+
54+
- name: Setup Apple API Key
55+
if: runner.os == 'macOS' && env.APPLE_API_KEY_PATH != ''
56+
shell: bash
57+
run: echo "${{ env.APPLE_API_KEY_PATH }}" > "$RUNNER_TEMP/apple-api-key.p8"
58+
59+
- uses: ./.github/actions/setup-bun
60+
with:
61+
install-flags: ${{ matrix.settings.bun_install_flags }}
62+
63+
- uses: actions/setup-node@v4
64+
with:
65+
node-version: "24"
66+
67+
- name: Azure login
68+
if: runner.os == 'Windows' && env.AZURE_CLIENT_ID != '' && env.AZURE_TENANT_ID != '' && env.AZURE_SUBSCRIPTION_ID != ''
69+
uses: azure/login@v2
70+
with:
71+
client-id: ${{ env.AZURE_CLIENT_ID }}
72+
tenant-id: ${{ env.AZURE_TENANT_ID }}
73+
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
74+
75+
- name: Prepare
76+
working-directory: packages/desktop
77+
env:
78+
RUST_TARGET: ${{ matrix.settings.target }}
79+
run: bun ./scripts/prepare.ts
80+
81+
- name: Build
82+
working-directory: packages/desktop
83+
run: bun run build
84+
85+
- name: Package macOS (signed)
86+
if: runner.os == 'macOS' && env.APPLE_CERTIFICATE != '' && env.APPLE_CERTIFICATE_PASSWORD != ''
87+
working-directory: packages/desktop
88+
timeout-minutes: 90
89+
env:
90+
CSC_LINK: ${{ env.APPLE_CERTIFICATE }}
91+
CSC_KEY_PASSWORD: ${{ env.APPLE_CERTIFICATE_PASSWORD }}
92+
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
93+
APPLE_API_KEY_ID: ${{ env.APPLE_API_KEY }}
94+
APPLE_API_ISSUER: ${{ env.APPLE_API_ISSUER }}
95+
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
96+
97+
- name: Package macOS (unsigned)
98+
if: runner.os == 'macOS' && (env.APPLE_CERTIFICATE == '' || env.APPLE_CERTIFICATE_PASSWORD == '')
99+
working-directory: packages/desktop
100+
timeout-minutes: 90
101+
env:
102+
CSC_IDENTITY_AUTO_DISCOVERY: "false"
103+
run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
104+
105+
- name: Package Windows
106+
if: runner.os == 'Windows'
107+
working-directory: packages/desktop
108+
timeout-minutes: 90
109+
shell: pwsh
110+
run: |
111+
$attempts = 3
112+
for ($attempt = 1; $attempt -le $attempts; $attempt++) {
113+
npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
114+
if ($LASTEXITCODE -eq 0) {
115+
exit 0
116+
}
117+
if ($attempt -eq $attempts) {
118+
exit $LASTEXITCODE
119+
}
120+
Write-Host "Windows packaging failed on attempt $attempt. Retrying in 20 seconds..."
121+
Start-Sleep -Seconds 20
122+
}
123+
124+
- name: Verify signed Windows artifacts
125+
if: runner.os == 'Windows'
126+
shell: pwsh
127+
run: |
128+
$files = @()
129+
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
130+
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
131+
$files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
132+
133+
foreach ($file in $files | Select-Object -Unique) {
134+
$sig = Get-AuthenticodeSignature $file
135+
Write-Host "$file => $($sig.Status)"
136+
}
137+
138+
- uses: actions/upload-artifact@v4
139+
with:
140+
name: opencode-electron-${{ matrix.settings.name }}
141+
path: |
142+
packages/desktop/dist/*
143+
!packages/desktop/dist/*unpacked
144+
!packages/desktop/dist/mac-arm64
145+
146+
publish-release:
147+
needs: build-electron
148+
runs-on: ubuntu-latest
149+
steps:
150+
- uses: actions/download-artifact@v4
151+
with:
152+
path: release-artifacts
153+
154+
- name: Create or update release
155+
id: meta
156+
shell: bash
157+
env:
158+
GH_TOKEN: ${{ github.token }}
159+
GH_REPO: ${{ github.repository }}
160+
GITHUB_SHA: ${{ github.sha }}
161+
TARGET_REF: ${{ github.sha }}
162+
run: |
163+
short_sha="${GITHUB_SHA::7}"
164+
stamp="$(date -u +%Y%m%d-%H%M)"
165+
release_tag="zexi-electron-${stamp}-${short_sha}"
166+
release_name="zexi electron build ${stamp} ${short_sha}"
167+
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
168+
gh release view "$release_tag" --repo "$GH_REPO" >/dev/null 2>&1 || gh release create "$release_tag" --repo "$GH_REPO" --target "$TARGET_REF" --title "$release_name" --notes ""
169+
170+
- name: Upload release assets
171+
env:
172+
GH_TOKEN: ${{ github.token }}
173+
GH_REPO: ${{ github.repository }}
174+
RELEASE_TAG: ${{ steps.meta.outputs.release_tag }}
175+
run: |
176+
find release-artifacts -type f \
177+
\( \
178+
-name '*.dmg' -o \
179+
-name '*.zip' -o \
180+
-name '*.exe' -o \
181+
-name 'latest*.yml' -o \
182+
-name '*.blockmap' \
183+
\) \
184+
-print0 | xargs -0 gh release upload "$RELEASE_TAG" --repo "$GH_REPO" --clobber
185+
186+
- name: Prune old zexi releases
187+
env:
188+
GH_TOKEN: ${{ github.token }}
189+
GH_REPO: ${{ github.repository }}
190+
run: |
191+
gh api "repos/$GH_REPO/releases" --paginate --jq '.[] | select(.tag_name | startswith("zexi-electron-")) | [.created_at, .tag_name] | @tsv' \
192+
| sort -r \
193+
| awk 'NR > 3 { print $2 }' \
194+
| while read -r tag; do
195+
[ -n "$tag" ] || continue
196+
gh release delete "$tag" --repo "$GH_REPO" --yes
197+
done

packages/desktop/electron-builder.config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const channel = (() => {
2626
return "dev"
2727
})()
2828

29+
const getUpdateOwner = () => process.env.OPENCODE_UPDATE_OWNER?.trim() || "wangzexi"
30+
const getUpdateRepo = () => process.env.OPENCODE_UPDATE_REPO?.trim() || "opencode"
31+
const getBetaUpdateRepo = () => process.env.OPENCODE_UPDATE_BETA_REPO?.trim() || getUpdateRepo()
32+
2933
const getBase = (): Configuration => ({
3034
artifactName: "opencode-desktop-${os}-${arch}.${ext}",
3135
directories: {
@@ -96,7 +100,7 @@ function getConfig() {
96100
appId: "ai.opencode.desktop.beta",
97101
productName: "OpenCode Beta",
98102
protocols: { name: "OpenCode Beta", schemes: ["opencode"] },
99-
publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" },
103+
publish: { provider: "github", owner: getUpdateOwner(), repo: getBetaUpdateRepo(), channel: "latest" },
100104
rpm: { packageName: "opencode-beta" },
101105
}
102106
}
@@ -106,7 +110,7 @@ function getConfig() {
106110
appId: "ai.opencode.desktop",
107111
productName: "OpenCode",
108112
protocols: { name: "OpenCode", schemes: ["opencode"] },
109-
publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" },
113+
publish: { provider: "github", owner: getUpdateOwner(), repo: getUpdateRepo(), channel: "latest" },
110114
rpm: { packageName: "opencode" },
111115
}
112116
}

packages/script/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ if (!semver.satisfies(process.versions.bun, expectedBunVersionRange)) {
2020
const env = {
2121
OPENCODE_CHANNEL: process.env["OPENCODE_CHANNEL"],
2222
OPENCODE_BUMP: process.env["OPENCODE_BUMP"],
23+
OPENCODE_FORK_SUFFIX: process.env["OPENCODE_FORK_SUFFIX"],
2324
OPENCODE_VERSION: process.env["OPENCODE_VERSION"],
2425
OPENCODE_RELEASE: process.env["OPENCODE_RELEASE"],
2526
}
@@ -30,9 +31,38 @@ const CHANNEL = await (async () => {
3031
return await $`git branch --show-current`.text().then((x) => x.trim())
3132
})()
3233
const IS_PREVIEW = CHANNEL !== "latest"
34+
const FORK_SUFFIX = await (async () => {
35+
if (env.OPENCODE_FORK_SUFFIX?.trim()) return env.OPENCODE_FORK_SUFFIX.trim()
36+
const branch = await $`git branch --show-current`.text().then((x) => x.trim())
37+
if (branch.startsWith("zexi/")) return "zexi"
38+
return ""
39+
})()
40+
const BASE_VERSION = await (async () => {
41+
const synced = await $`git log --format=%s -n 1 --grep=^sync\\ release\\ versions\\ for\\ v -- package.json packages/desktop-electron/package.json`
42+
.text()
43+
.then((x) => x.trim())
44+
.catch(() => "")
45+
const matched = synced.match(/v(\d+\.\d+\.\d+)/)
46+
if (matched) return matched[1]
47+
48+
const desktopPkg = await Bun.file(path.resolve(import.meta.dir, "../../desktop-electron/package.json"))
49+
.json()
50+
.catch(() => ({ version: "" }))
51+
if (typeof desktopPkg.version === "string" && desktopPkg.version) {
52+
return desktopPkg.version.replace(/-.+$/, "")
53+
}
54+
55+
return await fetch("https://registry.npmjs.org/opencode-ai/latest")
56+
.then((res) => {
57+
if (!res.ok) throw new Error(res.statusText)
58+
return res.json()
59+
})
60+
.then((data: any) => data.version)
61+
})()
3362

3463
const VERSION = await (async () => {
3564
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION
65+
if (FORK_SUFFIX) return `${BASE_VERSION}-${FORK_SUFFIX}`
3666
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
3767
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
3868
.then((res) => {

0 commit comments

Comments
 (0)