From 3ac4eb7b82db4a864c21feb93090949f33929fb7 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sun, 20 Sep 2026 20:00:08 +0200 Subject: [PATCH] ci: release pipelines for server, desktop, and android - android-release.yml: android-v* tags build signed release APK + AAB and publish a GitHub release; ANDROID_KEYSTORE_* secrets sign with a real key, debug-cert fallback keeps APKs sideload-installable - build.gradle.kts: env-driven releaseEnv signing config - AndroidManifest: declare dataSync foregroundServiceType on SystemForegroundService (lintVitalRelease fatal, runtime crash on API 34+) - desktop-release.yml: add libfuse2 + fuse3 for the AppImage toolchain - tauri.conf.json: .deb declares fuse3 runtime dep (on-demand FUSE mode) - tauri-plugin-http pinned ~2.6 + npm @tauri-apps/plugin-http@^2.6.1: clears the tauri build version-mismatch guard (rust 2.7 vs npm 2.5) that made every desktop release build fail Verified locally: goreleaser snapshot (12 binaries), assembleRelease + bundleRelease (signed APK+AAB), cargo tauri build (.deb + .AppImage). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/android-release.yml | 64 ++++++++++++++++++++++++ .github/workflows/desktop-release.yml | 2 +- ROADMAP.md | 1 + android/app/build.gradle.kts | 19 +++++++ android/app/src/main/AndroidManifest.xml | 5 ++ desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 5 ++ desktop/ui/package.json | 2 +- desktop/ui/yarn.lock | 8 +-- 9 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/android-release.yml diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 00000000..94382a6a --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,64 @@ +name: Android Release + +# Tag `android-v*` builds the release APK + AAB and publishes them to a +# GitHub release. If ANDROID_KEYSTORE_* secrets are configured the +# artifacts are signed with the real release key; otherwise the debug +# cert is used so APKs remain sideload-installable. +on: + push: + tags: ['android-v*'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android + env: + KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - uses: gradle/actions/setup-gradle@v4 + + - name: Decode release keystore + if: ${{ env.KEYSTORE_B64 != '' }} + run: | + echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.keystore" + { + echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore" + echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" + echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" + echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" + } >> "$GITHUB_ENV" + + - name: Build release APK + AAB + run: ./gradlew :app:assembleRelease :app:bundleRelease --console=plain + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + APK=$(find app/build/outputs/apk/release -name 'app-release*.apk' | head -1) + AAB=$(find app/build/outputs/bundle/release -name 'app-release*.aab' | head -1) + cp "$APK" "cloudreve-${{ github.ref_name }}.apk" + cp "$AAB" "cloudreve-${{ github.ref_name }}.aab" + if [ -n "$ANDROID_KEYSTORE_FILE" ]; then + SIG_NOTE="Signed with the release key." + else + SIG_NOTE="Signed with the debug certificate — sideload-installable. Configure the ANDROID_KEYSTORE_* secrets to sign with a release key." + fi + gh release create "${{ github.ref_name }}" \ + --title "Cloudreve Android ${{ github.ref_name }}" \ + --notes "Cloudreve Android app. ${SIG_NOTE}" \ + "cloudreve-${{ github.ref_name }}.apk" \ + "cloudreve-${{ github.ref_name }}.aab" diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 9f45993c..abe917dc 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -36,7 +36,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev patchelf + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev patchelf libfuse2 fuse3 - name: Install UI dependencies working-directory: desktop/ui diff --git a/ROADMAP.md b/ROADMAP.md index 0b93cfaa..8a1ad499 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -249,6 +249,7 @@ Goal: Windows + macOS + Linux from the `desktop/` tree in this repo. - Status: (1) notifications already per-OS (`win32_notif` / `notify_rust` / `mac_notification_sys`) — no abstraction needed; (2) hydration abstracted via `drive/placeholder` cfg swap — `cfapi` on Windows, `placeholder_non_windows` full-sync adapter elsewhere (FUSE landed on Linux; File Provider still open); (3) CI matrix builds + tests all 3 OSes; (4) packaging: `desktop-release.yml` on `desktop-v*` tags ships .msi/.exe (Windows), .dmg (macOS), .deb/.AppImage (Linux) — MSIX deferred (needs store signing). - Verified on Linux: `cargo test --workspace` green (49 tests), `cargo tauri build` produces working .deb + .AppImage. - Feature fallback on Linux/macOS until providers land: full sync without placeholders (download-on-access still works via sync engine). +- [x] Release pipelines for all four artifacts, all verified locally — server: `release.yml` + goreleaser on `v*` tags (12 binaries linux/win/darwin/freebsd + ghcr docker amd64/arm64/slim + manifests; snapshot build verified); desktop: `desktop-release.yml` on `desktop-v*` tags (.msi/.exe, .dmg, .deb/.AppImage — local `cargo tauri build` produced both Linux bundles, `.deb` declares `fuse3` for on-demand mode, `libfuse2`+`fuse3` added to runner deps for the AppImage toolchain); android: new `android-release.yml` on `android-v*` tags ships signed release APK + AAB (env-keystore via `ANDROID_KEYSTORE_*` secrets, debug-cert fallback so APKs sideload; `lintVitalRelease` caught a real missing `dataSync` foregroundServiceType on `SystemForegroundService` — fixed in manifest, would have crashed API 34+); `tauri-plugin-http` pinned `~2.6` + npm `@tauri-apps/plugin-http@^2.6.1` to clear the version-mismatch guard that blocked every desktop release build - [x] #167 (upstream desktop#49) — online-only thumbnails missing in Explorer: root cause was a client/server contract mismatch — the CE `/file/thumb` response carries only `url`/`expires` while the `cloudreve-api` model required `obfuscated`, failing deserialization on every thumbnail request (`E_FAIL` to Explorer; hydrated files were unaffected since Windows thumbs them locally). `obfuscated` is now `#[serde(default)]`; the decode path still runs when a server emits the flag - [x] Linux on-demand hydration via FUSE (`fuser`) — new "On-demand" sync mode in Add Drive (Linux-only picker): remote tree projects virtually into a FUSE mount at the configured path backed by inventory; files hydrate into a private store (`~/.cloudreve/fuse-store/`) on open with per-path serialization, sibling tmp dir + atomic rename, zero kernel cache TTLs; the watcher/sync engine operate on the store so hydrated edits sync normally; remote refreshes evict stale hydrated copies (event-blocked so no remote delete); engine writes gated by `local_updated_at`/`local_size` snapshots so downloads/hydrations never boomerang into uploads; `unlink`/`rename` on virtual files route through synthesized events + `MountCommand::Rename`; full-sync mode unchanged on Linux/macOS; covered by a real kernel-mount integration test (`tests/fuse_mount.rs`) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1f6cada7..d39ef5c6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -17,11 +17,30 @@ android { versionName = "0.1.0" } + signingConfigs { + // Optional real release key via env (CI secrets). Falls back to the + // debug cert so GitHub-release APKs are sideload-installable. + create("releaseEnv") { + val store = System.getenv("ANDROID_KEYSTORE_FILE") + if (store != null) { + storeFile = file(store) + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") + } + } + } + buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + signingConfig = + if (System.getenv("ANDROID_KEYSTORE_FILE") != null) + signingConfigs.getByName("releaseEnv") + else + signingConfigs.getByName("debug") } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f9527f80..1166f5ca 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -65,6 +65,11 @@ + +