name: Release on: push: tags: - 'v*' workflow_dispatch: inputs: tag: description: 'Tag to release (e.g., v4.0.0)' required: true env: CARGO_TERM_COLOR: always ANDROID_SDK_API_LEVEL: '36' ANDROID_BUILD_TOOLS: '36.0.0' ANDROID_NDK_VERSION: '28.2.13676358' jobs: verify: name: Verify release inputs runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Checkout FIPS Docker e2e context run: git clone --depth 1 https://github.com/mmalmi/fips.git ../fips - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Linux native dependencies run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libdbus-1-dev pkg-config - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Run release gate run: ./scripts/release-gate.sh build-cli: name: Build CLI release artifact (${{ matrix.target }}) runs-on: ${{ matrix.os }} needs: verify permissions: contents: read strategy: fail-fast: false matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-musl cross: true - os: ubuntu-latest target: aarch64-unknown-linux-musl cross: true - os: ubuntu-latest target: arm-unknown-linux-musleabihf cross: true - os: macos-14 target: aarch64-apple-darwin cross: false - os: windows-latest target: x86_64-pc-windows-msvc cross: false steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Build Linux musl CLI with Docker if: ${{ matrix.cross }} run: ./scripts/build-nvpn-linux-musl ${{ matrix.target }} - name: Build CLI natively if: ${{ !matrix.cross }} run: cargo build --release --target ${{ matrix.target }} -p nvpn - name: Package CLI artifact shell: bash run: | set -euo pipefail RELEASE_VERSION="${{ github.event.inputs.tag || github.ref_name }}" TARGET="${{ matrix.target }}" BIN_DIR="target/${TARGET}/release" rm -rf dist/nvpn mkdir -p dist/nvpn if [ "${{ runner.os }}" = "Windows" ]; then cp "${BIN_DIR}/nvpn.exe" dist/nvpn/ powershell -NoProfile -Command "Compress-Archive -Path 'dist/nvpn/*' -DestinationPath 'dist/nvpn-${RELEASE_VERSION}-${TARGET}.zip' -Force" exit 0 fi cp "${BIN_DIR}/nvpn" dist/nvpn/ cat > dist/nvpn/install.sh << 'INSTALL_SCRIPT' #!/bin/bash set -e path_contains() { case ":${PATH}:" in *":$1:"*) return 0 ;; *) return 1 ;; esac } default_install_dir() { if [ "$(uname -s)" = "Darwin" ] && { [ -d /opt/homebrew/bin ] || path_contains /opt/homebrew/bin; }; then printf '%s\n' /opt/homebrew/bin else printf '%s\n' /usr/local/bin fi } INSTALL_DIR="${1:-$(default_install_dir)}" install -d "${INSTALL_DIR}" install -m 755 nvpn "${INSTALL_DIR}/" INSTALL_SCRIPT chmod +x dist/nvpn/install.sh cat > dist/nvpn/README.txt << 'README_TXT' nvpn - Nostr-signaled WireGuard control plane ============================================ Binary included: nvpn - CLI control plane Quick install: ./install.sh ./install.sh ~/.local/bin README_TXT tar -czf "dist/nvpn-${TARGET}.tar.gz" -C dist nvpn cp "dist/nvpn-${TARGET}.tar.gz" "dist/nvpn-${RELEASE_VERSION}-${TARGET}.tar.gz" - name: Upload CLI artifact uses: actions/upload-artifact@v4 with: name: cli-${{ matrix.target }} path: | dist/nvpn-${{ github.event.inputs.tag || github.ref_name }}-${{ matrix.target }}.* dist/nvpn-${{ matrix.target }}.tar.gz retention-days: 7 build-macos-app: name: Build native macOS app runs-on: macos-14 needs: verify permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: aarch64-apple-darwin - name: Install XcodeGen run: brew install xcodegen - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Import macOS signing certificate shell: bash env: MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} run: | set -euo pipefail if [ -z "${MACOS_CERTIFICATE_P12_BASE64:-}" ]; then echo "No MACOS_CERTIFICATE_P12_BASE64 secret configured; macOS release build requires a Developer ID identity." exit 0 fi cert_path="${RUNNER_TEMP}/macos-developer-id.p12" keychain_path="${RUNNER_TEMP}/release-signing.keychain-db" keychain_password="$(openssl rand -hex 24)" printf '%s' "${MACOS_CERTIFICATE_P12_BASE64}" | base64 -D > "${cert_path}" security create-keychain -p "${keychain_password}" "${keychain_path}" security set-keychain-settings -lut 21600 "${keychain_path}" security unlock-keychain -p "${keychain_password}" "${keychain_path}" security import "${cert_path}" -P "${MACOS_CERTIFICATE_PASSWORD:-}" -A -t cert -f pkcs12 -k "${keychain_path}" security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keychain_password}" "${keychain_path}" security list-keychains -d user -s "${keychain_path}" $(security list-keychains -d user | tr -d '"') security default-keychain -s "${keychain_path}" security find-identity -v -p codesigning - name: Prepare macOS notarization key shell: bash env: ASC_AUTH_KEY_BASE64: ${{ secrets.ASC_AUTH_KEY_BASE64 }} ASC_AUTH_KEY_ID: ${{ secrets.ASC_AUTH_KEY_ID }} ASC_AUTH_KEY_ISSUER_ID: ${{ secrets.ASC_AUTH_KEY_ISSUER_ID }} run: | set -euo pipefail if [ -z "${ASC_AUTH_KEY_BASE64:-}" ]; then echo "No ASC_AUTH_KEY_BASE64 secret configured; macOS release build requires notarization credentials." exit 0 fi if [ -z "${ASC_AUTH_KEY_ID:-}" ] || [ -z "${ASC_AUTH_KEY_ISSUER_ID:-}" ]; then echo "ASC_AUTH_KEY_ID and ASC_AUTH_KEY_ISSUER_ID must be set with ASC_AUTH_KEY_BASE64." exit 1 fi key_path="${RUNNER_TEMP}/AuthKey_${ASC_AUTH_KEY_ID}.p8" printf '%s' "${ASC_AUTH_KEY_BASE64}" | base64 -D > "${key_path}" { echo "NVPN_ASC_AUTH_KEY_PATH=${key_path}" echo "NVPN_ASC_AUTH_KEY_ID=${ASC_AUTH_KEY_ID}" echo "NVPN_ASC_AUTH_KEY_ISSUER_ID=${ASC_AUTH_KEY_ISSUER_ID}" } >> "${GITHUB_ENV}" - name: macOS service install/uninstall e2e env: NVPN_RUN_MACOS_SERVICE_E2E: '1' run: ./scripts/e2e-macos-service.sh - name: Build native macOS app artifacts env: NVPN_MACOS_RUST_PROFILE: release NVPN_MACOS_XCODE_CONFIGURATION: Release NVPN_MACOS_RUST_TARGETS: aarch64-apple-darwin NVPN_RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} NVPN_MACOS_REQUIRE_SIGNING: '1' NVPN_MACOS_REQUIRE_NOTARIZATION: '1' MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} MACOS_NOTARY_PROFILE: ${{ secrets.MACOS_NOTARY_PROFILE }} MACOS_NOTARIZE_APPLE_ID: ${{ secrets.MACOS_NOTARIZE_APPLE_ID }} MACOS_NOTARIZE_APP_PASSWORD: ${{ secrets.MACOS_NOTARIZE_APP_PASSWORD }} MACOS_NOTARIZE_TEAM_ID: ${{ secrets.MACOS_NOTARIZE_TEAM_ID }} run: ./scripts/macos-build macos-release-artifacts - name: Upload native macOS app uses: actions/upload-artifact@v4 with: name: macos-arm64 path: | dist/nostr-vpn-${{ github.event.inputs.tag || github.ref_name }}-macos-arm64.dmg dist/nostr-vpn-${{ github.event.inputs.tag || github.ref_name }}-macos-arm64.app.tar.gz retention-days: 7 build-linux-app: name: Build native Linux app runs-on: ubuntu-latest needs: verify permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Install Linux native package dependencies run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ dpkg-dev \ fakeroot \ libadwaita-1-dev \ libdbus-1-dev \ libgtk-4-dev \ librsvg2-bin \ pkg-config \ zbar-tools - name: Install cargo-deb run: cargo install cargo-deb --locked - name: Build Linux desktop package run: | set -euo pipefail RELEASE_VERSION="${{ github.event.inputs.tag || github.ref_name }}" cargo build --release --locked --manifest-path linux/Cargo.toml (cd linux && cargo deb --no-build) mkdir -p dist cp "$(ls -1t linux/target/debian/*.deb | head -1)" "dist/nostr-vpn-${RELEASE_VERSION}-linux-x64.deb" dpkg-deb --info "dist/nostr-vpn-${RELEASE_VERSION}-linux-x64.deb" >/dev/null - name: Upload native Linux app uses: actions/upload-artifact@v4 with: name: linux-x64 path: dist/nostr-vpn-${{ github.event.inputs.tag || github.ref_name }}-linux-x64.deb retention-days: 7 build-windows-app: name: Build native Windows app runs-on: windows-latest needs: verify permissions: contents: read defaults: run: shell: pwsh steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Install Windows native package dependencies run: choco install llvm innosetup -y --no-progress - name: Build Windows installer env: NVPN_WINDOWS_LLVM_BIN: C:\Program Files\LLVM\bin run: | .\scripts\windows-build.ps1 -Configuration Release -Publish -Installer -Tag "${{ github.event.inputs.tag || github.ref_name }}" -OutputDir dist if (!(Test-Path "dist\nostr-vpn-${{ github.event.inputs.tag || github.ref_name }}-windows-x64-setup.exe")) { throw "Windows setup artifact was not produced" } - name: Upload native Windows app uses: actions/upload-artifact@v4 with: name: windows-x64 path: dist/nostr-vpn-${{ github.event.inputs.tag || github.ref_name }}-windows-x64-setup.exe retention-days: 7 build-android-app: name: Build Android app runs-on: ubuntu-latest needs: verify permissions: contents: read env: RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Setup Java uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: aarch64-linux-android - name: Cache Cargo dependencies uses: Swatinem/rust-cache@v2 with: cache-bin: false - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: gradle-version: 8.14.3 - name: Setup Android SDK uses: android-actions/setup-android@v3 with: packages: >- platform-tools platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358 - name: Install cargo-ndk run: cargo install cargo-ndk --locked - name: Prepare Android signing shell: bash run: | set -euo pipefail missing=() for name in ANDROID_KEYSTORE_B64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do if [ -z "${!name:-}" ]; then missing+=("${name}") fi done if [ "${#missing[@]}" -ne 0 ]; then echo "Missing Android signing secrets: ${missing[*]}" exit 1 fi key_path="${RUNNER_TEMP}/android-release.jks" printf '%s' "${ANDROID_KEYSTORE_B64}" | base64 --decode > "${key_path}" { printf 'ANDROID_KEYSTORE_PATH=%s\n' "${key_path}" printf 'ANDROID_HOME=%s\n' "${ANDROID_SDK_ROOT}" printf 'ANDROID_SDK_ROOT=%s\n' "${ANDROID_SDK_ROOT}" printf 'ANDROID_NDK_HOME=%s\n' "${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" printf 'NDK_HOME=%s\n' "${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" } >> "${GITHUB_ENV}" - name: Build Android artifacts run: gradle -p android :app:assembleRelease :app:bundleRelease - name: Package Android artifacts shell: bash run: | set -euo pipefail mkdir -p dist apk_path="$(find android/app/build/outputs/apk/release -maxdepth 1 -name '*.apk' -print -quit)" aab_path="$(find android/app/build/outputs/bundle/release -maxdepth 1 -name '*.aab' -print -quit)" if [ -z "${apk_path}" ] || [ -z "${aab_path}" ]; then echo "Expected Android APK/AAB artifacts were not produced." exit 1 fi apksigner="$(find "${ANDROID_SDK_ROOT}/build-tools" -path '*/apksigner' -type f | sort -V | tail -1)" "${apksigner}" verify --verbose "${apk_path}" cp "${apk_path}" "dist/nostr-vpn-${RELEASE_TAG}-android-arm64.apk" cp "${aab_path}" "dist/nostr-vpn-${RELEASE_TAG}-android-arm64.aab" - name: Upload Android app uses: actions/upload-artifact@v4 with: name: android-arm64 path: | dist/nostr-vpn-${{ env.RELEASE_TAG }}-android-arm64.apk dist/nostr-vpn-${{ env.RELEASE_TAG }}-android-arm64.aab retention-days: 7 release: name: Create GitHub release if: ${{ always() && needs.verify.result == 'success' && needs.build-cli.result == 'success' && needs.build-macos-app.result == 'success' && needs.build-linux-app.result == 'success' && needs.build-windows-app.result == 'success' && needs.build-android-app.result == 'success' }} needs: - verify - build-cli - build-macos-app - build-linux-app - build-windows-app - build-android-app runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.tag || github.ref }} - name: Download build artifacts uses: actions/download-artifact@v4 with: path: artifacts merge-multiple: true - name: Prepare release notes shell: bash env: RELEASE_REPOSITORY: ${{ github.repository }} RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} run: | node scripts/render-release-notes.mjs \ --tag "${RELEASE_TAG}" \ --commit "${GITHUB_SHA}" \ --asset-dir artifacts \ --asset-base-url "https://github.com/${RELEASE_REPOSITORY}/releases/download/${RELEASE_TAG}" \ --changelog CHANGELOG.md \ --built-line "Built signed and notarized Apple Silicon macOS DMG and updater archive." \ --built-line "Built Linux x64 desktop Debian package." \ --built-line "Built Windows x64 installer." \ --built-line "Built signed Android arm64 APK/AAB." \ --out release-notes.md - name: Create GitHub release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag || github.ref_name }} name: ${{ github.event.inputs.tag || github.ref_name }} draft: false prerelease: ${{ contains(github.event.inputs.tag || github.ref_name, '-') }} files: artifacts/* body_path: release-notes.md