name: Build & Release on: push: tags: - 'v*' workflow_dispatch: inputs: version: description: 'Version to build (e.g., v1.0.0)' required: false env: RUST_BACKTRACE: 1 CARGO_TERM_COLOR: always # Incremental state is never reused between CI runs -- pure disk cost. CARGO_INCREMENTAL: 0 jobs: test: name: Run Tests runs-on: linux/amd64 container: image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Cache Rust dependencies uses: actions/cache@v3 with: # Registry only -- never src-tauri/target. That directory is ~16 GB and # was cached under five separate keys, which filled the runner's 74 GB # disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026). # registry/src is omitted too: cargo re-extracts it for free from # registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted). path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db # One shared key across every job. The old per-job keys existed to stop # debug/release target artifacts clobbering each other; with target no # longer cached, registry contents are target-independent, so all jobs # want the same crates. First job to finish saves; the rest restore. key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-registry- - name: Cache Node dependencies uses: actions/cache@v3 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install - name: Run frontend tests run: | bunx svelte-kit sync bun run test --run continue-on-error: false # Same gate as build-and-test.yml. A release must not ship from a tree # that would fail the per-commit checks. rustfmt/clippy come from the # builder image; nothing is installed here. - name: Check Rust formatting run: | cd src-tauri cargo fmt --all -- --check continue-on-error: false # Advisory until the ~51 pre-existing warnings are cleared; see the longer # note in build-and-test.yml. Tighten both to `-- -D warnings` together. - name: Run clippy (advisory) run: | cd src-tauri cargo clippy --all-targets - name: Run Rust tests run: bun run test:rust continue-on-error: false - name: Check TypeScript run: bun run check continue-on-error: false build-linux: name: Build Linux runs-on: linux/amd64 needs: test container: image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Cache Rust dependencies uses: actions/cache@v3 with: # Registry only -- never src-tauri/target. That directory is ~16 GB and # was cached under five separate keys, which filled the runner's 74 GB # disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026). # registry/src is omitted too: cargo re-extracts it for free from # registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted). path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db # One shared key across every job. The old per-job keys existed to stop # debug/release target artifacts clobbering each other; with target no # longer cached, registry contents are target-independent, so all jobs # want the same crates. First job to finish saves; the rest restore. key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-registry- - name: Cache Node dependencies uses: actions/cache@v3 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install # The Linux job previously had no version step at all, so a tagged release # built Linux packages from whatever version happened to be committed. - name: Set app version from tag run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}" if: startsWith(github.ref, 'refs/tags/v') - name: Build for Linux run: bun run tauri build env: TAURI_SKIP_UPDATER: true - name: Prepare Linux artifacts run: | mkdir -p dist/linux # Match by extension, not by product name. Bundle filenames follow # `productName`, so renaming the app (jellytau -> JellyTau) made the # old `jellytau_*.deb` glob match nothing — and because the copy was # wrapped in `if [ -f ... ]`, the artifact simply vanished from the # release with no error. Each bundle directory holds one file. # # `if [ -f "dir/"*.ext ]` was also wrong on its own terms: with more # than one match `test` gets extra arguments and fails. # # No `shopt -s nullglob` here: the runner executes `run:` blocks with # POSIX sh, where shopt does not exist -- it exited 127 and killed the # step (which is why v0.9.0 and v0.9.1 built but never published). # Without nullglob an unmatched pattern stays literal, so test each # candidate instead. Same POSIX-only rule as traceability-check.yml. for bundle in \ src-tauri/target/release/bundle/appimage/*.AppImage \ src-tauri/target/release/bundle/deb/*.deb \ src-tauri/target/release/bundle/rpm/*.rpm; do [ -e "$bundle" ] || continue cp -v "$bundle" dist/linux/ done # A release with no Linux package is a failure, not a quiet success. if [ -z "$(ls -A dist/linux/)" ]; then echo "::error::No Linux bundles found under src-tauri/target/release/bundle/" exit 1 fi ls -lah dist/linux/ - name: Upload Linux build artifact uses: actions/upload-artifact@v3 with: name: jellytau-linux path: dist/linux/ retention-days: 7 build-windows: name: Build Windows runs-on: linux/amd64 needs: test # Cross-compiled from Linux via the official Tauri path (MSVC + cargo-xwin), # baked into the builder image. No toolchain installs here — the image has # cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target. container: image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Cache Rust dependencies uses: actions/cache@v3 with: # Registry only -- never src-tauri/target. That directory is ~16 GB and # was cached under five separate keys, which filled the runner's 74 GB # disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026). # registry/src is omitted too: cargo re-extracts it for free from # registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted). path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db # One shared key across every job. The old per-job keys existed to stop # debug/release target artifacts clobbering each other; with target no # longer cached, registry contents are target-independent, so all jobs # want the same crates. First job to finish saves; the rest restore. key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-registry- - name: Cache Windows CRT/SDK (cargo-xwin) uses: actions/cache@v3 with: path: ~/.cache/cargo-xwin # Contents track the xwin version baked into the builder image, not our # lockfile -- keying this on Cargo.lock re-downloaded the whole SDK on # every release bump. Bump the suffix by hand if the image's xwin moves. key: ${{ runner.os }}-cargo-xwin-v1 - name: Cache Node dependencies uses: actions/cache@v3 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} restore-keys: | ${{ runner.os }}-bun- # The tag is the single source of truth for a release version; the script # stamps every file that carries it (package.json, tauri.conf.json, # Cargo.toml, Cargo.lock). This step used to sed only tauri.conf.json, so # the other three shipped whatever was committed. - name: Set app version from tag run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}" if: startsWith(github.ref, 'refs/tags/v') - name: Build Windows (NSIS installer + exe) run: OUTPUT_DIR="$PWD/dist/windows" WIN_BUNDLES=nsis ./scripts/build-windows-cross.sh - name: List Windows artifacts run: ls -lah dist/windows/ - name: Upload Windows build artifact uses: actions/upload-artifact@v3 with: name: jellytau-windows path: dist/windows/ retention-days: 7 build-android: name: Build Android runs-on: linux/amd64 needs: test container: image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest env: ANDROID_HOME: /opt/android-sdk ANDROID_SDK_ROOT: /opt/android-sdk ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Cache Rust dependencies uses: actions/cache@v3 with: # Registry only -- never src-tauri/target. That directory is ~16 GB and # was cached under five separate keys, which filled the runner's 74 GB # disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026). # registry/src is omitted too: cargo re-extracts it for free from # registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted). path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db # One shared key across every job. The old per-job keys existed to stop # debug/release target artifacts clobbering each other; with target no # longer cached, registry contents are target-independent, so all jobs # want the same crates. First job to finish saves; the rest restore. key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-registry- - name: Cache Node dependencies uses: actions/cache@v3 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} restore-keys: | ${{ runner.os }}-bun- - name: Install dependencies run: bun install # Stamp before `android init`: it derives its generated project (including # the initial versionCode) from tauri.conf.json. - name: Set app version from tag run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}" if: startsWith(github.ref, 'refs/tags/v') - name: Initialize Android project run: bun run tauri android init # Re-run after init: tauri.properties only exists now, and its # autogenerated versionCode (0.0.15 -> 15) is both tiny and NOT monotonic # against the 1000 floor already shipped in the field. The script rewrites # it as 1000 + major*10000 + minor*100 + patch. Runs unconditionally so # untagged builds get a sane code too, derived from git describe. - name: Pin a monotonic Android versionCode run: ./scripts/set-version.sh "${GITHUB_REF#refs/tags/}" - name: Sync custom Android sources & gradle config run: ./scripts/sync-android-sources.sh - name: Write signing keystore run: | echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/jellytau-release.jks" cat > src-tauri/gen/android/keystore.properties <> $GITHUB_OUTPUT echo "RELEASE_NAME=JellyTau ${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Download Linux artifacts uses: actions/download-artifact@v3 with: name: jellytau-linux path: artifacts/linux/ - name: Download Windows artifacts uses: actions/download-artifact@v3 with: name: jellytau-windows path: artifacts/windows/ - name: Download Android artifacts uses: actions/download-artifact@v3 with: name: jellytau-android path: artifacts/android/ - name: Prepare release notes id: release_notes run: | VERSION="${{ steps.tag_name.outputs.VERSION }}" echo "## JellyTau $VERSION Release" > release_notes.md echo "" >> release_notes.md echo "### Downloads" >> release_notes.md echo "" >> release_notes.md echo "#### Linux" >> release_notes.md echo "- **AppImage** - Run directly on most Linux distributions" >> release_notes.md echo "- **DEB** - Install via \`sudo dpkg -i JellyTau_*.deb\` (Ubuntu/Debian)" >> release_notes.md echo "- **RPM** - Install via \`sudo rpm -i JellyTau-*.rpm\` (Fedora/openSUSE)" >> release_notes.md echo "" >> release_notes.md echo "#### Windows" >> release_notes.md echo "- **Installer (.exe)** - Run \`JellyTau_*-setup.exe\` (NSIS). Unsigned — SmartScreen may warn on first run." >> release_notes.md echo "" >> release_notes.md echo "#### Android" >> release_notes.md echo "- **APK** - Install via \`adb install jellytau-release.apk\` or sideload via file manager" >> release_notes.md echo "- **AAB** - Upload to Google Play Console or testing platforms" >> release_notes.md echo "" >> release_notes.md echo "### What's New" >> release_notes.md echo "" >> release_notes.md echo "See [CHANGELOG.md](CHANGELOG.md) for detailed changes." >> release_notes.md echo "" >> release_notes.md echo "### Installation" >> release_notes.md echo "" >> release_notes.md echo "#### Linux (AppImage)" >> release_notes.md echo "\`\`\`bash" >> release_notes.md echo "chmod +x JellyTau_*.AppImage" >> release_notes.md echo "./JellyTau_*.AppImage" >> release_notes.md echo "\`\`\`" >> release_notes.md echo "" >> release_notes.md echo "#### Linux (DEB)" >> release_notes.md echo "\`\`\`bash" >> release_notes.md echo "sudo dpkg -i JellyTau_*.deb" >> release_notes.md echo "jellytau" >> release_notes.md echo "\`\`\`" >> release_notes.md echo "" >> release_notes.md echo "#### Android" >> release_notes.md echo "- Sideload: Download APK and install via file manager or ADB" >> release_notes.md echo "- Play Store: Coming soon" >> release_notes.md echo "" >> release_notes.md echo "### Known Issues" >> release_notes.md echo "" >> release_notes.md echo "See [GitHub Issues](../../issues) for reported bugs." >> release_notes.md echo "" >> release_notes.md echo "### Requirements" >> release_notes.md echo "" >> release_notes.md echo "**Linux:**" >> release_notes.md echo "- 64-bit Linux system" >> release_notes.md echo "- GLIBC 2.29+" >> release_notes.md echo "" >> release_notes.md echo "**Android:**" >> release_notes.md echo "- Android 8.0 or higher" >> release_notes.md echo "- 50MB free storage" >> release_notes.md echo "" >> release_notes.md echo "---" >> release_notes.md echo "Built with Tauri, SvelteKit, and Rust" >> release_notes.md - name: Publish Gitea release & upload assets env: # GITEA_TOKEN (a PAT) is preferred; falls back to the auto-provided token. GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e command -v jq >/dev/null || { echo "❌ jq is required on the runner"; exit 1; } VERSION="${{ steps.tag_name.outputs.VERSION }}" API="${GITHUB_SERVER_URL}/api/v1" REPO="${GITHUB_REPOSITORY}" TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}" case "$VERSION" in *rc*|*beta*|*alpha*) PRE=true;; *) PRE=false;; esac PAYLOAD=$(jq -n \ --arg tag "$VERSION" \ --arg name "JellyTau $VERSION" \ --rawfile body release_notes.md \ --argjson pre "$PRE" \ '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:$pre}') echo "đŸ“Ļ Creating release $VERSION on $REPO" # -f drops on HTTP error; capture status so an existing release (409) is handled gracefully. HTTP=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/repos/$REPO/releases" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ -d "$PAYLOAD") if [ "$HTTP" = "201" ]; then RELEASE_ID=$(jq -r '.id' resp.json) elif [ "$HTTP" = "409" ]; then echo "â„šī¸ Release $VERSION already exists; fetching its id to upload assets" RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$VERSION" \ -H "Authorization: token $TOKEN" | jq -r '.id') else echo "❌ Failed to create release (HTTP $HTTP):"; cat resp.json; exit 1 fi echo "Release id=$RELEASE_ID" for f in artifacts/android/* artifacts/linux/* artifacts/windows/*; do [ -f "$f" ] || continue echo "âŦ†ī¸ Uploading $(basename "$f")" curl -fsS -X POST \ "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \ -H "Authorization: token $TOKEN" \ -F "attachment=@$f" >/dev/null done echo "✅ Release $VERSION published with assets"