name: Build Lotus Chat Desktop on: push: branches: [main] workflow_dispatch: env: GITEA_URL: https://code.lotusguild.org REPO: LotusGuild/cinny-desktop jobs: prepare: runs-on: ubuntu-latest outputs: version: ${{ steps.ver.outputs.version }} release_id: ${{ steps.release.outputs.release_id }} steps: - name: Compute version id: ver run: echo "version=4.12.${{ github.run_number }}" >> $GITHUB_OUTPUT - name: Create or update release id: release env: TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | VERSION="4.12.${{ github.run_number }}" EXISTING=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/tags/latest" \ -H "Authorization: token $TOKEN" 2>/dev/null || true) RELEASE_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ] && [ "$RELEASE_ID" != "" ]; then curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\":\"Lotus Chat $VERSION\",\"body\":\"Built from ${{ github.sha }}\"}" > /dev/null else RELEASE_ID=$(curl -sf -X POST "$GITEA_URL/api/v1/repos/$REPO/releases" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"latest\",\"name\":\"Lotus Chat $VERSION\",\"prerelease\":true,\"body\":\"Built from ${{ github.sha }}\"}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") fi echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT build-windows: needs: prepare runs-on: windows steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file: .node-version - name: Checkout submodules (shallow) shell: powershell run: git submodule update --init --depth=1 - name: Patch version shell: powershell run: | $ver = '${{ needs.prepare.outputs.version }}' node -e "const fs=require('fs');const d=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));d.version='$ver';fs.writeFileSync('src-tauri/tauri.conf.json',JSON.stringify(d,null,2),'utf8');" - uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - name: Install frontend deps shell: powershell run: cd cinny; npm ci - name: Install Tauri deps shell: powershell run: npm ci - name: Build shell: powershell env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' NODE_OPTIONS: '--max_old_space_size=4096' # Sparse registry avoids a full git clone of the crates.io index — # eliminates the curl SSL handshake failures seen on Windows runners. CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse # Disable HTTP/2 multiplexing — ALPN negotiation can reset on Windows Schannel. CARGO_HTTP_MULTIPLEXING: 'false' # Retry transient network errors before failing. CARGO_NET_RETRY: '5' run: | # USERPROFILE is set by Windows directly; more reliable than C:\Users\$USERNAME $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" # Also add the actual toolchain bin to bypass rustup shim execution issues $toolchain = Get-ChildItem "$env:USERPROFILE\.rustup\toolchains" -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'stable' } | Select-Object -First 1 if ($toolchain) { $env:PATH = "$($toolchain.FullName)\bin;$env:PATH" } Write-Host "cargo: $((Get-Command cargo -ErrorAction SilentlyContinue).Source)" cargo --version npm run tauri -- build --bundles nsis - name: Upload to release shell: powershell env: TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_ID: ${{ needs.prepare.outputs.release_id }} VERSION: ${{ needs.prepare.outputs.version }} run: | $releaseId = $env:RELEASE_ID $VERSION = $env:VERSION Write-Host "Version: $VERSION Release: $releaseId" $nsis = "src-tauri\target\release\bundle\nsis" $files = @( "$nsis\Lotus Chat_${VERSION}_x64-setup.exe", "$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip", "$nsis\Lotus Chat_${VERSION}_x64-setup.nsis.zip.sig" ) $names = @("LotusChat-x86_64-setup.exe", "LotusChat-x86_64-setup.nsis.zip", "LotusChat-x86_64-setup.nsis.zip.sig") for ($i = 0; $i -lt $files.Length; $i++) { $existing = (Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets" ` -Headers @{ Authorization = "token $env:TOKEN" }) | Where-Object { $_.name -eq $names[$i] } if ($existing) { Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets/$($existing.id)" ` -Method Delete -Headers @{ Authorization = "token $env:TOKEN" } } $bytes = [System.IO.File]::ReadAllBytes($files[$i]) Invoke-RestMethod -Uri "$env:GITEA_URL/api/v1/repos/$env:REPO/releases/$releaseId/assets?name=$($names[$i])" ` -Method Post ` -Headers @{ Authorization = "token $env:TOKEN"; "Content-Type" = "application/octet-stream" } ` -Body $bytes } build-linux: needs: prepare runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - uses: actions/setup-node@v4 with: node-version-file: .node-version - name: Install system deps run: | apt-get update apt-get install -y \ curl wget file gcc imagemagick \ libwebkit2gtk-4.1-dev \ libssl-dev \ libxdo-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ patchelf \ xdg-utils \ squashfs-tools - name: Ensure icons are RGBA PNG run: | for f in src-tauri/icons/*.png; do info=$(identify -verbose "$f" 2>/dev/null | grep "Type:" | head -1) echo "$f: $info" convert "$f" -type TrueColorAlpha PNG32:"$f" done - name: Set up Rust toolchain run: | source "$HOME/.cargo/env" 2>/dev/null || true if command -v cargo >/dev/null 2>&1; then echo "Using existing Rust: $(cargo --version)" else curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal source "$HOME/.cargo/env" fi echo "$HOME/.cargo/bin" >> $GITHUB_PATH - uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - name: Patch version run: python3 -c "import json; d=json.load(open('src-tauri/tauri.conf.json')); d['version']='${{ needs.prepare.outputs.version }}'; open('src-tauri/tauri.conf.json','w').write(json.dumps(d,indent=2))" - name: Install frontend deps run: cd cinny && npm ci - name: Install Tauri deps run: npm ci - name: Stage AppRun and linuxdeploy for AppImage bundler run: | set -e mkdir -p ~/.cache/tauri cp tools/AppRun-x86_64 ~/.cache/tauri/AppRun-x86_64 chmod +x ~/.cache/tauri/AppRun-x86_64 wget -q \ "https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-x86_64.AppImage" \ -O /tmp/linuxdeploy.AppImage chmod +x /tmp/linuxdeploy.AppImage rm -rf /root/linuxdeploy-root (cd /root && /tmp/linuxdeploy.AppImage --appimage-extract) mv /root/squashfs-root /root/linuxdeploy-root echo "Extracted linuxdeploy:" ls /root/linuxdeploy-root/ ls /root/linuxdeploy-root/usr/bin/ 2>/dev/null || echo "no usr/bin" # Pre-stage plugin scripts next to linuxdeploy so it finds them via /proc/self/exe lookup wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" \ -O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh wget -q "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \ -O /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh chmod +x /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gtk.sh \ /root/linuxdeploy-root/usr/bin/linuxdeploy-plugin-gstreamer.sh gcc -o ~/.cache/tauri/linuxdeploy-x86_64.AppImage tools/ld_wrapper.c chmod +x ~/.cache/tauri/linuxdeploy-x86_64.AppImage - name: Build env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' NODE_OPTIONS: '--max_old_space_size=4096' RUST_LOG: tauri_bundler=debug run: npm run tauri -- build --bundles appimage,deb - name: Show linuxdeploy wrapper log if: always() run: cat /tmp/ld-wrapper.log 2>/dev/null || echo "no wrapper log found" - name: Upload to release env: TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_ID: ${{ needs.prepare.outputs.release_id }} VERSION: ${{ needs.prepare.outputs.version }} run: | APPIMAGE_DIR="src-tauri/target/release/bundle/appimage" DEB_DIR="src-tauri/target/release/bundle/deb" upload() { local name="$1" path="$2" local existing_id existing_id=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \ -H "Authorization: token $TOKEN" \ | python3 -c "import sys,json; assets=json.load(sys.stdin); print(next((str(a['id']) for a in assets if a['name']=='$name'), ''))" 2>/dev/null || true) if [ -n "$existing_id" ]; then curl -sf -X DELETE "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$existing_id" \ -H "Authorization: token $TOKEN" || true fi echo "Uploading $name" curl -sf -X POST \ "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @"$path" } upload "LotusChat-x86_64.AppImage" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage" upload "LotusChat-x86_64.AppImage.tar.gz" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz" upload "LotusChat-x86_64.AppImage.tar.gz.sig" "$APPIMAGE_DIR/Lotus Chat_${VERSION}_amd64.AppImage.tar.gz.sig" upload "LotusChat-x86_64.deb" "$DEB_DIR/Lotus Chat_${VERSION}_amd64.deb" update-manifest: needs: [prepare, build-windows, build-linux] runs-on: ubuntu-latest steps: - name: Generate and upload release.json env: TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_ID: ${{ needs.prepare.outputs.release_id }} VERSION: ${{ needs.prepare.outputs.version }} run: | BASE="$GITEA_URL/LotusGuild/cinny-desktop/releases/download/latest" WIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64-setup.nsis.zip.sig") LIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64.AppImage.tar.gz.sig") DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) python3 -c "import json,sys; v,d,wu,ws,lu,ls=sys.argv[1:]; print(json.dumps({'version':v,'notes':'Latest Lotus Chat release','pub_date':d,'platforms':{'windows-x86_64':{'url':wu,'signature':ws},'linux-x86_64':{'url':lu,'signature':ls}}},indent=2))" \ "$VERSION" "$DATE" \ "$BASE/LotusChat-x86_64-setup.nsis.zip" "$WIN_SIG" \ "$BASE/LotusChat-x86_64.AppImage.tar.gz" "$LIN_SIG" \ > release.json cat release.json OLD=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets" \ -H "Authorization: token $TOKEN" \ | python3 -c "import sys,json; print(next((str(a['id']) for a in json.load(sys.stdin) if a['name']=='release.json'), ''))" 2>/dev/null || true) [ -n "$OLD" ] && curl -sf -X DELETE \ "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets/$OLD" \ -H "Authorization: token $TOKEN" || true curl -sf -X POST \ "$GITEA_URL/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=release.json" \ -H "Authorization: token $TOKEN" \ -H "Content-Type: application/json" \ --data-binary @release.json