name: Release on: push: tags: - 'v*' concurrency: group: release-${{ github.ref }} cancel-in-progress: true jobs: # ============================================ # Desktop builds (Windows, macOS, Linux) # ============================================ build-desktop: strategy: fail-fast: false matrix: include: - platform: macos-latest args: '--target aarch64-apple-darwin' arch: aarch64 os: macos - platform: macos-latest args: '--target x86_64-apple-darwin' arch: x86_64 os: macos - platform: ubuntu-22.04 args: '' arch: x86_64 os: linux - platform: windows-latest args: '' arch: x86_64 os: windows runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: lts/* cache: 'npm' - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Rust cache uses: swatinem/rust-cache@v2 with: workspaces: './src-tauri -> target' key: ${{ matrix.os }}-${{ matrix.arch }} - name: Install Linux dependencies if: matrix.platform == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - run: npm ci # tauri-action 自动执行 beforeBuildCommand (npm run build) + cargo build - name: Build desktop app uses: tauri-apps/tauri-action@v0 with: args: ${{ matrix.args }} - name: Prepare artifacts shell: bash run: | mkdir -p release-artifacts BUNDLE_DIR="src-tauri/target/release/bundle" if [[ "${{ matrix.os }}" == "macos" ]]; then BUNDLE_DIR="src-tauri/target/${{ matrix.arch }}-apple-darwin/release/bundle" fi if [[ "${{ matrix.os }}" == "windows" ]]; then find "$BUNDLE_DIR/nsis" -name "*.exe" -exec cp {} release-artifacts/ \; 2>/dev/null || true elif [[ "${{ matrix.os }}" == "macos" ]]; then find "$BUNDLE_DIR/dmg" -name "*.dmg" -exec cp {} release-artifacts/ \; 2>/dev/null || true elif [[ "${{ matrix.os }}" == "linux" ]]; then find "$BUNDLE_DIR/deb" -name "*.deb" -exec cp {} release-artifacts/ \; 2>/dev/null || true find "$BUNDLE_DIR/appimage" -name "*.AppImage" -exec cp {} release-artifacts/ \; 2>/dev/null || true fi echo "Artifacts:" ls -R release-artifacts - uses: actions/upload-artifact@v7 with: name: artifacts-desktop-${{ matrix.os }}-${{ matrix.arch }} path: release-artifacts if-no-files-found: warn retention-days: 1 # ============================================ # Android builds (arm64 + armv7 并行) # ============================================ build-android: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - target: aarch64 rust_target: aarch64-linux-android label: arm64 - target: armv7 rust_target: armv7-linux-androideabi label: armv7 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: lts/* cache: 'npm' - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '17' - uses: android-actions/setup-android@v3 - run: sdkmanager "ndk;29.0.14206865" - name: Install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_target }} - name: Rust cache uses: swatinem/rust-cache@v2 with: workspaces: './src-tauri -> target' key: android-${{ matrix.target }} - name: Gradle cache uses: actions/cache@v5 with: path: | ~/.gradle/caches ~/.gradle/wrapper src-tauri/gen/android/.gradle key: gradle-${{ matrix.target }}-${{ hashFiles('src-tauri/gen/android/**/*.gradle*', 'src-tauri/gen/android/**/gradle-wrapper.properties') }} restore-keys: gradle- - run: npm ci - name: Setup Android signing run: | cd src-tauri/gen/android echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties - name: Build and sign APK run: | VERSION=${{ github.ref_name }} BT="$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools/ | tail -1)" npx tauri android build --apk --target ${{ matrix.target }} SIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk -path '*/release/*-signed*' -name '*.apk' -type f 2>/dev/null | head -1) RELEASE_APK=$(find src-tauri/gen/android/app/build/outputs/apk -path '*/release/*.apk' -name '*.apk' -type f 2>/dev/null | head -1) APK_PATH="${SIGNED_APK:-$RELEASE_APK}" DEST="OpenCodeUI-${VERSION}-android-${{ matrix.label }}.apk" if $BT/apksigner verify "$APK_PATH" 2>/dev/null; then cp "$APK_PATH" "$DEST" else $BT/zipalign -v 4 "$APK_PATH" "${APK_PATH}.aligned" $BT/apksigner sign \ --ks $RUNNER_TEMP/keystore.jks \ --ks-key-alias ${{ secrets.ANDROID_KEY_ALIAS }} \ --ks-pass pass:${{ secrets.ANDROID_KEY_PASSWORD }} \ --key-pass pass:${{ secrets.ANDROID_KEY_PASSWORD }} \ --out "$DEST" "${APK_PATH}.aligned" fi mkdir -p release-artifacts mv "$DEST" release-artifacts/ env: NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/29.0.14206865 - uses: actions/upload-artifact@v7 with: name: artifacts-android-${{ matrix.label }} path: release-artifacts/* retention-days: 1 # ============================================ # Publish Release # ============================================ publish-release: permissions: contents: write runs-on: ubuntu-latest needs: [build-desktop, build-android] steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v8 with: path: artifacts pattern: artifacts-* merge-multiple: true - name: Display artifacts run: ls -R artifacts - name: Extract changelog id: changelog run: | TAG="${{ github.ref_name }}" BODY=$(sed -n "/^## \[${TAG}\]/,/^## \[/{ /^## \[${TAG}\]/d; /^## \[/d; p; }" CHANGELOG.md) BODY=$(echo "$BODY" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba;}') if [ -z "$BODY" ]; then BODY="No changelog entry found for ${TAG}." fi echo "$BODY" > /tmp/release-body.md printf '\n---\n\n**Desktop:** Windows (.exe), macOS (.dmg), Linux (.deb, .AppImage)\n**Mobile:** Android (.apk) — arm64, armeabi-v7a\n' >> /tmp/release-body.md - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: 'OpenCodeUI ${{ github.ref_name }}' body_path: /tmp/release-body.md files: artifacts/* draft: false prerelease: ${{ contains(github.ref_name, 'canary') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}