name: Build Windows Desktop Client on: workflow_dispatch: inputs: app_version: description: "Optional app version for this build, e.g. 0.1.3" required: false default: "" jobs: build-windows-client: name: Build Windows Client runs-on: ubuntu-latest defaults: run: shell: bash working-directory: desktop-client steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: x86_64-pc-windows-msvc - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential \ clang \ lld \ llvm \ curl \ wget \ file \ nsis \ osslsigncode \ pkg-config \ libssl-dev \ libayatana-appindicator3-dev \ librsvg2-dev \ patchelf - name: Install cargo-xwin run: | if ! cargo xwin --version >/dev/null 2>&1; then cargo install --locked cargo-xwin fi - name: Install desktop client dependencies run: npm install - name: Apply requested app version if: ${{ github.event.inputs.app_version != '' }} run: | set -euo pipefail VERSION="${{ github.event.inputs.app_version }}" export VERSION case "${VERSION}" in *[!0-9A-Za-z.+-]*|'') echo "Invalid app_version: ${VERSION}" exit 1 ;; esac node -e ' const fs = require("fs"); const version = process.env.VERSION; const pkgPath = "package.json"; const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); pkg.version = version; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); ' node -e ' const fs = require("fs"); const version = process.env.VERSION; const configPath = "src-tauri/tauri.conf.json"; const config = JSON.parse(fs.readFileSync(configPath, "utf8")); config.version = version; fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n"); ' perl -0pi -e 's/^version = ".*?"$/version = "'"${VERSION}"'"/m' src-tauri/Cargo.toml echo "Using app version ${VERSION}" - name: Build bundled Windows tunnel helper run: npm run helper:windows-x64 - name: Prepare signing certificate if: ${{ secrets.WINDOWS_SIGN_PFX_B64 != '' && secrets.WINDOWS_SIGN_PFX_PASSWORD != '' }} run: | printf '%s' "${{ secrets.WINDOWS_SIGN_PFX_B64 }}" | base64 -d > /tmp/nexavpn-signing-cert.pfx - name: Sign bundled Windows tunnel helper if: ${{ secrets.WINDOWS_SIGN_PFX_B64 != '' && secrets.WINDOWS_SIGN_PFX_PASSWORD != '' }} run: | set -euo pipefail timestamp_url="${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }}" if [ -z "${timestamp_url}" ]; then timestamp_url="http://timestamp.digicert.com" fi helper_path="src-tauri/bundled/windows-x64/nexavpn-tunnel-helper.exe" signed_path="${helper_path}.signed" osslsigncode sign \ -pkcs12 /tmp/nexavpn-signing-cert.pfx \ -pass "${{ secrets.WINDOWS_SIGN_PFX_PASSWORD }}" \ -h sha256 \ -ts "${timestamp_url}" \ -in "${helper_path}" \ -out "${signed_path}" mv "${signed_path}" "${helper_path}" - name: Build Windows installer run: npm run tauri:build:windows-x64:linux - name: Prepare artifact names run: | set -euo pipefail VERSION="${{ github.event.inputs.app_version }}" if [ -n "${VERSION}" ]; then echo "ARTIFACT_SUFFIX=-${VERSION}" >> "${GITHUB_ENV}" else echo "ARTIFACT_SUFFIX=" >> "${GITHUB_ENV}" fi - name: Sign Windows desktop executable and installer if: ${{ secrets.WINDOWS_SIGN_PFX_B64 != '' && secrets.WINDOWS_SIGN_PFX_PASSWORD != '' }} run: | set -euo pipefail timestamp_url="${{ secrets.WINDOWS_SIGN_TIMESTAMP_URL }}" if [ -z "${timestamp_url}" ]; then timestamp_url="http://timestamp.digicert.com" fi sign_file() { local input_path="$1" local output_path="${input_path}.signed" osslsigncode sign \ -pkcs12 /tmp/nexavpn-signing-cert.pfx \ -pass "${{ secrets.WINDOWS_SIGN_PFX_PASSWORD }}" \ -h sha256 \ -ts "${timestamp_url}" \ -in "${input_path}" \ -out "${output_path}" mv "${output_path}" "${input_path}" } sign_file "src-tauri/target/x86_64-pc-windows-msvc/release/nexavpn-desktop.exe" shopt -s nullglob for installer in src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe; do sign_file "${installer}" done - name: Cleanup signing certificate if: ${{ always() && secrets.WINDOWS_SIGN_PFX_B64 != '' && secrets.WINDOWS_SIGN_PFX_PASSWORD != '' }} run: rm -f /tmp/nexavpn-signing-cert.pfx - name: Upload Windows installer uses: christopherhx/gitea-upload-artifact@v4 with: name: NexaVPN-windows-installer${{ env.ARTIFACT_SUFFIX }} path: | desktop-client/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe desktop-client/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.msi if-no-files-found: error - name: Upload raw Windows build outputs uses: christopherhx/gitea-upload-artifact@v4 with: name: NexaVPN-windows-raw-build${{ env.ARTIFACT_SUFFIX }} path: | desktop-client/src-tauri/target/x86_64-pc-windows-msvc/release/nexavpn-desktop.exe desktop-client/src-tauri/bundled/windows-x64/nexavpn-tunnel-helper.exe desktop-client/src-tauri/bundled/windows-x64/wireguard-installer.msi if-no-files-found: error