mirror of
https://github.com/exo-explore/exo.git
synced 2025-12-23 22:27:50 -05:00
291 lines
12 KiB
YAML
291 lines
12 KiB
YAML
name: Build EXO macOS DMG
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
|
|
jobs:
|
|
build-macos-app:
|
|
runs-on: "macos-26"
|
|
env:
|
|
SPARKLE_VERSION: 2.8.1
|
|
SPARKLE_DOWNLOAD_PREFIX: ${{ secrets.SPARKLE_DOWNLOAD_PREFIX }}
|
|
SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
|
|
SPARKLE_ED25519_PUBLIC: ${{ secrets.SPARKLE_ED25519_PUBLIC }}
|
|
SPARKLE_ED25519_PRIVATE: ${{ secrets.SPARKLE_ED25519_PRIVATE }}
|
|
SPARKLE_S3_BUCKET: ${{ secrets.SPARKLE_S3_BUCKET }}
|
|
SPARKLE_S3_PREFIX: ${{ secrets.SPARKLE_S3_PREFIX }}
|
|
AWS_REGION: ${{ secrets.AWS_REGION }}
|
|
EXO_BUILD_NUMBER: ${{ github.run_number }}
|
|
EXO_LIBP2P_NAMESPACE: ${{ github.ref_name }}
|
|
|
|
steps:
|
|
# ============================================================
|
|
# Checkout and tag validation
|
|
# ============================================================
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Derive release version from tag
|
|
run: |
|
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
if [[ "$VERSION" == *-alpha* ]]; then
|
|
echo "IS_ALPHA=true" >> $GITHUB_ENV
|
|
else
|
|
echo "IS_ALPHA=false" >> $GITHUB_ENV
|
|
fi
|
|
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
|
|
|
|
- name: Ensure tag commit is on main
|
|
run: |
|
|
git fetch origin main
|
|
# Alpha tags can be on any branch, production tags must be on main
|
|
if [[ "$IS_ALPHA" == "true" ]]; then
|
|
echo "Alpha tag detected, skipping main branch check"
|
|
elif ! git merge-base --is-ancestor origin/main HEAD; then
|
|
echo "Production tag must point to a commit on main"
|
|
exit 1
|
|
fi
|
|
|
|
# ============================================================
|
|
# Install dependencies
|
|
# ============================================================
|
|
|
|
- name: Select Xcode 26.2
|
|
run: |
|
|
sudo xcode-select -s /Applications/Xcode_26.2.app
|
|
if ! xcrun -f metal >/dev/null 2>&1; then
|
|
echo "Metal toolchain is not installed."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Install Homebrew packages
|
|
run: brew install just awscli macmon
|
|
|
|
- name: Install UV
|
|
uses: astral-sh/setup-uv@v6
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: uv.lock
|
|
|
|
- name: Setup Python
|
|
run: |
|
|
uv python install
|
|
uv sync --locked
|
|
|
|
- name: Build dashboard
|
|
run: |
|
|
cd dashboard
|
|
npm ci
|
|
npm run build
|
|
|
|
- name: Install Sparkle CLI
|
|
run: |
|
|
CLI_URL="${SPARKLE_CLI_URL:-https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz}"
|
|
echo "Downloading Sparkle CLI from: $CLI_URL"
|
|
mkdir -p /tmp/sparkle
|
|
curl --fail --location --output /tmp/sparkle.tar.xz "$CLI_URL"
|
|
tar -xJf /tmp/sparkle.tar.xz -C /tmp/sparkle --strip-components=1
|
|
echo "SPARKLE_BIN=/tmp/sparkle/bin" >> $GITHUB_ENV
|
|
|
|
- name: Prepare code-signing keychain
|
|
env:
|
|
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
|
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
|
|
run: |
|
|
KEYCHAIN_PATH="$HOME/Library/Keychains/build.keychain-db"
|
|
|
|
# Create fresh keychain
|
|
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Disable auto-lock (no timeout, no lock-on-sleep)
|
|
security set-keychain-settings "$KEYCHAIN_PATH"
|
|
|
|
# Add to search list while preserving existing keychains
|
|
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
|
|
|
|
# Set as default and unlock
|
|
security default-keychain -s "$KEYCHAIN_PATH"
|
|
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Import certificate with full access for codesign
|
|
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/cert.p12
|
|
security import /tmp/cert.p12 -k "$KEYCHAIN_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" \
|
|
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
|
|
rm /tmp/cert.p12
|
|
|
|
# Allow codesign to access the key without prompting
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
|
|
|
|
# Verify keychain is unlocked and identity is available
|
|
echo "Verifying signing identity..."
|
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
|
|
|
# Setup provisioning profile
|
|
mkdir -p "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
|
echo "$PROVISIONING_PROFILE" | base64 --decode > "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/EXO.provisionprofile"
|
|
|
|
# Export keychain path for other steps
|
|
echo "BUILD_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV
|
|
|
|
# ============================================================
|
|
# Build the bundle
|
|
# ============================================================
|
|
|
|
- name: Build PyInstaller bundle
|
|
run: uv run pyinstaller packaging/pyinstaller/exo.spec
|
|
|
|
- name: Build Swift app
|
|
env:
|
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
|
SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
|
|
SPARKLE_ED25519_PUBLIC: ${{ secrets.SPARKLE_ED25519_PUBLIC }}
|
|
run: |
|
|
cd app/EXO
|
|
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
|
|
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
|
|
xcodebuild clean build \
|
|
-scheme EXO \
|
|
-configuration Release \
|
|
-derivedDataPath build \
|
|
MARKETING_VERSION="$RELEASE_VERSION" \
|
|
CURRENT_PROJECT_VERSION="$EXO_BUILD_NUMBER" \
|
|
EXO_BUILD_TAG="$RELEASE_VERSION" \
|
|
EXO_BUILD_COMMIT="$GITHUB_SHA" \
|
|
SPARKLE_FEED_URL="$SPARKLE_FEED_URL" \
|
|
SPARKLE_ED25519_PUBLIC="$SPARKLE_ED25519_PUBLIC" \
|
|
CODE_SIGNING_IDENTITY="$SIGNING_IDENTITY" \
|
|
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=YES
|
|
mkdir -p ../../output
|
|
cp -R build/Build/Products/Release/EXO.app ../../output/EXO.app
|
|
|
|
- name: Inject PyInstaller runtime
|
|
run: |
|
|
rm -rf output/EXO.app/Contents/Resources/exo
|
|
mkdir -p output/EXO.app/Contents/Resources
|
|
cp -R dist/exo output/EXO.app/Contents/Resources/exo
|
|
|
|
- name: Codesign PyInstaller runtime
|
|
env:
|
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
|
run: |
|
|
cd output
|
|
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
|
|
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
|
|
RUNTIME_DIR="EXO.app/Contents/Resources/exo"
|
|
find "$RUNTIME_DIR" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 |
|
|
while IFS= read -r -d '' file; do
|
|
/usr/bin/codesign --force --timestamp --options runtime \
|
|
--sign "$SIGNING_IDENTITY" "$file"
|
|
done
|
|
|
|
- name: Sign, notarize, and create DMG
|
|
env:
|
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
|
APPLE_NOTARIZATION_TEAM: ${{ secrets.APPLE_NOTARIZATION_TEAM }}
|
|
run: |
|
|
cd output
|
|
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" "$BUILD_KEYCHAIN_PATH"
|
|
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$BUILD_KEYCHAIN_PATH" | awk -F '"' '{print $2}')
|
|
/usr/bin/codesign --deep --force --timestamp --options runtime \
|
|
--sign "$SIGNING_IDENTITY" EXO.app
|
|
mkdir -p dmg-root
|
|
cp -R EXO.app dmg-root/
|
|
ln -s /Applications dmg-root/Applications
|
|
DMG_NAME="EXO-${RELEASE_VERSION}.dmg"
|
|
hdiutil create -volname "EXO" -srcfolder dmg-root -ov -format UDZO "$DMG_NAME"
|
|
/usr/bin/codesign --force --timestamp --options runtime \
|
|
--sign "$SIGNING_IDENTITY" "$DMG_NAME"
|
|
if [[ -n "$APPLE_NOTARIZATION_USERNAME" ]]; then
|
|
SUBMISSION_OUTPUT=$(xcrun notarytool submit "$DMG_NAME" \
|
|
--apple-id "$APPLE_NOTARIZATION_USERNAME" \
|
|
--password "$APPLE_NOTARIZATION_PASSWORD" \
|
|
--team-id "$APPLE_NOTARIZATION_TEAM" \
|
|
--wait --timeout 15m 2>&1)
|
|
echo "$SUBMISSION_OUTPUT"
|
|
|
|
SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | awk 'tolower($1)=="id:" && $2 ~ /^[0-9a-fA-F-]+$/ {print $2; exit}')
|
|
STATUS=$(echo "$SUBMISSION_OUTPUT" | awk 'tolower($1)=="status:" {print $2; exit}')
|
|
|
|
if [[ -n "$SUBMISSION_ID" ]]; then
|
|
xcrun notarytool log "$SUBMISSION_ID" \
|
|
--apple-id "$APPLE_NOTARIZATION_USERNAME" \
|
|
--password "$APPLE_NOTARIZATION_PASSWORD" \
|
|
--team-id "$APPLE_NOTARIZATION_TEAM" > notarization-log.txt || true
|
|
echo "===== Notarization Log ====="
|
|
cat notarization-log.txt
|
|
echo "============================"
|
|
fi
|
|
|
|
if [[ "$STATUS" != "Accepted" ]]; then
|
|
echo "Notarization failed with status: ${STATUS:-Unknown}"
|
|
exit 1
|
|
fi
|
|
|
|
xcrun stapler staple "$DMG_NAME"
|
|
fi
|
|
|
|
- name: Generate Sparkle appcast
|
|
env:
|
|
SPARKLE_DOWNLOAD_PREFIX: ${{ env.SPARKLE_DOWNLOAD_PREFIX }}
|
|
SPARKLE_ED25519_PRIVATE: ${{ secrets.SPARKLE_ED25519_PRIVATE }}
|
|
IS_ALPHA: ${{ env.IS_ALPHA }}
|
|
run: |
|
|
set -euo pipefail
|
|
cd output
|
|
DOWNLOAD_PREFIX="${SPARKLE_DOWNLOAD_PREFIX:-https://assets.exolabs.net}"
|
|
echo "$SPARKLE_ED25519_PRIVATE" > sparkle_ed25519.key
|
|
chmod 600 sparkle_ed25519.key
|
|
|
|
CHANNEL_FLAG=""
|
|
if [[ "$IS_ALPHA" == "true" ]]; then
|
|
CHANNEL_FLAG="--channel alpha"
|
|
echo "Generating appcast for alpha channel"
|
|
fi
|
|
|
|
$SPARKLE_BIN/generate_appcast \
|
|
--ed-key-file sparkle_ed25519.key \
|
|
--download-url-prefix "$DOWNLOAD_PREFIX" \
|
|
$CHANNEL_FLAG \
|
|
.
|
|
|
|
# ============================================================
|
|
# Upload artifacts
|
|
# ============================================================
|
|
|
|
- name: Upload DMG
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: EXO-dmg-${{ env.RELEASE_VERSION }}
|
|
path: output/EXO-${{ env.RELEASE_VERSION }}.dmg
|
|
|
|
- name: Upload to S3
|
|
if: env.SPARKLE_S3_BUCKET != ''
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
AWS_REGION: ${{ env.AWS_REGION }}
|
|
SPARKLE_S3_BUCKET: ${{ env.SPARKLE_S3_BUCKET }}
|
|
SPARKLE_S3_PREFIX: ${{ env.SPARKLE_S3_PREFIX }}
|
|
IS_ALPHA: ${{ env.IS_ALPHA }}
|
|
run: |
|
|
set -euo pipefail
|
|
cd output
|
|
PREFIX="${SPARKLE_S3_PREFIX:-}"
|
|
if [[ -n "$PREFIX" && "${PREFIX: -1}" != "/" ]]; then
|
|
PREFIX="${PREFIX}/"
|
|
fi
|
|
DMG_NAME="EXO-${RELEASE_VERSION}.dmg"
|
|
aws s3 cp "$DMG_NAME" "s3://${SPARKLE_S3_BUCKET}/${PREFIX}${DMG_NAME}"
|
|
if [[ "$IS_ALPHA" != "true" ]]; then
|
|
aws s3 cp "$DMG_NAME" "s3://${SPARKLE_S3_BUCKET}/${PREFIX}EXO-latest.dmg"
|
|
fi
|
|
aws s3 cp appcast.xml "s3://${SPARKLE_S3_BUCKET}/${PREFIX}appcast.xml" --content-type application/xml --cache-control no-cache
|