fix: avoid empty apple signing env in release #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Auto-generate Tauri updater metadata for GitHub Releases | |
| name: Release with Updater Metadata | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write | |
| jobs: | |
| create-release: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_id: ${{ steps.create-release.outputs.result }} | |
| steps: | |
| - name: get version from tag | |
| run: echo "PACKAGE_VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV | |
| - name: create or get release | |
| id: create-release | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const tag = `v${process.env.PACKAGE_VERSION}`; | |
| // Try to find existing release first | |
| try { | |
| const { data } = await github.rest.repos.getReleaseByTag({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag: tag, | |
| }); | |
| console.log(`Found existing release: ${data.id}`); | |
| return data.id; | |
| } catch (e) { | |
| // Release doesn't exist, create it | |
| const { data } = await github.rest.repos.createRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag_name: tag, | |
| name: tag, | |
| body: '⏳ Build in progress...', | |
| generate_release_notes: false, | |
| draft: true, | |
| prerelease: false, | |
| }); | |
| console.log(`Created new release: ${data.id}`); | |
| return data.id; | |
| } | |
| build-tauri: | |
| needs: create-release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # macOS - Universal Binary (supports both Intel and Apple Silicon) | |
| - platform: macos-14 | |
| args: '--target universal-apple-darwin' | |
| target: 'aarch64-apple-darwin,x86_64-apple-darwin' | |
| # Linux - x64 | |
| - platform: ubuntu-22.04 | |
| args: '' | |
| target: '' | |
| # Windows - x64 | |
| - platform: windows-latest | |
| args: '' | |
| target: '' | |
| runs-on: ${{ matrix.platform }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: install just | |
| uses: extractions/setup-just@v2 | |
| - name: install pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: setup node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: 'pnpm' | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: install dependencies (ubuntu only) | |
| if: matrix.platform == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf | |
| - name: verify updater signing secret | |
| shell: bash | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| run: | | |
| if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then | |
| echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for signed updater artifacts." | |
| echo "::error::Add it in GitHub repository settings before pushing a release tag." | |
| exit 1 | |
| fi | |
| - name: install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: install macOS universal Rust targets | |
| if: matrix.platform == 'macos-14' | |
| run: rustup target add aarch64-apple-darwin x86_64-apple-darwin | |
| - name: cache Rust dependencies | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: './src-tauri -> target' | |
| add-rust-environment-hash-key: 'false' | |
| key: ${{ hashFiles('src-tauri/Cargo.toml', 'src-tauri/.cargo/config.toml') }} | |
| cache-workspace-crates: 'true' | |
| cache-on-failure: 'true' | |
| - name: install frontend dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline | |
| - uses: tauri-apps/tauri-action@v0 | |
| id: tauri-build | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| with: | |
| # Linux: skip auto-upload so we can post-process the AppImage first | |
| releaseId: ${{ matrix.platform != 'ubuntu-22.04' && needs.create-release.outputs.release_id || '' }} | |
| args: ${{ matrix.args }} | |
| updaterJsonPreferNsis: false | |
| includeUpdaterJson: false | |
| # Create portable Windows zip (no installer required) | |
| - name: Create portable Windows zip | |
| if: matrix.platform == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| # Derive version from git tag (same source as create-release job) | |
| $version = "${{ github.ref_name }}" -replace '^v', '' | |
| if (-not $version) { | |
| Write-Error "Could not determine version from github.ref_name" | |
| exit 1 | |
| } | |
| # Dynamically find the main exe, excluding setup/installer/uninstall binaries | |
| $candidates = @(Get-ChildItem "src-tauri/target/release/*.exe" | | |
| Where-Object { $_.Name -notmatch '(?i)(setup|install|uninstall)' }) | |
| if ($candidates.Count -ne 1) { | |
| Write-Error "Expected exactly 1 app exe, found $($candidates.Count): $($candidates.Name -join ', ')" | |
| exit 1 | |
| } | |
| $exe = $candidates[0] | |
| Write-Output "Found exe: $($exe.FullName)" | |
| $zipName = "EchoProfile_${version}_x64-portable.zip" | |
| Compress-Archive -Path $exe.FullName -DestinationPath $zipName | |
| if (-not (Test-Path $zipName)) { | |
| Write-Error "Failed to create portable zip" | |
| exit 1 | |
| } | |
| Write-Output "Created portable zip: $zipName" | |
| Write-Output "PORTABLE_ZIP_PATH=$zipName" >> $env:GITHUB_ENV | |
| - name: Upload portable Windows zip to release | |
| if: matrix.platform == 'windows-latest' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| gh release upload "${{ github.ref_name }}" ` | |
| "${{ env.PORTABLE_ZIP_PATH }}" ` | |
| --repo "${{ github.repository }}" ` | |
| --clobber | |
| # Fix AppImage EGL crash on Arch Linux and other rolling-release distros. | |
| # | |
| # The AppImage bundles Ubuntu-compiled EGL/Mesa/Wayland libs, but does NOT | |
| # bundle WebKitGPUProcess. The system's WebKitGPUProcess inherits AppRun's | |
| # LD_LIBRARY_PATH, loads the bundled (incompatible) EGL, and crashes with | |
| # EGL_BAD_ALLOC. Removing these GPU-driver-dependent libs forces the system's | |
| # native EGL stack to be used, which is compatible with the system's WebKitGPUProcess. | |
| # | |
| # See: https://github.com/3kyou1/EchoProfile/issues | |
| # See: https://github.com/tauri-apps/tauri/issues/11988 | |
| - name: Post-process AppImage (fix EGL crash on Arch Linux) | |
| if: matrix.platform == 'ubuntu-22.04' | |
| run: | | |
| set -euo pipefail | |
| # Find the built AppImage | |
| APPIMAGE=$(find src-tauri/target/release/bundle/appimage -name '*.AppImage' -not -name '*.sig' | head -1) | |
| if [ -z "$APPIMAGE" ]; then | |
| echo "::error::No AppImage found in bundle output" | |
| exit 1 | |
| fi | |
| echo "Found AppImage: $APPIMAGE" | |
| # Extract AppImage contents | |
| rm -rf squashfs-root | |
| chmod +x "$APPIMAGE" | |
| "$APPIMAGE" --appimage-extract | |
| # Remove GPU-driver-dependent libs that cause ABI mismatch with host system. | |
| # These Ubuntu-compiled libs conflict with the host's Mesa/GPU drivers when | |
| # the system's WebKitGPUProcess loads them via inherited LD_LIBRARY_PATH. | |
| echo "Removing conflicting EGL/Mesa/Wayland libs..." | |
| pushd squashfs-root/usr/lib/ > /dev/null | |
| rm -fv libEGL.so* libEGL_mesa.so* libGLESv2.so* libgbm.so* \ | |
| libwayland-client.so* libwayland-server.so* libwayland-egl.so* | |
| popd > /dev/null | |
| # Inject WEBKIT_DISABLE_DMABUF_RENDERER into GTK apprun hook as defense-in-depth | |
| HOOK_FILE="squashfs-root/apprun-hooks/linuxdeploy-plugin-gtk.sh" | |
| if [ -f "$HOOK_FILE" ]; then | |
| echo '' >> "$HOOK_FILE" | |
| echo '# Fix WebKitGTK EGL crash on rolling-release distros (issue #186)' >> "$HOOK_FILE" | |
| echo 'export WEBKIT_DISABLE_DMABUF_RENDERER=1' >> "$HOOK_FILE" | |
| echo "Injected WEBKIT_DISABLE_DMABUF_RENDERER into apprun hook" | |
| else | |
| echo "::warning::GTK apprun hook not found at $HOOK_FILE" | |
| fi | |
| # Download appimagetool for repackaging (pinned to stable release 1.9.1) | |
| APPIMAGETOOL_SHA256="ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0" | |
| wget -q "https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage" | |
| echo "${APPIMAGETOOL_SHA256} appimagetool-x86_64.AppImage" | sha256sum -c - | |
| chmod +x appimagetool-x86_64.AppImage | |
| # Repackage (APPIMAGE_EXTRACT_AND_RUN bypasses FUSE requirement on CI) | |
| APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 ./appimagetool-x86_64.AppImage squashfs-root "$APPIMAGE" | |
| # Cleanup | |
| rm -rf squashfs-root appimagetool-x86_64.AppImage | |
| echo "AppImage post-processing complete: $APPIMAGE" | |
| - name: Sign and upload post-processed Linux artifacts | |
| if: matrix.platform == 'ubuntu-22.04' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| # Find all Linux bundle artifacts | |
| BUNDLE_DIR="src-tauri/target/release/bundle" | |
| RELEASE_TAG="${GITHUB_REF_NAME}" | |
| # Upload AppImage + re-sign | |
| APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name '*.AppImage' -not -name '*.sig' | head -1) | |
| if [ -n "$APPIMAGE" ]; then | |
| # Re-sign for Tauri updater (the post-processed AppImage has a different checksum) | |
| # tauri signer sign auto-creates ${APPIMAGE}.sig on disk — no stdout redirect | |
| pnpm tauri signer sign \ | |
| --private-key "$TAURI_SIGNING_PRIVATE_KEY" \ | |
| --password "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \ | |
| "$APPIMAGE" | |
| [ -s "${APPIMAGE}.sig" ] || { echo "::error::Signature file missing or empty"; exit 1; } | |
| echo "Re-signed: ${APPIMAGE}.sig" | |
| # Upload AppImage and signature to release | |
| gh release upload "$RELEASE_TAG" \ | |
| "$APPIMAGE" "${APPIMAGE}.sig" \ | |
| --repo "${{ github.repository }}" \ | |
| --clobber | |
| echo "Uploaded AppImage to release $RELEASE_TAG" | |
| fi | |
| # Upload deb package (not post-processed, upload as-is) | |
| for DEB in "$BUNDLE_DIR"/deb/*.deb; do | |
| if [ -f "$DEB" ]; then | |
| gh release upload "$RELEASE_TAG" "$DEB" \ | |
| --repo "${{ github.repository }}" \ | |
| --clobber | |
| echo "Uploaded: $(basename "$DEB")" | |
| fi | |
| done | |
| # Upload rpm package if present | |
| for RPM in "$BUNDLE_DIR"/rpm/*.rpm; do | |
| if [ -f "$RPM" ]; then | |
| gh release upload "$RELEASE_TAG" "$RPM" \ | |
| --repo "${{ github.repository }}" \ | |
| --clobber | |
| echo "Uploaded: $(basename "$RPM")" | |
| fi | |
| done | |
| generate-updater-metadata: | |
| needs: [create-release, build-tauri] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate and upload latest.json | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const releaseId = ${{ needs.create-release.outputs.release_id }}; | |
| // Fetch release metadata | |
| const { data: releaseData } = await github.rest.repos.getRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: releaseId | |
| }); | |
| const version = releaseData.tag_name.replace(/^v/, ''); | |
| const pubDate = new Date().toISOString(); | |
| const platforms = {}; | |
| // Helper to fetch signature using GitHub API (works for draft releases) | |
| const fetchSignature = async (assetId) => { | |
| try { | |
| const { data } = await github.rest.repos.getReleaseAsset({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| asset_id: assetId, | |
| headers: { accept: 'application/octet-stream' } | |
| }); | |
| // data is ArrayBuffer, convert to string | |
| return Buffer.from(data).toString('utf-8'); | |
| } catch (e) { | |
| console.log(`Failed to fetch signature for asset ${assetId}:`, e.message); | |
| return ''; | |
| } | |
| }; | |
| const findAssetPair = (pattern, sigPattern) => { | |
| const asset = releaseData.assets.find(a => a?.name && pattern.test(a.name) && !a.name.endsWith('.sig')); | |
| const sig = releaseData.assets.find(a => a?.name && sigPattern.test(a.name)); | |
| return { asset, sig }; | |
| }; | |
| // Construct public download URL (will be valid after publish) | |
| const getPublicUrl = (filename) => | |
| `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${{ github.ref_name }}/${filename}`; | |
| // macOS - Universal Binary (supports both Apple Silicon and Intel) | |
| const macUniversal = findAssetPair(/_universal\.app\.tar\.gz$/, /_universal\.app\.tar\.gz\.sig$/); | |
| if (macUniversal.asset) { | |
| const signature = macUniversal.sig ? await fetchSignature(macUniversal.sig.id) : ''; | |
| const url = getPublicUrl(macUniversal.asset.name); | |
| // Both darwin-aarch64 and darwin-x86_64 point to the same universal binary | |
| platforms['darwin-aarch64'] = { signature, url }; | |
| platforms['darwin-x86_64'] = { signature, url }; | |
| } | |
| // Linux | |
| const linux = findAssetPair(/\.AppImage$/, /\.AppImage\.sig$/); | |
| if (linux.asset) { | |
| const signature = linux.sig ? await fetchSignature(linux.sig.id) : ''; | |
| platforms['linux-x86_64'] = { signature, url: getPublicUrl(linux.asset.name) }; | |
| } | |
| // Windows | |
| const windows = findAssetPair(/x64-setup\.exe$/, /x64-setup\.exe\.sig$/); | |
| if (windows.asset) { | |
| const signature = windows.sig ? await fetchSignature(windows.sig.id) : ''; | |
| platforms['windows-x86_64'] = { signature, url: getPublicUrl(windows.asset.name) }; | |
| } | |
| const latestJson = { | |
| version, | |
| notes: `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/tag/${{ github.ref_name }}`, | |
| pub_date: pubDate, | |
| platforms | |
| }; | |
| console.log('📄 latest.json:', JSON.stringify(latestJson, null, 2)); | |
| // Delete the existing latest.json asset | |
| const existing = releaseData.assets.find(a => a.name === 'latest.json'); | |
| if (existing) { | |
| await github.rest.repos.deleteReleaseAsset({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| asset_id: existing.id | |
| }); | |
| } | |
| // Upload latest.json | |
| const uploadUrl = releaseData.upload_url.replace('{?name,label}', '?name=latest.json'); | |
| await github.request({ | |
| method: 'POST', | |
| url: uploadUrl, | |
| headers: { 'content-type': 'application/json' }, | |
| data: JSON.stringify(latestJson, null, 2) | |
| }); | |
| - name: Publish release | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const releaseId = ${{ needs.create-release.outputs.release_id }}; | |
| const { data: generatedNotes } = await github.rest.repos.generateReleaseNotes({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag_name: '${{ github.ref_name }}', | |
| }); | |
| await github.rest.repos.updateRelease({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| release_id: releaseId, | |
| body: generatedNotes.body, | |
| draft: false, | |
| }); | |
| console.log('🎉 Release published!'); |