name: Release on: push: tags: ['v*'] workflow_dispatch: inputs: publish_release: description: 'Publish the GitHub release after all platform build and verification jobs pass' type: boolean default: true permissions: contents: write jobs: build: name: Build ${{ matrix.label }} strategy: fail-fast: false matrix: include: - label: macOS-arm64 os: macos-26 target: aarch64-apple-darwin bundles: app,dmg artifact: src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg - label: Windows-x64 os: windows-latest target: x86_64-pc-windows-msvc bundles: nsis artifact: src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe - label: Linux-x64-AppImage os: ubuntu-22.04 target: x86_64-unknown-linux-gnu bundles: appimage artifact: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 with: submodules: recursive - uses: actions/setup-node@v6 with: node-version: 22 cache: npm - name: Verify release version run: node scripts/verify-release-version.js - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Install Linux dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ libappindicator3-dev \ librsvg2-dev \ appstream \ curl \ patchelf \ libdbus-1-dev \ pkg-config \ xvfb \ libtinfo5 \ rpm \ cpio \ libarchive-tools \ desktop-file-utils \ xdg-utils - run: npm ci - name: Provision locked engines if: runner.os != 'macOS' run: node scripts/provision-engines.js --target ${{ matrix.target }} - name: Build package if: runner.os != 'Linux' run: npm run tauri build -- -vv --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} env: APPIMAGE_EXTRACT_AND_RUN: 1 - name: Build Linux native packages if: runner.os == 'Linux' run: npm run tauri build -- -vv --target ${{ matrix.target }} --bundles deb,rpm env: APPIMAGE_EXTRACT_AND_RUN: 1 - name: Verify and preserve Linux native packages if: runner.os == 'Linux' run: | node scripts/verify-linux-packages.js --target ${{ matrix.target }} mkdir -p "$RUNNER_TEMP/firelink-native-packages/deb" "$RUNNER_TEMP/firelink-native-packages/rpm" cp src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb "$RUNNER_TEMP/firelink-native-packages/deb/" cp src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm "$RUNNER_TEMP/firelink-native-packages/rpm/" - name: Build Linux AppImage if: runner.os == 'Linux' run: npm run tauri build -- -vv --target ${{ matrix.target }} --bundles appimage env: APPIMAGE_EXTRACT_AND_RUN: 1 FIRELINK_OMIT_ENGINE_DIST_FOR_TAURI_BUNDLE: '1' - name: Install pinned appimagetool (Linux only) if: runner.os == 'Linux' env: APPIMAGETOOL_URL: https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage APPIMAGETOOL_SHA256: a6d71e2b6cd66f8e8d16c37ad164658985e0cf5fcaa950c90a482890cb9d13e0 run: | curl -fsSL "$APPIMAGETOOL_URL" -o "$RUNNER_TEMP/appimagetool" echo "$APPIMAGETOOL_SHA256 $RUNNER_TEMP/appimagetool" | sha256sum -c - chmod +x "$RUNNER_TEMP/appimagetool" - name: Repack AppImage with engines (Linux only) if: runner.os == 'Linux' run: node scripts/repack-linux-appimage-engines.js --target ${{ matrix.target }} --appimagetool "$RUNNER_TEMP/appimagetool" - name: Verify macOS packaged engines and launch if: runner.os == 'macOS' run: | APP="src-tauri/target/${{ matrix.target }}/release/bundle/macos/Firelink.app" DMG="$(find src-tauri/target/${{ matrix.target }}/release/bundle/dmg -name '*.dmg' -print -quit)" test -n "$DMG" npm run verify:macos-signing -- --app "$APP" --dmg "$DMG" node scripts/verify-binaries.js --search-root "$APP" --target ${{ matrix.target }} node scripts/smoke-packaged-app.js --executable "$APP/Contents/MacOS/firelink" - name: Verify Windows installer payload and launch if: runner.os == 'Windows' shell: pwsh run: | $installer = Get-ChildItem "src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe" | Select-Object -First 1 if (-not $installer) { throw "Windows NSIS installer artifact was not produced." } $extractRoot = "$env:RUNNER_TEMP/firelink-installer" Remove-Item -Recurse -Force $extractRoot -ErrorAction SilentlyContinue & 7z x $installer.FullName "-o$extractRoot" -y if ($LASTEXITCODE -ne 0) { throw "7z failed to extract the Windows installer payload (exit code $LASTEXITCODE)." } node scripts/verify-binaries.js --search-root "$extractRoot" --target ${{ matrix.target }} $portableRoot = "$env:RUNNER_TEMP/firelink-portable-payload" $portableArtifactDir = "$env:RUNNER_TEMP/firelink-portable" Remove-Item -Recurse -Force $portableRoot -ErrorAction SilentlyContinue Remove-Item -Recurse -Force $portableArtifactDir -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path $portableRoot -Force | Out-Null New-Item -ItemType Directory -Path $portableArtifactDir -Force | Out-Null $payloadExe = Get-ChildItem $extractRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ieq "firelink.exe" } | Sort-Object FullName | Select-Object -First 1 if (-not $payloadExe) { throw "firelink.exe was not found in the extracted installer payload." } Copy-Item (Join-Path $payloadExe.Directory.FullName '*') $portableRoot -Recurse -Force Set-Content -Path (Join-Path $portableRoot 'portable.flag') -Value 'portable' -NoNewline @" Firelink portable Extract this folder to a writable location and launch firelink.exe. Close Firelink before copying or moving this folder. Settings, queues, logs, and WebView data are stored under data\. Only one Firelink instance can run at a time; close the installed app before launching this copy. Credentials, browser cookies, and URL query/fragment data are not persisted in portable queue records. Saved site passwords remain in the Windows credential store and are not portable. The portable folder contains the extension pairing credential; treat it as sensitive and do not share it. Saved absolute download locations may need to be selected again after moving to another drive. The portable archive does not register the firelink:// protocol; use the installer for browser launch integration. "@ | Set-Content -Path (Join-Path $portableRoot 'PORTABLE_README.txt') New-Item -ItemType Directory -Path (Join-Path $portableRoot 'data') -Force | Out-Null $portableExe = Join-Path $portableRoot 'firelink.exe' node scripts/verify-binaries.js --search-root "$portableRoot" --target ${{ matrix.target }} node scripts/smoke-packaged-app.js --executable $portableExe --assert-no-visible-child-windows --assert-portable-data Get-ChildItem $portableRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^(unins|Uninstall).*\.exe$' } | Remove-Item -Force -ErrorAction SilentlyContinue $portableDataDir = Join-Path $portableRoot 'data' for ($attempt = 1; $attempt -le 10; $attempt++) { Remove-Item -Recurse -Force $portableDataDir -ErrorAction SilentlyContinue if (-not (Test-Path $portableDataDir)) { break } Start-Sleep -Milliseconds (200 * $attempt) } if (Test-Path $portableDataDir) { throw "Portable test data could not be removed after smoke; refusing to package a ZIP containing runtime data." } $portableZip = "$portableArtifactDir/Firelink_${{ github.ref_name }}_Windows-x64-portable.zip" 7z a -tzip $portableZip "$portableRoot\*" -y $installRoot = "$env:RUNNER_TEMP\FirelinkSmoke" Remove-Item -Recurse -Force $installRoot -ErrorAction SilentlyContinue $install = Start-Process -FilePath $installer.FullName -ArgumentList @("/S", "/D=$installRoot") -Wait -PassThru if ($install.ExitCode -ne 0) { throw "Silent NSIS install failed with exit code $($install.ExitCode)." } $exe = $null foreach ($attempt in 1..60) { $exe = Get-ChildItem $installRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ieq "firelink.exe" } | Sort-Object FullName | Select-Object -First 1 if ($exe) { break } Start-Sleep -Seconds 1 } if (-not $exe) { throw "firelink.exe was not found under $installRoot after silent install." } node scripts/smoke-packaged-app.js --executable $exe.FullName --assert-no-visible-child-windows - name: Verify Linux AppImage payload and launch if: runner.os == 'Linux' run: | APPIMAGE="$(find src-tauri/target/${{ matrix.target }}/release/bundle/appimage -name '*.AppImage' -print -quit)" chmod +x "$APPIMAGE" (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/$APPIMAGE" --appimage-extract >/dev/null) node scripts/verify-binaries.js --search-root "$RUNNER_TEMP/squashfs-root" --target ${{ matrix.target }} xvfb-run -a node scripts/smoke-packaged-app.js --executable "$RUNNER_TEMP/squashfs-root/AppRun" - uses: actions/upload-artifact@v7 with: name: Firelink-${{ matrix.label }}-${{ github.ref_name }} path: ${{ matrix.artifact }} if-no-files-found: error retention-days: 3 - uses: actions/upload-artifact@v7 if: runner.os == 'Windows' with: name: Firelink-Windows-x64-portable-${{ github.ref_name }} path: ${{ runner.temp }}/firelink-portable/*.zip if-no-files-found: error retention-days: 3 - uses: actions/upload-artifact@v7 if: runner.os == 'Linux' with: name: Firelink-Linux-x64-Deb-${{ github.ref_name }} path: ${{ runner.temp }}/firelink-native-packages/deb/*.deb if-no-files-found: error retention-days: 3 - uses: actions/upload-artifact@v7 if: runner.os == 'Linux' with: name: Firelink-Linux-x64-RPM-${{ github.ref_name }} path: ${{ runner.temp }}/firelink-native-packages/rpm/*.rpm if-no-files-found: error retention-days: 3 publish: name: Publish GitHub release if: >- startsWith(github.ref, 'refs/tags/v') && ( github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release) ) needs: build runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v7 - uses: actions/download-artifact@v8 with: path: release-assets merge-multiple: true - name: Extract changelog release notes shell: bash run: | VERSION="${GITHUB_REF_NAME#v}" awk '/^## \['"$VERSION"'\]/{flag=1; next} /^## \[/{if(flag) exit} flag' CHANGELOG.md > release_notes.md test -s release_notes.md - name: Normalize release asset names shell: bash run: | VERSION="${GITHUB_REF_NAME#v}" rename_asset() { local pattern="$1" local destination="$2" local source mapfile -d '' -t matches < <(find release-assets -maxdepth 1 -type f -name "$pattern" -print0) if (( ${#matches[@]} != 1 )); then echo "::error::Expected exactly one release asset matching $pattern, found ${#matches[@]}" exit 1 fi source="${matches[0]}" mv "$source" "release-assets/$destination" } rename_asset '*.dmg' "Firelink_${VERSION}_macOS-ARM64.dmg" rename_asset '*.AppImage' "Firelink_${VERSION}_Linux-x64.AppImage" rename_asset '*.deb' "Firelink_${VERSION}_Linux-x64.deb" rename_asset '*.rpm' "Firelink_${VERSION}_Linux-x64.rpm" rename_asset '*.exe' "Firelink_${VERSION}_Windows-x64-setup.exe" rename_asset '*.zip' "Firelink_${VERSION}_Windows-x64-portable.zip" - name: Generate checksums run: | cd release-assets find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS - uses: softprops/action-gh-release@v3 with: files: release-assets/** body_path: release_notes.md