Skip to content

build-release

build-release #53

Workflow file for this run

# Build workflow for public repo (oa-desktop-builds)
# Triggered by repository_dispatch from private repo (oa-desktop)
#
# This workflow runs in a PUBLIC repo to use free GitHub Actions minutes,
# while keeping the source code in a private repo.
#
# Required secrets (set in THIS public repo):
# GH_PAT - GitHub PAT with access to private repo + oa-fastchat submodule
# CLOUDFLARE_ACCOUNT_ID - Cloudflare account ID
# CLOUDFLARE_R2_ACCESS_KEY - R2 access key ID
# CLOUDFLARE_R2_SECRET_KEY - R2 secret access key
#
# Optional (for signed + notarized macOS builds):
# APPLE_ID - Apple ID email
# APPLE_PASSWORD - App-specific password (appleid.apple.com)
# APPLE_TEAM_ID - Apple Developer Team ID
# MACOS_CERTIFICATE - Base64-encoded .p12 certificate
# MACOS_CERTIFICATE_PWD - Password for the .p12 certificate
name: Build Release
on:
repository_dispatch:
types: [build-release]
env:
R2_BUCKET: oa-desktop-releases
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
# Private repo to checkout
PRIVATE_REPO: OpenAnonymity/oa-desktop
jobs:
# =============================================================================
# macOS Builds (arm64 + x64)
# =============================================================================
build-macos:
strategy:
fail-fast: false
matrix:
include:
- runner: macos-latest # M1/M2 (arm64)
arch: arm64
- runner: macos-15-intel # Intel (x64)
arch: x64
runs-on: ${{ matrix.runner }}
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout private repo
uses: actions/checkout@v4
with:
repository: ${{ env.PRIVATE_REPO }}
ref: ${{ github.event.client_payload.ref }}
submodules: recursive
token: ${{ secrets.GH_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Determine version
id: version
run: |
# Version is passed from private repo via client_payload
if [ -n "${{ github.event.client_payload.version }}" ]; then
VERSION="${{ github.event.client_payload.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION for ${{ matrix.arch }}"
- name: Install dependencies
run: npm ci
- name: Check signing availability
id: signing
run: |
if [ -n "$APPLE_TEAM_ID" ]; then
echo "available=true" >> $GITHUB_OUTPUT
else
echo "available=false" >> $GITHUB_OUTPUT
fi
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build app (unsigned)
if: steps.signing.outputs.available != 'true'
run: npm run make -- --arch=${{ matrix.arch }}
env:
OBFUSCATE: '1'
- name: Import code signing certificate
if: steps.signing.outputs.available == 'true'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
# Decode certificate
echo $MACOS_CERTIFICATE | base64 --decode > $RUNNER_TEMP/certificate.p12
# Create and configure keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security default-keychain -s $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
security import $RUNNER_TEMP/certificate.p12 -P "$MACOS_CERTIFICATE_PWD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Add to search list (keep existing keychains)
security list-keychains -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | tr -d '"')
# Verify certificate was imported
echo "Installed certificates:"
security find-identity -v -p codesigning
echo "Certificate imported successfully"
- name: Build app (signed + notarized)
if: steps.signing.outputs.available == 'true'
env:
OBFUSCATE: '1'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npm run make -- --arch=${{ matrix.arch }}
- name: List build artifacts
run: |
echo "=== Build output for ${{ matrix.arch }} ==="
find out/make -type f \( -name "*.zip" -o -name "*.yml" -o -name "*.blockmap" -o -name "*.dmg" \) | head -20
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-artifacts
path: |
out/make/zip/darwin/**/*.zip
out/make/zip/darwin/**/*.zip.blockmap
out/make/zip/darwin/**/latest-*-mac.yml
out/make/*.dmg
if-no-files-found: error
# =============================================================================
# Windows Build (x64)
# =============================================================================
build-windows:
runs-on: windows-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout private repo
uses: actions/checkout@v4
with:
repository: ${{ env.PRIVATE_REPO }}
ref: ${{ github.event.client_payload.ref }}
submodules: recursive
token: ${{ secrets.GH_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Determine version
id: version
shell: bash
run: |
if [ -n "${{ github.event.client_payload.version }}" ]; then
VERSION="${{ github.event.client_payload.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION for Windows"
- name: Install dependencies
run: npm ci
- name: Build app
run: npm run make
env:
OBFUSCATE: '1'
- name: List build artifacts
shell: bash
run: |
echo "=== Build output for Windows ==="
find out/make -type f \( -name "*.exe" -o -name "*.nupkg" -o -name "*.msi" \) | head -20
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-x64-artifacts
path: |
out/make/squirrel.windows/**/*.exe
out/make/squirrel.windows/**/*.nupkg
if-no-files-found: error
# =============================================================================
# Linux Build (x64)
# =============================================================================
build-linux:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout private repo
uses: actions/checkout@v4
with:
repository: ${{ env.PRIVATE_REPO }}
ref: ${{ github.event.client_payload.ref }}
submodules: recursive
token: ${{ secrets.GH_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Determine version
id: version
run: |
if [ -n "${{ github.event.client_payload.version }}" ]; then
VERSION="${{ github.event.client_payload.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION for Linux"
- name: Install dependencies
run: npm ci
- name: Install Linux build dependencies
run: |
sudo apt-get update
sudo apt-get install -y dpkg fakeroot rpm
- name: Build app
run: npm run make
env:
OBFUSCATE: '1'
- name: List build artifacts
run: |
echo "=== Build output for Linux ==="
find out/make -type f \( -name "*.deb" -o -name "*.rpm" \) | head -20
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-x64-artifacts
path: |
out/make/deb/**/*.deb
out/make/rpm/**/*.rpm
if-no-files-found: error
# =============================================================================
# Upload all artifacts to R2
# =============================================================================
upload-to-r2:
if: always()
needs: [build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- name: Prepare artifact directories
run: |
mkdir -p artifacts/macos-arm64 artifacts/macos-x64 artifacts/windows artifacts/linux
- name: Download macOS arm64 artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: macos-arm64-artifacts
path: artifacts/macos-arm64
- name: Download macOS x64 artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: macos-x64-artifacts
path: artifacts/macos-x64
- name: Download Windows artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: windows-x64-artifacts
path: artifacts/windows
- name: Download Linux artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: linux-x64-artifacts
path: artifacts/linux
- name: List downloaded artifacts
run: |
echo "=== Downloaded artifacts ==="
find artifacts -type f
- name: Install rclone
run: |
curl -O https://downloads.rclone.org/rclone-current-linux-amd64.deb
sudo dpkg -i rclone-current-linux-amd64.deb
- name: Configure rclone for R2
run: |
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf << EOF
[r2]
type = s3
provider = Cloudflare
access_key_id = ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY }}
secret_access_key = ${{ secrets.CLOUDFLARE_R2_SECRET_KEY }}
endpoint = ${{ env.R2_ENDPOINT }}
acl = private
no_check_bucket = true
EOF
- name: Upload to R2
run: |
VERSION="${{ github.event.client_payload.version }}"
echo "Uploading version $VERSION to R2..."
# =====================================================================
# macOS uploads (arm64 + x64)
# =====================================================================
echo ""
echo "Uploading macOS artifacts..."
# Upload update manifest files for electron-updater
# New naming: latest-arm64-mac.yml, latest-x64-mac.yml
find artifacts/macos-arm64 artifacts/macos-x64 -name "latest-*-mac.yml" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
# Also create copies with OLD naming for backward compatibility with <= 0.0.35 clients
# Old clients look for: latest-mac-arm64-mac.yml, latest-mac-x64-mac.yml
ARM64_MANIFEST=$(find artifacts/macos-arm64 -name "latest-arm64-mac.yml" | head -1)
X64_MANIFEST=$(find artifacts/macos-x64 -name "latest-x64-mac.yml" | head -1)
if [ -n "$ARM64_MANIFEST" ]; then
echo " Creating backward-compat copy: latest-mac-arm64-mac.yml"
rclone copyto "$ARM64_MANIFEST" r2:${{ env.R2_BUCKET }}/latest-mac-arm64-mac.yml
fi
if [ -n "$X64_MANIFEST" ]; then
echo " Creating backward-compat copy: latest-mac-x64-mac.yml"
rclone copyto "$X64_MANIFEST" r2:${{ env.R2_BUCKET }}/latest-mac-x64-mac.yml
fi
# Upload ZIP files and blockmaps
find artifacts/macos-arm64 artifacts/macos-x64 -name "*.zip" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
find artifacts/macos-arm64 artifacts/macos-x64 -name "*.zip.blockmap" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
# Upload DMGs
find artifacts/macos-arm64 artifacts/macos-x64 -name "*.dmg" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
# Copy DMGs to stable "latest" URLs (architecture-specific)
# Use while read to handle filenames with spaces
find artifacts/macos-arm64 artifacts/macos-x64 -name "*.dmg" -print0 | while IFS= read -r -d '' DMG_FILE; do
FILENAME=$(basename "$DMG_FILE")
# Extract arch from filename (e.g., "Open Anonymity-0.0.25-arm64.dmg" -> "arm64")
if [[ "$FILENAME" =~ -([^-]+)\.dmg$ ]]; then
ARCH="${BASH_REMATCH[1]}"
STABLE_NAME="Open-Anonymity-latest-mac-${ARCH}.dmg"
echo " Copying $FILENAME -> $STABLE_NAME"
rclone copyto "$DMG_FILE" r2:${{ env.R2_BUCKET }}/$STABLE_NAME
fi
done
# =====================================================================
# Windows uploads
# =====================================================================
echo ""
echo "Uploading Windows artifacts..."
# Upload all Windows installers
find artifacts/windows -name "*.exe" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
# Copy Setup.exe to stable "latest" URL
SETUP_EXE=$(find artifacts/windows -name "*Setup*.exe" | head -1)
if [ -n "$SETUP_EXE" ]; then
echo " Copying $(basename "$SETUP_EXE") -> Open-Anonymity-latest-win-x64.exe"
rclone copyto "$SETUP_EXE" r2:${{ env.R2_BUCKET }}/Open-Anonymity-latest-win-x64.exe
fi
# =====================================================================
# Linux uploads
# =====================================================================
echo ""
echo "Uploading Linux artifacts..."
# Upload all Linux packages
find artifacts/linux -name "*.deb" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
find artifacts/linux -name "*.rpm" -exec rclone copy {} r2:${{ env.R2_BUCKET }}/ \;
# Copy to stable "latest" URLs
DEB_FILE=$(find artifacts/linux -name "*.deb" | head -1)
if [ -n "$DEB_FILE" ]; then
echo " Copying $(basename "$DEB_FILE") -> Open-Anonymity-latest-linux-x64.deb"
rclone copyto "$DEB_FILE" r2:${{ env.R2_BUCKET }}/Open-Anonymity-latest-linux-x64.deb
fi
RPM_FILE=$(find artifacts/linux -name "*.rpm" | head -1)
if [ -n "$RPM_FILE" ]; then
echo " Copying $(basename "$RPM_FILE") -> Open-Anonymity-latest-linux-x64.rpm"
rclone copyto "$RPM_FILE" r2:${{ env.R2_BUCKET }}/Open-Anonymity-latest-linux-x64.rpm
fi
echo ""
echo "Upload complete!"
- name: Verify upload
run: |
echo "=== Files in R2 bucket ==="
rclone ls r2:${{ env.R2_BUCKET }}/ | head -30
- name: Print download URLs
run: |
echo ""
echo "============================================"
echo " Release uploaded successfully!"
echo "============================================"
echo ""
echo "Update server URL: https://cdn.openanonymity.ai"
echo "Version: ${{ github.event.client_payload.version }}"
echo ""
echo "Auto-update manifests:"
echo " - latest-arm64-mac.yml (new clients >= 0.0.36)"
echo " - latest-x64-mac.yml (new clients >= 0.0.36)"
echo " - latest-mac-arm64-mac.yml (old clients <= 0.0.35)"
echo " - latest-mac-x64-mac.yml (old clients <= 0.0.35)"
echo ""
echo "Stable download links (always latest):"
echo ""
echo " macOS (Apple Silicon): https://cdn.openanonymity.ai/Open-Anonymity-latest-mac-arm64.dmg"
echo " macOS (Intel): https://cdn.openanonymity.ai/Open-Anonymity-latest-mac-x64.dmg"
echo " Windows (64-bit): https://cdn.openanonymity.ai/Open-Anonymity-latest-win-x64.exe"
echo " Linux (DEB): https://cdn.openanonymity.ai/Open-Anonymity-latest-linux-x64.deb"
echo " Linux (RPM): https://cdn.openanonymity.ai/Open-Anonymity-latest-linux-x64.rpm"
echo ""
echo "macOS users will be notified of updates automatically."
echo ""
- name: Notify private repo of completion
if: always()
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
https://api.github.com/repos/${{ env.PRIVATE_REPO }}/dispatches \
-d '{"event_type":"build-complete","client_payload":{"version":"${{ github.event.client_payload.version }}","status":"${{ job.status }}","run_id":"${{ github.run_id }}","run_url":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}}'