name: Desktop Release on: push: branches: [main] tags: - "v*" paths: - "src/**" - "src-tauri/**" - "package.json" - "app-icon.svg" - "index.html" - "tsconfig.json" - "vite.config.ts" - ".gitea/workflows/desktop-release.yml" workflow_dispatch: permissions: code: read releases: read jobs: bootstrap-updater-key: name: Bootstrap updater signing key runs-on: ubuntu-latest permissions: code: write actions: write outputs: ready: ${{ steps.bootstrap.outputs.ready }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Bootstrap signing key if missing id: bootstrap shell: bash env: EXISTING_SIGNING_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} run: | set -euo pipefail if [ -n "$EXISTING_SIGNING_KEY" ] && ! grep -q '__FIELDLENS_UPDATER_PUBKEY__' src-tauri/tauri.conf.json; then echo "ready=true" >> "$GITHUB_OUTPUT" exit 0 fi npm install npm run tauri signer generate -- --ci -w /tmp/fieldlens-updater.key >/tmp/tauri-signer.log 2>&1 test -s /tmp/fieldlens-updater.key test -s /tmp/fieldlens-updater.key.pub api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}" jq -n --rawfile data /tmp/fieldlens-updater.key \ '{data:$data, description:"FieldLens Print Tauri updater private signing key"}' \ >/tmp/updater-secret.json curl -fsS \ -X PUT \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ --data-binary @/tmp/updater-secret.json \ "$api/actions/secrets/TAURI_SIGNING_PRIVATE_KEY" >/dev/null node <<'NODE' const fs = require('fs') const path = 'src-tauri/tauri.conf.json' const config = JSON.parse(fs.readFileSync(path, 'utf8')) config.plugins.updater.pubkey = fs.readFileSync('/tmp/fieldlens-updater.key.pub', 'utf8').trim() fs.writeFileSync(path, JSON.stringify(config, null, 2) + '\n') NODE git config user.name "FieldLens Desktop CI" git config user.email "ci@noreply.git.arrowstack.dev" git add src-tauri/tauri.conf.json git commit -m "chore: initialize updater public key" git push origin HEAD:main rm -f /tmp/fieldlens-updater.key /tmp/fieldlens-updater.key.pub /tmp/updater-secret.json echo "ready=false" >> "$GITHUB_OUTPUT" windows-nsis: name: Windows x64 NSIS needs: bootstrap-updater-key if: needs.bootstrap-updater-key.outputs.ready == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: dtolnay/rust-toolchain@stable with: targets: x86_64-pc-windows-msvc - name: Install Windows cross-build tools run: | sudo apt-get update sudo apt-get install -y nsis lld llvm clang cargo install --locked cargo-xwin - name: Install Node dependencies run: npm install - name: Verify updater signing configuration shell: bash run: | set -euo pipefail ! grep -q '__FIELDLENS_UPDATER_PUBKEY__' src-tauri/tauri.conf.json test -n "${TAURI_SIGNING_PRIVATE_KEY:-}" env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - name: Build signed Windows installer run: npm run bundle:windows env: XWIN_CACHE_DIR: ${{ runner.temp }}/xwin-cache TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" - name: Stage Windows installer and signature shell: bash run: | set -euo pipefail source_file="$(find src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name '*-setup.exe' -print -quit)" test -n "$source_file" test -s "$source_file.sig" mkdir -p dist cp "$source_file" dist/fieldlens-print-windows-x64-setup.exe cp "$source_file.sig" dist/fieldlens-print-windows-x64-setup.exe.sig - uses: actions/upload-artifact@v3 with: name: fieldlens-print-windows-x64 path: dist/* if-no-files-found: error retention-days: 14 linux: name: Linux x64 DEB + AppImage needs: bootstrap-updater-key if: needs.bootstrap-updater-key.outputs.ready == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: dtolnay/rust-toolchain@stable - name: Install Tauri Linux prerequisites run: | sudo apt-get update sudo apt-get install -y \ build-essential \ curl \ wget \ file \ libwebkit2gtk-4.1-dev \ libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ patchelf - name: Install Node dependencies run: npm install - name: Verify updater signing configuration shell: bash run: | set -euo pipefail ! grep -q '__FIELDLENS_UPDATER_PUBKEY__' src-tauri/tauri.conf.json test -n "${TAURI_SIGNING_PRIVATE_KEY:-}" env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - name: Build signed Linux packages run: npm run bundle:linux env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" - name: Stage Linux packages and updater signature shell: bash run: | set -euo pipefail deb_file="$(find src-tauri/target/release/bundle/deb -maxdepth 1 -type f -name '*.deb' -print -quit)" appimage_file="$(find src-tauri/target/release/bundle/appimage -maxdepth 1 -type f -name '*.AppImage' -print -quit)" test -n "$deb_file" test -n "$appimage_file" test -s "$appimage_file.sig" mkdir -p dist cp "$deb_file" dist/fieldlens-print-linux-x64.deb cp "$appimage_file" dist/fieldlens-print-linux-x64.AppImage cp "$appimage_file.sig" dist/fieldlens-print-linux-x64.AppImage.sig - uses: actions/upload-artifact@v3 with: name: fieldlens-print-linux-x64 path: dist/* if-no-files-found: error retention-days: 14 publish-release: name: Publish public desktop release needs: [bootstrap-updater-key, windows-nsis, linux] if: needs.bootstrap-updater-key.outputs.ready == 'true' runs-on: ubuntu-latest permissions: code: write releases: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - uses: actions/download-artifact@v3 with: name: fieldlens-print-windows-x64 path: release-assets - uses: actions/download-artifact@v3 with: name: fieldlens-print-linux-x64 path: release-assets - name: Build updater manifest shell: bash run: | set -euo pipefail version="$(node -p "require('./package.json').version")" windows_sig="$(cat release-assets/fieldlens-print-windows-x64-setup.exe.sig)" linux_sig="$(cat release-assets/fieldlens-print-linux-x64.AppImage.sig)" base="https://fieldlens.arrowstack.dev/downloads" jq -n \ --arg version "$version" \ --arg windows_sig "$windows_sig" \ --arg linux_sig "$linux_sig" \ --arg windows_url "$base/fieldlens-print-windows-x64-setup.exe" \ --arg linux_url "$base/fieldlens-print-linux-x64.AppImage" \ '{ version: $version, notes: "FieldLens Print desktop update", platforms: { "windows-x86_64": {signature:$windows_sig, url:$windows_url}, "linux-x86_64": {signature:$linux_sig, url:$linux_url} } }' > release-assets/latest.json windows_sha="$(sha256sum release-assets/fieldlens-print-windows-x64-setup.exe | awk '{print $1}')" linux_deb_sha="$(sha256sum release-assets/fieldlens-print-linux-x64.deb | awk '{print $1}')" jq -n \ --arg version "$version" \ --arg windows_sha "$windows_sha" \ --arg linux_deb_sha "$linux_deb_sha" \ --arg windows_url "https://fieldlens.arrowstack.dev/downloads/fieldlens-print-windows-x64-setup.exe" \ --arg linux_deb_url "https://fieldlens.arrowstack.dev/downloads/fieldlens-print-linux-x64.deb" \ '{ version: $version, notes: "FieldLens Print desktop update", downloads: { "windows-x86_64": {url:$windows_url, sha256:$windows_sha}, "linux-x86_64-deb": {url:$linux_deb_url, sha256:$linux_deb_sha} } }' > release-assets/release.json - name: Update stable public downloads shell: bash run: | set -euo pipefail mkdir -p downloads cp release-assets/fieldlens-print-windows-x64-setup.exe downloads/ cp release-assets/fieldlens-print-windows-x64-setup.exe.sig downloads/ cp release-assets/fieldlens-print-linux-x64.deb downloads/ cp release-assets/fieldlens-print-linux-x64.AppImage downloads/ cp release-assets/fieldlens-print-linux-x64.AppImage.sig downloads/ cp release-assets/latest.json downloads/latest.json cp release-assets/release.json downloads/release.json deb_sha="$(sha256sum downloads/fieldlens-print-linux-x64.deb | awk '{print $1}')" sed -E "s/EXPECTED_SHA256=\"[0-9a-f]{64}\"/EXPECTED_SHA256=\"$deb_sha\"/" downloads/install.sh > /tmp/install.sh mv /tmp/install.sh downloads/install.sh sha256sum \ downloads/fieldlens-print-linux-x64.deb \ downloads/fieldlens-print-linux-x64.AppImage \ downloads/fieldlens-print-windows-x64-setup.exe \ | sed 's#downloads/##' > downloads/SHA256SUMS git config user.name "FieldLens Desktop CI" git config user.email "ci@noreply.git.arrowstack.dev" git add downloads if ! git diff --cached --quiet; then git commit -m "release: publish FieldLens Print $(node -p "require('./package.json').version") downloads" git push origin HEAD:main fi - name: Publish Gitea release assets shell: bash env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SHA: ${{ gitea.sha }} run: | set -euo pipefail version="$(node -p "require('./package.json').version")" release_tag="v$version" api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}" auth_header="Authorization: token ${GITEA_TOKEN}" status="$(curl -sS -o /tmp/release.json -w '%{http_code}' -H "$auth_header" "$api/releases/tags/$release_tag")" if [ "$status" = "404" ]; then jq -n \ --arg tag "$release_tag" \ --arg sha "$GITEA_SHA" \ '{ tag_name:$tag, target_commitish:$sha, name:("FieldLens Print " + $tag), body:"Signed FieldLens Print desktop installers and updater artifacts.", draft:false, prerelease:false }' > /tmp/create-release.json curl -fsS -X POST -H "$auth_header" -H "Content-Type: application/json" \ --data-binary @/tmp/create-release.json "$api/releases" > /tmp/release.json elif [ "$status" != "200" ]; then cat /tmp/release.json exit 1 fi release_id="$(jq -r '.id' /tmp/release.json)" curl -fsS -H "$auth_header" "$api/releases/$release_id/assets" > /tmp/assets.json for file in \ release-assets/fieldlens-print-windows-x64-setup.exe \ release-assets/fieldlens-print-windows-x64-setup.exe.sig \ release-assets/fieldlens-print-linux-x64.deb \ release-assets/fieldlens-print-linux-x64.AppImage \ release-assets/fieldlens-print-linux-x64.AppImage.sig \ release-assets/latest.json \ release-assets/release.json do name="$(basename "$file")" existing_id="$(jq -r --arg name "$name" '.[] | select(.name == $name) | .id' /tmp/assets.json | head -n 1)" if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then curl -fsS -X DELETE -H "$auth_header" "$api/releases/$release_id/assets/$existing_id" fi curl -fsS -X POST -H "$auth_header" -F "attachment=@$file" "$api/releases/$release_id/assets?name=$name" done