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>
pull/3593/head
Tomas Dvorak 2 weeks ago
parent f81ebbd1da
commit 3ac4eb7b82

@ -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"

@ -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

@ -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/<id>`) 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`)

@ -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")
}
}

@ -65,6 +65,11 @@
</intent-filter>
</service>
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<provider
android:name=".provider.CloudreveDocumentsProvider"
android:authorities="${applicationId}.documents"

@ -38,7 +38,7 @@ urlencoding = "2.1"
# Main sync service crate
cloudreve-sync = { path = "../crates/cloudreve-sync" }
tauri-plugin-http = "2"
tauri-plugin-http = "~2.6"
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2.4.9"
tauri-plugin-dialog = "2.7.1"

@ -39,6 +39,11 @@
"bundle": {
"active": true,
"targets": "all",
"linux": {
"deb": {
"depends": ["fuse3"]
}
},
"icon": [
"icons/32x32.png",
"icons/128x128.png",

@ -18,7 +18,7 @@
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-deep-link": "^2.4.9",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-http": "^2.6.1",
"@tauri-apps/plugin-opener": "^2.5.4",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-positioner": "^2.3.2",

@ -835,10 +835,10 @@
dependencies:
"@tauri-apps/api" "^2.11.0"
"@tauri-apps/plugin-http@^2.5.9":
version "2.5.9"
resolved "https://registry.npmmirror.com/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz#f612a86239b95f6b2d5d211e26d512176c9f490b"
integrity sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==
"@tauri-apps/plugin-http@^2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-http/-/plugin-http-2.6.1.tgz#1d384f2ef194b528c807ec972f7fd4b8e4f1d79b"
integrity sha512-9gtfe6eOaVKofASoMZ4Ph8E6vDYnpUTbMXrLt4FkXPzC5+O+wnGqJ9fz2koSPYcQt10bb3cx5qHkU0AU1l8tcQ==
dependencies:
"@tauri-apps/api" "^2.11.0"

Loading…
Cancel
Save