Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.macos-release.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copy to the repository root .env.macos-release and set permissions to 0600.
# These are placeholders. Never commit a real certificate or password.
BUILD_CERTIFICATE_BASE64=REPLACE_WITH_BASE64_P12
P12_PASSWORD=REPLACE_WITH_P12_EXPORT_PASSWORD
KEYCHAIN_PASSWORD=REPLACE_WITH_TEMPORARY_KEYCHAIN_PASSWORD
APPLE_ID=developer@example.com
APPLE_TEAM_ID=REPLACE_WITH_TEAM_ID
APPLE_APP_SPECIFIC_PASSWORD=REPLACE_WITH_APP_SPECIFIC_PASSWORD
84 changes: 45 additions & 39 deletions .github/workflows/napstrfy-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
- 'remote-protocol/**'
- 'static/**'
- '.github/workflows/napstrfy-desktop.yml'
- 'scripts/macos-release.mjs'
- 'tests/macos-release.test.mjs'

permissions:
contents: read
Expand All @@ -31,9 +33,11 @@ jobs:
- name: macOS-Intel
runner: macos-15-intel
bundles: app
mac_arch: x86_64
- name: macOS-Apple-Silicon
runner: macos-15
bundles: app
mac_arch: arm64
runs-on: ${{ matrix.runner }}
defaults:
run:
Expand All @@ -47,6 +51,9 @@ jobs:
cache: npm
cache-dependency-path: android/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- name: Verify native macOS runner
if: runner.os == 'macOS'
run: test "$(uname -m)" = '${{ matrix.mac_arch }}'
- name: Install Linux build and audio dependencies
if: runner.os == 'Linux'
run: |
Expand Down Expand Up @@ -76,26 +83,39 @@ jobs:
writeFileSync('src-tauri/tauri.ci.conf.json', JSON.stringify({ version: tag ? tag.slice(1) : config.version }));
JS
- name: Build installer
if: runner.os != 'macOS'
env:
APPIMAGE_EXTRACT_AND_RUN: '1'
NO_STRIP: '1'
run: npm run bundle -- --verbose --config src-tauri/tauri.ci.conf.json --bundles '${{ matrix.bundles }}' -- --locked
- name: Package macOS DMG
- name: Test macOS release helper
if: runner.os == 'macOS'
run: node --test ../tests/macos-release.test.mjs
- name: Build community macOS DMG for pull requests
if: runner.os == 'macOS' && github.event_name == 'pull_request'
run: npm run macos-build
- name: Build signed and notarized macOS DMG
if: runner.os == 'macOS' && github.event_name != 'pull_request'
env:
BUILD_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
# Retry only packaging, without recompiling or skipping verification.
# Verbose output exposes hdiutil errors hidden by the default logger.
for attempt in 1 2 3; do
if npm run tauri -- bundle --verbose --ci --config src-tauri/tauri.ci.conf.json --bundles dmg; then
break
fi
if [[ "$attempt" == 3 ]]; then
echo 'Napstrfy DMG packaging failed after three attempts' >&2
exit 1
fi
echo "DMG packaging attempt $attempt failed; retrying in 10 seconds" >&2
sleep 10
done
args=(--ci)
if [[ -n "$BUILD_TAG" ]]; then
args+=(--tag "$BUILD_TAG")
fi
npm run macos-build:signed -- "${args[@]}"
- name: Always clean macOS signing session
if: always() && runner.os == 'macOS'
run: |
if [[ -f ../scripts/macos-release.mjs ]]; then
node ../scripts/macos-release.mjs --app napstrfy --cleanup
fi
- name: Prepare and smoke-test AppImage
if: runner.os == 'Linux'
run: |
Expand All @@ -112,31 +132,17 @@ jobs:
echo 'Napstrfy failed its AppImage startup check' >&2
exit 1
fi
- name: Verify macOS application signature
if: runner.os == 'macOS'
run: |
# A DMG-only build removes the intermediate .app after packaging.
# Verify the application users will actually install from the DMG.
dmg="$(find src-tauri/target/release/bundle/dmg -maxdepth 1 -name '*.dmg' -print -quit)"
if [[ -z "$dmg" ]]; then
echo 'Tauri did not produce the expected Napstrfy DMG' >&2
exit 1
fi
mount_point="$(mktemp -d "$RUNNER_TEMP/napstrfy-dmg.XXXXXX")"
cleanup_dmg() {
hdiutil detach -quiet "$mount_point" >/dev/null 2>&1 || true
rmdir "$mount_point" >/dev/null 2>&1 || true
}
trap cleanup_dmg EXIT
hdiutil attach -readonly -nobrowse -mountpoint "$mount_point" "$dmg" >/dev/null
app="$mount_point/Napstrfy.app"
if [[ ! -f "$app/Contents/MacOS/napstrfy" ]]; then
echo 'The finished DMG does not contain the Napstrfy application' >&2
exit 1
fi
codesign --verify --deep --strict --verbose=2 "$app"
- name: Collect installers and checksums
env:
MACOS_BUILD_MODE: ${{ github.event_name == 'pull_request' && 'unsigned' || 'signed' }}
run: |
if [[ "$RUNNER_OS" == 'macOS' ]]; then
artifacts="src-tauri/target/macos-release/$MACOS_BUILD_MODE"
(cd "$artifacts" && shasum -a 256 -c ./*.dmg.sha256)
mkdir -p installers
cp "$artifacts"/*.dmg "$artifacts"/*.dmg.sha256 installers/
exit 0
fi
node --input-type=module <<'JS'
import { readdirSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs';
import { join, basename } from 'node:path';
Expand Down Expand Up @@ -200,7 +206,7 @@ jobs:
cat > "$notes" <<'NOTES'
Napstr and Napstrfy installers.

The macOS DMGs are ad-hoc-signed, unnotarized community builds. After the first blocked launch, open System Settings → Privacy & Security and choose Open Anyway.
Release macOS DMGs are Developer ID signed, notarized, and stapled. SHA-256 checksums accompany the installers.
NOTES
create_args=(release create "$RELEASE_TAG" --verify-tag --draft --title "Napstr $RELEASE_TAG" --notes-file "$notes")
if [[ "$RELEASE_TAG" == *-* ]]; then
Expand All @@ -211,4 +217,4 @@ jobs:
gh "${create_args[@]}" || gh release view "$RELEASE_TAG" >/dev/null
fi
shopt -s nullglob
gh release upload "$RELEASE_TAG" installers/*.AppImage installers/*.exe installers/*.dmg --clobber
gh release upload "$RELEASE_TAG" installers/*.AppImage installers/*.exe installers/*.dmg installers/*.sha256 --clobber
173 changes: 127 additions & 46 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
name: Build desktop and Android installers
name: Build Napstr desktop and Napstrfy Android installers

on:
workflow_dispatch:
inputs:
release_tag:
description: Release tag to create (for example, v0.1.0 or v0.1.5-rc1)
required: true
description: Release tag to create (v0.1.0 or v0.1.5-rc1); leave empty for macOS test installers
required: false
type: string
push:
tags: ['v*']
pull_request:
types: [opened, synchronize, reopened, edited]

permissions:
contents: write
contents: read

jobs:
build:
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
name: Napstr ${{ matrix.platform }}
permissions:
contents: write
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
strategy:
Expand All @@ -33,17 +39,11 @@ jobs:
tor_url: https://archive.torproject.org/tor-package-archive/torbrowser/15.0.20/tor-expert-bundle-windows-x86_64-15.0.20.tar.gz
tor_sha256: d59bff934e3ad876e1623e24ae60c19aeea56f50178093b9f86fba230639f949
- platform: macos-15-intel
args: --config src-tauri/tauri.macos-community.conf.json --bundles dmg
tor_platform: macos
mac_arch: x86_64
tor_url: https://archive.torproject.org/tor-package-archive/torbrowser/15.0.20/tor-expert-bundle-macos-x86_64-15.0.20.tar.gz
tor_sha256: 6ec3048b3a5d55e297f35d84830d0e338884d702aac3db49056633c1223841df
- platform: macos-15
args: --config src-tauri/tauri.macos-community.conf.json --target aarch64-apple-darwin --bundles dmg
tor_platform: macos
mac_arch: arm64
tor_url: https://archive.torproject.org/tor-package-archive/torbrowser/15.0.20/tor-expert-bundle-macos-aarch64-15.0.20.tar.gz
tor_sha256: 73fdccde8136678e41a625160993e6a9dc4f4ff8cd376318b5e41e5627d55682
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v7
Expand All @@ -63,6 +63,9 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin' || '' }}
- name: Verify native macOS runner
if: runner.os == 'macOS'
run: test "$(uname -m)" = '${{ matrix.mac_arch }}'
- name: Install Linux system dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
Expand Down Expand Up @@ -97,14 +100,15 @@ jobs:
run: npm run check
- name: Test native core
run: cargo test --manifest-path src-tauri/Cargo.toml
- name: Test macOS release helper
if: runner.os == 'macOS'
run: node --test tests/macos-release.test.mjs
- name: Add pinned Tor Expert Bundle
if: runner.os != 'macOS'
shell: bash
run: bash scripts/prepare-tor.sh '${{ matrix.tor_platform }}' '${{ matrix.tor_url }}' '${{ matrix.tor_sha256 }}'
- name: Ad-hoc sign bundled macOS Tor runtime
if: matrix.tor_platform == 'macos'
shell: bash
run: bash scripts/adhoc-sign-macos.sh src-tauri/resources/tor/macos '${{ matrix.mac_arch }}'
- uses: tauri-apps/tauri-action@v1
if: runner.os != 'macOS'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Do not let linuxdeploy strip the bundled, already-built Tor runtime.
Expand All @@ -118,7 +122,7 @@ jobs:
releaseBody: |
Cross-platform Napstr desktop installers.

The macOS DMGs are ad-hoc-signed, unnotarized community builds. After the first blocked launch, open System Settings → Privacy & Security and choose Open Anyway.
Release macOS DMGs are Developer ID signed, notarized, and stapled. SHA-256 checksums accompany the macOS installers.
releaseDraft: true
prerelease: ${{ contains(env.RELEASE_TAG, '-') }}
args: ${{ matrix.args }}
Expand Down Expand Up @@ -155,44 +159,121 @@ jobs:
# tauri-action has already uploaded the initial bundle. Replace that
# asset with the repaired, verified AppImage under the same filename.
gh release upload "$RELEASE_TAG" "$appimage" --clobber
- name: Verify macOS community DMG
if: matrix.tor_platform == 'macos'
- name: Build signed and notarized macOS DMG
if: runner.os == 'macOS'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: npm run macos-build:signed -- --ci --tag "$RELEASE_TAG"
- name: Always clean macOS signing session
if: always() && runner.os == 'macOS'
shell: bash
run: |
dmg="$(find src-tauri/target -type f -path '*/release/bundle/dmg/*.dmg' -print -quit)"
if [[ -z "$dmg" ]]; then
echo "Tauri did not produce the expected macOS DMG" >&2
exit 1
fi

mount_point="$(mktemp -d "$RUNNER_TEMP/napstr-dmg.XXXXXX")"
cleanup_dmg() {
hdiutil detach -quiet "$mount_point" >/dev/null 2>&1 || true
rmdir "$mount_point" >/dev/null 2>&1 || true
}
trap cleanup_dmg EXIT
hdiutil attach -readonly -nobrowse -mountpoint "$mount_point" "$dmg" >/dev/null

app="$(find "$mount_point" -maxdepth 2 -type d -name 'Napstr.app' -print -quit)"
if [[ -z "$app" ]]; then
echo "The finished DMG does not contain Napstr.app" >&2
exit 1
if [[ -f scripts/macos-release.mjs ]]; then
node scripts/macos-release.mjs --cleanup
fi
codesign --verify --deep --strict --verbose=2 "$app"
app_binary="$app/Contents/MacOS/napstr"
tor_binary="$(find "$app/Contents/Resources" -type f -path '*/tor/macos/tor/tor' -print -quit)"
if [[ ! -f "$app_binary" || -z "$tor_binary" ]]; then
echo "The app bundle is missing Napstr or its bundled Tor executable" >&2
exit 1
- name: Upload verified macOS installer and checksum
if: runner.os == 'macOS'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
artifacts=src-tauri/target/macos-release/signed
(cd "$artifacts" && shasum -a 256 -c ./*.dmg.sha256)
if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
create_args=(release create "$RELEASE_TAG" --target "$GITHUB_SHA" --draft --title "Napstr $RELEASE_TAG" --notes "Napstr and Napstrfy installers. Release macOS DMGs are Developer ID signed, notarized, and stapled.")
if [[ "$RELEASE_TAG" == *-* ]]; then
create_args+=(--prerelease)
fi
gh "${create_args[@]}" || gh release view "$RELEASE_TAG" >/dev/null
fi
gh release upload "$RELEASE_TAG" "$artifacts"/*.dmg "$artifacts"/*.dmg.sha256 --clobber

bash scripts/verify-macos-bundle.sh "$app_binary" "$tor_binary" '${{ matrix.mac_arch }}'

cleanup_dmg
trap - EXIT
macos_test:
if: >-
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
(github.event_name == 'pull_request' &&
startsWith(github.event.pull_request.title, '[build]') &&
(github.event.action != 'edited' || github.event.changes.title != null))
name: Napstr ${{ matrix.name }}
env:
MACOS_BUILD_MODE: >-
${{ (github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.user.login != 'dependabot[bot]' &&
github.actor != 'dependabot[bot]')) && 'signed' || 'unsigned' }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS-Intel
runner: macos-15-intel
arch: x86_64
- name: macOS-Apple-Silicon
runner: macos-15
arch: arm64
runs-on: ${{ matrix.runner }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- uses: dtolnay/rust-toolchain@stable
- name: Verify native macOS runner
run: test "$(uname -m)" = '${{ matrix.arch }}'
- run: npm ci
- name: Verify interface
run: npm run check
- name: Test JavaScript and macOS release helper
run: npm run test:unit
- name: Test native core
run: cargo test --manifest-path src-tauri/Cargo.toml --locked
- name: Build community macOS DMG for fork or Dependabot pull requests
if: env.MACOS_BUILD_MODE == 'unsigned'
run: npm run macos-build
- name: Build signed and notarized macOS DMG
if: env.MACOS_BUILD_MODE == 'signed'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: npm run macos-build:signed -- --ci
- name: Always clean macOS signing session
if: always()
run: |
if [[ -f scripts/macos-release.mjs ]]; then
node scripts/macos-release.mjs --cleanup
fi
- name: Verify installer checksums
run: |
cd "src-tauri/target/macos-release/$MACOS_BUILD_MODE"
shasum -a 256 -c ./*.dmg.sha256
- name: Upload verified installer and checksum
uses: actions/upload-artifact@v4
with:
name: Napstr-${{ matrix.name }}-${{ env.MACOS_BUILD_MODE }}
path: |
src-tauri/target/macos-release/${{ env.MACOS_BUILD_MODE }}/*.dmg
src-tauri/target/macos-release/${{ env.MACOS_BUILD_MODE }}/*.dmg.sha256
if-no-files-found: error

android:
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
name: Build Napstrfy Android APK
permissions:
contents: write
runs-on: ubuntu-22.04
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
Expand Down Expand Up @@ -305,7 +386,7 @@ jobs:
cp "$apk" "$asset"

if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
create_args=(release create "$RELEASE_TAG" --draft --title "Napstr $RELEASE_TAG" --notes "Cross-platform Napstr desktop installers and the Napstrfy Android companion.")
create_args=(release create "$RELEASE_TAG" --target "$GITHUB_SHA" --draft --title "Napstr $RELEASE_TAG" --notes "Cross-platform Napstr desktop installers and the Napstrfy Android companion.")
if [[ "$RELEASE_TAG" == *-* ]]; then
create_args+=(--prerelease)
fi
Expand Down
Loading
Loading