|
| 1 | +name: fork-release |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + inputs: |
| 6 | + tag: |
| 7 | + description: "Release tag (e.g. v0.1.0-fork)" |
| 8 | + required: true |
| 9 | + type: string |
| 10 | + draft: |
| 11 | + description: "Create release as draft" |
| 12 | + required: false |
| 13 | + type: boolean |
| 14 | + default: false |
| 15 | + push: |
| 16 | + tags: |
| 17 | + - 'v*-fork*' |
| 18 | + |
| 19 | +concurrency: |
| 20 | + group: fork-release-${{ github.ref }} |
| 21 | + cancel-in-progress: false |
| 22 | + |
| 23 | +permissions: |
| 24 | + contents: write |
| 25 | + |
| 26 | +jobs: |
| 27 | + # ─── CLI (Linux x64 + Windows x64) ────────────────────────────────── |
| 28 | + build-cli: |
| 29 | + if: github.repository == 'Rwanbt/opencode' |
| 30 | + runs-on: ubuntu-latest |
| 31 | + timeout-minutes: 30 |
| 32 | + steps: |
| 33 | + - uses: actions/checkout@v4 |
| 34 | + |
| 35 | + - uses: oven-sh/setup-bun@v2 |
| 36 | + with: |
| 37 | + bun-version: latest |
| 38 | + |
| 39 | + - name: Install deps |
| 40 | + run: bun install |
| 41 | + |
| 42 | + - name: Resolve version |
| 43 | + id: ver |
| 44 | + run: | |
| 45 | + RAW="${{ inputs.tag || github.ref_name }}" |
| 46 | + # strip leading v and trailing -fork* → x.y.z |
| 47 | + VER="${RAW#v}" |
| 48 | + VER="${VER%%-fork*}" |
| 49 | + # Must be semver-like; fallback to 0.0.0 if weird |
| 50 | + if ! echo "$VER" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then |
| 51 | + VER="0.0.0" |
| 52 | + fi |
| 53 | + echo "version=$VER" >> "$GITHUB_OUTPUT" |
| 54 | + echo "tag=$RAW" >> "$GITHUB_OUTPUT" |
| 55 | +
|
| 56 | + - name: Build CLI (cross-compile all targets via Bun) |
| 57 | + working-directory: packages/opencode |
| 58 | + run: bun run script/build.ts --skip-embed-web-ui |
| 59 | + env: |
| 60 | + OPENCODE_VERSION: ${{ steps.ver.outputs.version }} |
| 61 | + OPENCODE_CHANNEL: fork |
| 62 | + |
| 63 | + - name: Package artifacts |
| 64 | + working-directory: packages/opencode/dist |
| 65 | + run: | |
| 66 | + set -e |
| 67 | + echo "dist contents:" |
| 68 | + ls -la |
| 69 | + # Linux x64 (glibc) |
| 70 | + if [ -d opencode-linux-x64 ]; then |
| 71 | + tar czf opencode-linux-x64.tar.gz -C opencode-linux-x64 . |
| 72 | + fi |
| 73 | + # Windows x64 |
| 74 | + if [ -d opencode-windows-x64 ]; then |
| 75 | + (cd opencode-windows-x64 && zip -qr ../opencode-windows-x64.zip .) |
| 76 | + fi |
| 77 | + ls -lh *.tar.gz *.zip 2>/dev/null || true |
| 78 | +
|
| 79 | + - uses: actions/upload-artifact@v4 |
| 80 | + with: |
| 81 | + name: cli-bundles |
| 82 | + path: | |
| 83 | + packages/opencode/dist/opencode-linux-x64.tar.gz |
| 84 | + packages/opencode/dist/opencode-windows-x64.zip |
| 85 | + if-no-files-found: error |
| 86 | + |
| 87 | + # ─── Android APK ──────────────────────────────────────────────────── |
| 88 | + build-android: |
| 89 | + if: github.repository == 'Rwanbt/opencode' |
| 90 | + runs-on: ubuntu-latest |
| 91 | + timeout-minutes: 60 |
| 92 | + steps: |
| 93 | + - uses: actions/checkout@v4 |
| 94 | + |
| 95 | + - name: Setup Java |
| 96 | + uses: actions/setup-java@v4 |
| 97 | + with: |
| 98 | + distribution: 'temurin' |
| 99 | + java-version: '21' |
| 100 | + |
| 101 | + - name: Setup Android SDK |
| 102 | + uses: android-actions/setup-android@v3 |
| 103 | + |
| 104 | + - name: Install Android NDK + CMake |
| 105 | + run: | |
| 106 | + rm -rf $ANDROID_HOME/ndk/* 2>/dev/null || true |
| 107 | + sdkmanager "ndk;27.0.12077973" "cmake;3.22.1" |
| 108 | + echo "NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV |
| 109 | + echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.0.12077973" >> $GITHUB_ENV |
| 110 | +
|
| 111 | + - name: Setup Rust |
| 112 | + uses: dtolnay/rust-toolchain@stable |
| 113 | + with: |
| 114 | + targets: aarch64-linux-android |
| 115 | + |
| 116 | + - uses: Swatinem/rust-cache@v2 |
| 117 | + with: |
| 118 | + workspaces: packages/mobile/src-tauri |
| 119 | + cache-targets: true |
| 120 | + |
| 121 | + - uses: oven-sh/setup-bun@v2 |
| 122 | + |
| 123 | + - name: Install dependencies |
| 124 | + run: bun install |
| 125 | + |
| 126 | + - name: Download runtime binaries |
| 127 | + run: | |
| 128 | + JNIDIR="packages/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a" |
| 129 | + ASSETS="packages/mobile/src-tauri/gen/android/app/src/main/assets/runtime" |
| 130 | + RUNTIME="packages/mobile/src-tauri/assets/runtime" |
| 131 | + mkdir -p "$JNIDIR" "$ASSETS" "$RUNTIME/node_modules/@parcel/watcher" |
| 132 | +
|
| 133 | + curl -fsSL https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64-musl.zip -o /tmp/bun.zip |
| 134 | + unzip -o -q /tmp/bun.zip -d /tmp/bun-extract |
| 135 | + cp /tmp/bun-extract/bun-linux-aarch64-musl/bun "$JNIDIR/libbun_exec.so" |
| 136 | +
|
| 137 | + ALPINE_INDEX="https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64" |
| 138 | + resolve_apk() { |
| 139 | + curl -fsSL "$ALPINE_INDEX/" \ |
| 140 | + | grep -oE "$1-[0-9][0-9.]*-r[0-9]+\.apk" \ |
| 141 | + | sort -uV | tail -1 |
| 142 | + } |
| 143 | +
|
| 144 | + MUSL_APK=$(resolve_apk "musl") |
| 145 | + curl -fsSL "$ALPINE_INDEX/$MUSL_APK" -o /tmp/musl.apk |
| 146 | + cd /tmp && tar -xzf musl.apk lib/ld-musl-aarch64.so.1 |
| 147 | + cp /tmp/lib/ld-musl-aarch64.so.1 "$GITHUB_WORKSPACE/$JNIDIR/libmusl_linker.so" |
| 148 | + cd "$GITHUB_WORKSPACE" |
| 149 | +
|
| 150 | + LIBSTDCPP_APK=$(resolve_apk "libstdc\+\+") |
| 151 | + LIBGCC_APK=$(resolve_apk "libgcc") |
| 152 | + curl -fsSL "$ALPINE_INDEX/$LIBSTDCPP_APK" -o /tmp/libstdcpp.apk |
| 153 | + curl -fsSL "$ALPINE_INDEX/$LIBGCC_APK" -o /tmp/libgcc.apk |
| 154 | + cd /tmp && tar -xzf libstdcpp.apk --wildcards 'usr/lib/libstdc++.so.6.*' |
| 155 | + cd /tmp && tar -xzf libgcc.apk usr/lib/libgcc_s.so.1 |
| 156 | + cp /tmp/usr/lib/libstdc++.so.6.* "$GITHUB_WORKSPACE/$JNIDIR/libstdcpp_compat.so" |
| 157 | + cp /tmp/usr/lib/libgcc_s.so.1 "$GITHUB_WORKSPACE/$JNIDIR/libgcc_compat.so" |
| 158 | + cd "$GITHUB_WORKSPACE" |
| 159 | +
|
| 160 | + curl -fsSL https://github.com/robxu9/bash-static/releases/latest/download/bash-linux-aarch64 -o "$JNIDIR/libbash_exec.so" |
| 161 | +
|
| 162 | + RG_VERSION=$(curl -fsSL "https://api.github.com/repos/BurntSushi/ripgrep/releases/latest" | jq -r '.tag_name') |
| 163 | + curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/ripgrep-${RG_VERSION}-aarch64-unknown-linux-gnu.tar.gz" -o /tmp/rg.tar.gz |
| 164 | + tar -xzf /tmp/rg.tar.gz -C /tmp |
| 165 | + find /tmp -name "rg" -type f | head -1 | xargs -I{} cp {} "$JNIDIR/librg_exec.so" |
| 166 | +
|
| 167 | + curl -fsSL "https://landley.net/toybox/bin/toybox-aarch64" -o "$JNIDIR/libtoybox_exec.so" |
| 168 | +
|
| 169 | + NDK=$ANDROID_HOME/ndk/27.0.12077973 |
| 170 | + $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang \ |
| 171 | + -o "$JNIDIR/libpty_server.so" \ |
| 172 | + packages/mobile/src-tauri/gen/android/pty_server.c \ |
| 173 | + -O2 -Wall -Wextra -Wno-unused-parameter |
| 174 | +
|
| 175 | + chmod 755 "$JNIDIR"/* |
| 176 | +
|
| 177 | + - name: Build llama.cpp for Android |
| 178 | + run: | |
| 179 | + JNIDIR="packages/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a" |
| 180 | + HEADERS="packages/mobile/src-tauri/gen/android/llama-headers" |
| 181 | + NDK=$ANDROID_HOME/ndk/27.0.12077973 |
| 182 | +
|
| 183 | + git clone --depth 1 https://github.com/ggml-org/llama.cpp.git /tmp/llama.cpp |
| 184 | +
|
| 185 | + cmake -B /tmp/llama.cpp/build-android \ |
| 186 | + -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ |
| 187 | + -DANDROID_ABI=arm64-v8a \ |
| 188 | + -DANDROID_PLATFORM=android-28 \ |
| 189 | + -DCMAKE_C_FLAGS="-march=armv8.2-a+dotprod+fp16" \ |
| 190 | + -DCMAKE_CXX_FLAGS="-march=armv8.2-a+dotprod+fp16" \ |
| 191 | + -DGGML_OPENMP=OFF \ |
| 192 | + -DGGML_LLAMAFILE=OFF \ |
| 193 | + -DLLAMA_CURL=OFF \ |
| 194 | + -DBUILD_SHARED_LIBS=ON \ |
| 195 | + -DCMAKE_BUILD_TYPE=Release \ |
| 196 | + -S /tmp/llama.cpp |
| 197 | + cmake --build /tmp/llama.cpp/build-android --config Release -j$(nproc) |
| 198 | +
|
| 199 | + cp /tmp/llama.cpp/build-android/bin/libllama.so "$JNIDIR/" |
| 200 | + cp /tmp/llama.cpp/build-android/bin/libggml.so "$JNIDIR/" |
| 201 | + cp /tmp/llama.cpp/build-android/bin/libggml-base.so "$JNIDIR/" |
| 202 | + cp /tmp/llama.cpp/build-android/bin/libggml-cpu.so "$JNIDIR/" |
| 203 | + $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip "$JNIDIR"/libllama.so "$JNIDIR"/libggml*.so |
| 204 | +
|
| 205 | + cmake -B /tmp/llama.cpp/build-static \ |
| 206 | + -DCMAKE_SYSTEM_NAME=Linux \ |
| 207 | + -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ |
| 208 | + -DCMAKE_C_COMPILER=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang \ |
| 209 | + -DCMAKE_CXX_COMPILER=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang++ \ |
| 210 | + -DCMAKE_C_FLAGS="-march=armv8.2-a" \ |
| 211 | + -DCMAKE_CXX_FLAGS="-march=armv8.2-a" \ |
| 212 | + -DBUILD_SHARED_LIBS=OFF \ |
| 213 | + -DGGML_OPENMP=OFF \ |
| 214 | + -DGGML_LLAMAFILE=OFF \ |
| 215 | + -DLLAMA_CURL=OFF \ |
| 216 | + -DCMAKE_BUILD_TYPE=Release \ |
| 217 | + -S /tmp/llama.cpp |
| 218 | + cmake --build /tmp/llama.cpp/build-static --config Release --target llama-server -j$(nproc) |
| 219 | + cp /tmp/llama.cpp/build-static/bin/llama-server "$JNIDIR/libllama_server.so" |
| 220 | + $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip "$JNIDIR/libllama_server.so" |
| 221 | +
|
| 222 | + mkdir -p "$HEADERS" |
| 223 | + cp /tmp/llama.cpp/include/*.h "$HEADERS/" |
| 224 | + cp /tmp/llama.cpp/ggml/include/*.h "$HEADERS/" |
| 225 | +
|
| 226 | + - name: Bundle OpenCode CLI |
| 227 | + run: | |
| 228 | + RUNTIME="packages/mobile/src-tauri/assets/runtime" |
| 229 | + ASSETS="packages/mobile/src-tauri/gen/android/app/src/main/assets/runtime" |
| 230 | + mkdir -p "$RUNTIME/node_modules/@parcel/watcher" "$ASSETS/node_modules/@parcel/watcher" |
| 231 | + node scripts/bundle-mobile.mjs --outdir "$RUNTIME" |
| 232 | + echo 'export function createWrapper() { return undefined }' > "$RUNTIME/node_modules/@parcel/watcher/wrapper.js" |
| 233 | + echo '{"name":"@parcel/watcher","version":"0.0.0","main":"wrapper.js"}' > "$RUNTIME/node_modules/@parcel/watcher/package.json" |
| 234 | + cp "$RUNTIME/opencode-cli.js" "$ASSETS/opencode-cli.js" |
| 235 | + cp -r "$RUNTIME/node_modules" "$ASSETS/" |
| 236 | + cp "$RUNTIME"/*.wasm "$ASSETS/" 2>/dev/null || true |
| 237 | +
|
| 238 | + - name: Install Tauri CLI |
| 239 | + run: | |
| 240 | + bun add -g @tauri-apps/cli |
| 241 | + cargo install tauri-cli --version "^2" --locked || true |
| 242 | +
|
| 243 | + - name: Build frontend |
| 244 | + working-directory: packages/mobile |
| 245 | + run: bun run build |
| 246 | + |
| 247 | + - name: Clear cargo config |
| 248 | + run: rm -f packages/mobile/src-tauri/.cargo/config.toml |
| 249 | + |
| 250 | + - name: Download ONNX Runtime for Android |
| 251 | + run: | |
| 252 | + ORT_VERSION="1.22.0" |
| 253 | + curl -fsSL "https://repo1.maven.org/maven2/com/microsoft/onnxruntime/onnxruntime-android/${ORT_VERSION}/onnxruntime-android-${ORT_VERSION}.aar" -o /tmp/ort.aar |
| 254 | + cd /tmp && unzip -o ort.aar "jni/arm64-v8a/*" |
| 255 | + mkdir -p /tmp/ort-android/lib |
| 256 | + cp /tmp/jni/arm64-v8a/libonnxruntime.so /tmp/ort-android/lib/ |
| 257 | + echo "ORT_LIB_LOCATION=/tmp/ort-android" >> $GITHUB_ENV |
| 258 | + echo "ORT_PREFER_DYNAMIC_LINK=1" >> $GITHUB_ENV |
| 259 | +
|
| 260 | + - name: Build APK |
| 261 | + working-directory: packages/mobile |
| 262 | + run: bunx tauri android build --target aarch64 |
| 263 | + |
| 264 | + - name: Sign APK (debug keystore) |
| 265 | + run: | |
| 266 | + APK="packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk" |
| 267 | + SIGNED="packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/opencode-mobile.apk" |
| 268 | + keytool -genkey -v \ |
| 269 | + -keystore /tmp/ci.keystore \ |
| 270 | + -alias opencode -keyalg RSA -keysize 2048 -validity 10000 \ |
| 271 | + -storepass android -keypass android \ |
| 272 | + -dname "CN=OpenCode CI, OU=Dev, O=OpenCode, L=Paris, ST=IDF, C=FR" |
| 273 | + cp "$APK" "$SIGNED" |
| 274 | + java -jar "$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | tail -1)/lib/apksigner.jar" sign \ |
| 275 | + --ks /tmp/ci.keystore --ks-pass pass:android --key-pass pass:android \ |
| 276 | + "$SIGNED" |
| 277 | +
|
| 278 | + - uses: actions/upload-artifact@v4 |
| 279 | + with: |
| 280 | + name: apk |
| 281 | + path: packages/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/opencode-mobile.apk |
| 282 | + if-no-files-found: error |
| 283 | + |
| 284 | + # ─── Create GitHub Release ────────────────────────────────────────── |
| 285 | + release: |
| 286 | + needs: [build-cli, build-android] |
| 287 | + if: github.repository == 'Rwanbt/opencode' |
| 288 | + runs-on: ubuntu-latest |
| 289 | + steps: |
| 290 | + - name: Resolve tag |
| 291 | + id: tag |
| 292 | + run: | |
| 293 | + TAG="${{ inputs.tag || github.ref_name }}" |
| 294 | + echo "tag=$TAG" >> "$GITHUB_OUTPUT" |
| 295 | +
|
| 296 | + - uses: actions/download-artifact@v4 |
| 297 | + with: |
| 298 | + name: cli-bundles |
| 299 | + path: ./release |
| 300 | + |
| 301 | + - uses: actions/download-artifact@v4 |
| 302 | + with: |
| 303 | + name: apk |
| 304 | + path: ./release |
| 305 | + |
| 306 | + - name: List artifacts |
| 307 | + run: ls -lh ./release |
| 308 | + |
| 309 | + - uses: softprops/action-gh-release@v2 |
| 310 | + with: |
| 311 | + tag_name: ${{ steps.tag.outputs.tag }} |
| 312 | + name: ${{ steps.tag.outputs.tag }} |
| 313 | + draft: ${{ inputs.draft || false }} |
| 314 | + generate_release_notes: true |
| 315 | + fail_on_unmatched_files: true |
| 316 | + files: | |
| 317 | + release/opencode-linux-x64.tar.gz |
| 318 | + release/opencode-windows-x64.zip |
| 319 | + release/opencode-mobile.apk |
0 commit comments