build-nym-vpn-apple #63
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: build-nym-vpn-apple | |
on: | |
pull_request: | |
types: [opened, synchronize, reopened, ready_for_review] | |
paths: | |
- ".github/workflows/build-nym-vpn-apple.yml" | |
- "nym-vpn-apple/**" | |
workflow_dispatch: | |
inputs: | |
release_type: | |
description: "Select build mode" | |
type: choice | |
options: | |
- "pr — Pull Request (unsigned debug build)" | |
- "qa — QA Release (signed, automatic, TestFlight/App Store export)" | |
- "ship — Ship Release (signed, automatic, App Store distribution)" | |
default: "pr — Pull Request (unsigned debug build)" | |
workflow_call: | |
inputs: | |
release_type: | |
description: "Build type: pr | qa | ship (raw or labeled)" | |
type: string | |
default: "pr" | |
outputs: | |
RUST_VERSION: | |
value: ${{ jobs.build-apple.outputs.RUST_VERSION }} | |
env: | |
CARGO_TERM_COLOR: always | |
UPLOAD_DIR_IOS: ios_artifacts | |
RAW_RELEASE_TYPE: ${{ (github.event_name == 'pull_request' && 'pr') || inputs.release_type || 'pr' }} | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
cancel-in-progress: true | |
jobs: | |
build-apple: | |
if: github.actor != 'dependabot[bot]' | |
runs-on: AppleSilicon | |
timeout-minutes: 60 | |
outputs: | |
UPLOAD_DIR_IOS: ${{ env.UPLOAD_DIR_IOS }} | |
RUST_VERSION: ${{ steps.rust-version.outputs.rustc }} | |
steps: | |
- name: Checkout repo | |
uses: actions/checkout@v4 | |
- name: Normalize release_type | |
run: | | |
sel="${{ env.RAW_RELEASE_TYPE }}" | |
case "$sel" in | |
pr*|PR*) mode=pr ;; | |
qa*|QA*) mode=qa ;; | |
ship*|SHIP*) mode=ship ;; | |
*) mode=pr ;; | |
esac | |
echo "RELEASE_TYPE=$mode" >> "$GITHUB_ENV" | |
- name: Show selected release type | |
run: echo "RELEASE_TYPE=${{ env.RELEASE_TYPE }}" | |
- name: Install rust toolchain | |
uses: dtolnay/rust-toolchain@master | |
with: | |
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }} | |
components: rustfmt, clippy | |
targets: x86_64-apple-darwin aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim | |
- name: Install cargo-swift | |
run: | | |
cargo install \ | |
--git https://github.com/antoniusnaumann/cargo-swift \ | |
--rev 53b948e8f37dd018300ae3fee2d0fd5ece59e2cd \ | |
cargo-swift | |
- name: Install cargo-license | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
run: | | |
cargo install cargo-license | |
- name: Setup Node (for appdmg) | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20' | |
- name: Install appdmg | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
run: npm install -g appdmg | |
- name: Install Protoc | |
uses: arduino/setup-protoc@v3 | |
with: | |
repo-token: ${{ secrets.GITHUB_TOKEN }} | |
- name: Install Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version: ${{ vars.REQUIRED_GOLANG_VERSION }} | |
cache: false | |
- name: Get workspace version | |
id: workspace-version | |
uses: nicolaiunrein/cargo-get@master | |
with: | |
subcommand: workspace.package.version --entry nym-vpn-core | |
- name: Install cargo-edit | |
uses: baptiste0928/cargo-install@v3 | |
with: | |
crate: cargo-edit | |
- name: Add existing Homebrew to PATH (no install) | |
run: | | |
set -euxo pipefail | |
for HB in /opt/homebrew/bin/brew /usr/local/bin/brew; do | |
if [ -x "$HB" ]; then | |
echo "$(dirname "$HB")" >> "$GITHUB_PATH" # future steps | |
export PATH="$(dirname "$HB"):$PATH" # this step | |
eval "$("$HB" shellenv)" # set Homebrew env vars | |
{ | |
echo "HOMEBREW_PREFIX=$HOMEBREW_PREFIX" | |
echo "HOMEBREW_CELLAR=$HOMEBREW_CELLAR" | |
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" | |
} >> "$GITHUB_ENV" | |
"$HB" --version | |
exit 0 | |
fi | |
done | |
echo "❌ Homebrew binary not found at /opt/homebrew/bin/brew or /usr/local/bin/brew" >&2 | |
exit 1 | |
- name: Update prebundled servers | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
working-directory: nym-vpn-apple/scripts | |
run: | | |
sh UpdatePrebundledServers.sh | |
- name: Enable QA mode | |
if: env.RELEASE_TYPE == 'qa' | |
working-directory: nym-vpn-apple | |
run: | | |
sed -E ':a;N;$!ba; s|(</dict>[[:space:]]*</plist>)| <key>EnvironmentVariables</key>\n <dict>\n <key>RUST_LOG</key>\n <string>debug</string>\n </dict>\n\1|' "Daemon/net.nymtech.vpn.helper.plist" | |
- name: Update licences | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
run: | | |
cargo license -j --avoid-dev-deps --current-dir ./nym-vpn-core --filter-platform x86_64-apple-darwin --avoid-build-deps > ./nym-vpn-apple/NymVPN/Resources/LibLicences.json | |
- name: Install Fastlane | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
run: | | |
brew install fastlane | |
- name: Build core | |
working-directory: nym-vpn-apple/scripts | |
env: | |
VPNLIB_SENTRY_DSN: ${{ secrets.VPND_SENTRY_DSN }} | |
run: | | |
sh BuildCore.sh | |
- name: Build iOS (PR, unsigned Debug) | |
if: env.RELEASE_TYPE == 'pr' | |
working-directory: nym-vpn-apple | |
env: | |
IPHONEOS_DEPLOYMENT_TARGET: 16.0 | |
run: | | |
set -euxo pipefail | |
DERIVED_DATA="$PWD/.DerivedData-iOS" | |
xcodebuild \ | |
-workspace NymVPN.xcworkspace \ | |
-scheme NymVPN \ | |
-configuration Debug \ | |
-destination 'generic/platform=iOS' \ | |
-sdk iphoneos \ | |
-derivedDataPath "$DERIVED_DATA" \ | |
CODE_SIGNING_ALLOWED=NO \ | |
DEVELOPMENT_TEAM= \ | |
build | |
find "$DERIVED_DATA/Build/Products" -maxdepth 3 -name '*.app' -print || true | |
- name: Prepare App Store Connect API key | |
if: env.RELEASE_TYPE != 'pr' | |
id: asc-key | |
working-directory: nym-vpn-apple | |
env: | |
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
ASC_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }} | |
run: | | |
set -euo pipefail | |
KEY_PATH="$RUNNER_TEMP/AuthKey_${ASC_KEY_ID}.p8" | |
echo "$ASC_KEY_BASE64" | base64 --decode > "$KEY_PATH" | |
chmod 600 "$KEY_PATH" | |
echo "key_path=$KEY_PATH" >> "$GITHUB_OUTPUT" | |
# - name: Archive iOS (QA/Ship, Distribution, Automatic, ASC auth) | |
# if: env.RELEASE_TYPE != 'pr' | |
# id: ios-archive | |
# working-directory: nym-vpn-apple | |
# env: | |
# IPHONEOS_DEPLOYMENT_TARGET: 16.0 | |
# DEVELOPMENT_TEAM: ${{ secrets.APPLE_TEAM_ID }} | |
# run: | | |
# set -euxo pipefail | |
# ARCHIVE_PATH="$PWD/build/NymVPN-iOS.xcarchive" | |
# xcrun xcodebuild \ | |
# -workspace NymVPN.xcworkspace \ | |
# -scheme NymVPN \ | |
# -configuration Release \ | |
# -destination 'generic/platform=iOS' \ | |
# -sdk iphoneos \ | |
# -archivePath "$ARCHIVE_PATH" \ | |
# CODE_SIGN_STYLE=Automatic \ | |
# CODE_SIGNING_ALLOWED=YES \ | |
# DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ | |
# -allowProvisioningUpdates \ | |
# -authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}" \ | |
# -authenticationKeyID "${{ secrets.APP_STORE_CONNECT_API_KEY }}" \ | |
# -authenticationKeyPath "${{ steps.asc-key.outputs.key_path }}" \ | |
# clean archive | |
# echo "archive_path=$ARCHIVE_PATH" >> "$GITHUB_OUTPUT" | |
# - name: Export IPA (QA/Ship – App Store Connect, ASC auth) | |
# if: env.RELEASE_TYPE != 'pr' | |
# working-directory: nym-vpn-apple | |
# run: | | |
# set -euxo pipefail | |
# EXPORT_DIR="$PWD/build/export-${{ env.RELEASE_TYPE }}" | |
# mkdir -p "$EXPORT_DIR" | |
# cat > exportOptions.plist <<'EOF' | |
# <?xml version="1.0" encoding="UTF-8"?> | |
# <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
# <plist version="1.0"> | |
# <dict> | |
# <key>method</key><string>app-store-connect</string> | |
# <key>signingStyle</key><string>automatic</string> | |
# <key>destination</key><string>export</string> | |
# <key>stripSwiftSymbols</key><true/> | |
# <key>compileBitcode</key><false/> | |
# </dict> | |
# </plist> | |
# EOF | |
# xcrun xcodebuild -exportArchive \ | |
# -archivePath "${{ steps.ios-archive.outputs.archive_path }}" \ | |
# -exportOptionsPlist exportOptions.plist \ | |
# -exportPath "$EXPORT_DIR" \ | |
# -allowProvisioningUpdates \ | |
# -authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}" \ | |
# -authenticationKeyID "${{ secrets.APP_STORE_CONNECT_API_KEY }}" \ | |
# -authenticationKeyPath "${{ steps.asc-key.outputs.key_path }}" | |
# echo "Exported files:"; ls -lah "$EXPORT_DIR" | |
# - name: Upload to TestFlight (QA) | |
# if: env.RELEASE_TYPE == 'qa' | |
# working-directory: nym-vpn-apple | |
# env: | |
# ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
# ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
# ASC_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }} | |
# run: | | |
# set -euxo pipefail | |
# IPA="$(ls build/export-qa/*.ipa | head -n1)" | |
# fastlane ios upload_testflight ipa_path:"$IPA" changelog:"QA build ${GITHUB_SHA}" | |
- name: Archive macOS (QA/Ship, signed Release, automatic) | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
id: mac-archive | |
working-directory: nym-vpn-apple | |
env: | |
MACOSX_DEPLOYMENT_TARGET: 13.0 | |
DEVELOPMENT_TEAM: ${{ secrets.APPLE_TEAM_ID }} | |
run: | | |
set -euxo pipefail | |
ARCHIVE_PATH="$PWD/build/NymVPNDaemon-macOS.xcarchive" | |
xcodebuild \ | |
-workspace NymVPN.xcworkspace \ | |
-scheme NymVPNDaemon \ | |
-configuration Release \ | |
-destination 'generic/platform=macOS' \ | |
-archivePath "$ARCHIVE_PATH" \ | |
DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM}" \ | |
CODE_SIGN_STYLE=Automatic \ | |
CODE_SIGNING_ALLOWED=YES \ | |
-allowProvisioningUpdates \ | |
clean archive | |
echo "archive_path=$ARCHIVE_PATH" >> "$GITHUB_OUTPUT" | |
- name: Build macOS (PR, unsigned Debug) | |
if: env.RELEASE_TYPE == 'pr' | |
working-directory: nym-vpn-apple | |
env: | |
MACOSX_DEPLOYMENT_TARGET: 13.0 | |
run: | | |
set -euxo pipefail | |
DERIVED_DATA="$PWD/.DerivedData-macOS" | |
xcodebuild \ | |
-workspace NymVPN.xcworkspace \ | |
-scheme NymVPNDaemon \ | |
-configuration Debug \ | |
-destination 'generic/platform=macOS' \ | |
-derivedDataPath "$DERIVED_DATA" \ | |
CODE_SIGNING_ALLOWED=NO \ | |
DEVELOPMENT_TEAM= \ | |
build | |
find "$DERIVED_DATA/Build/Products" -maxdepth 3 -name '*.app' -print || true | |
- name: Create DMG | |
if: ${{ env.RELEASE_TYPE == 'qa' || env.RELEASE_TYPE == 'ship' }} | |
id: make-dmg | |
working-directory: nym-vpn-apple | |
run: | | |
set -euo pipefail | |
# Locate the .app | |
if [[ "${RELEASE_TYPE}" == "pr" ]]; then | |
ROOT="./.DerivedData-macOS/Build/Products" | |
APP_PATH="$(find "$ROOT" -type d -name 'NymVPN*.app' -print -quit || true)" | |
[[ -z "${APP_PATH:-}" ]] && APP_PATH="$(find "$ROOT" -type d -name '*.app' -print -quit || true)" | |
else | |
ARCH="${{ steps.mac-archive.outputs.archive_path }}/Products/Applications" | |
APP_PATH="$(find "$ARCH" -maxdepth 1 -type d -name 'NymVPN*.app' -print -quit || true)" | |
[[ -z "${APP_PATH:-}" ]] && APP_PATH="$(find "$ARCH" -maxdepth 1 -type d -name '*.app' -print -quit || true)" | |
fi | |
if [[ -z "${APP_PATH:-}" || ! -d "$APP_PATH" ]]; then | |
echo "❌ Could not locate a .app to package" >&2 | |
exit 1 | |
fi | |
echo "✅ Using app: $APP_PATH" | |
# DMG assets (from NymVPN/Resources) | |
BG="$PWD/NymVPN/Resources/background_dmg.png" | |
ICON="$PWD/NymVPN/Resources/macDmgIcon.icns" | |
if [[ ! -f "$BG" || ! -f "$ICON" ]]; then | |
echo "❌ Missing DMG assets:" | |
echo " $BG" | |
echo " $ICON" | |
exit 1 | |
fi | |
OUT_DIR="$PWD/build/dmg" | |
mkdir -p "$OUT_DIR" | |
DMG_NAME="NymVPN_${RELEASE_TYPE}.dmg" | |
# appdmg spec | |
cat > appdmg.json <<'JSON' | |
{ | |
"title": "NymVPN Installer", | |
"background": "__BG__", | |
"icon": "__ICON__", | |
"contents": [ | |
{ "x": 607, "y": 290, "type": "link", "path": "/Applications" }, | |
{ "x": 192, "y": 290, "type": "file", "path": "__APP__" } | |
], | |
"icon-size": 160, | |
"window": { "size": { "width": 800, "height": 600 } } | |
} | |
JSON | |
# Inject paths (macOS sed needs the '' arg) | |
sed -i '' "s#__BG__#${BG//\//\\/}#g" appdmg.json | |
sed -i '' "s#__ICON__#${ICON//\//\\/}#g" appdmg.json | |
sed -i '' "s#__APP__#${APP_PATH//\//\\/}#g" appdmg.json | |
# Create DMG | |
appdmg appdmg.json "$OUT_DIR/$DMG_NAME" | |
echo "dmg_path=$OUT_DIR/$DMG_NAME" >> "$GITHUB_OUTPUT" | |
echo "✅ DMG created at: $OUT_DIR/$DMG_NAME" | |
- name: Notarize DMG with notarytool (QA/Ship) | |
if: env.RELEASE_TYPE != 'pr' | |
id: notarize-dmg | |
working-directory: nym-vpn-apple | |
env: | |
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} | |
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
ASC_KEY_PATH: ${{ steps.asc-key.outputs.key_path }} # from your earlier "Prepare App Store Connect API key" step | |
run: | | |
set -euxo pipefail | |
# If your DMG step exposes an output like `dmg_path`, prefer that: | |
if [ -n "${{ steps.make-dmg.outputs.dmg_path || '' }}" ]; then | |
DMG="${{ steps.make-dmg.outputs.dmg_path }}" | |
else | |
# Otherwise, fall back to a deterministic path/name you created earlier: | |
DMG="$PWD/build/dmg/NymVPN_${RELEASE_TYPE}.dmg" | |
fi | |
test -f "$DMG" || { echo "❌ DMG not found at $DMG"; exit 1; } | |
xcrun notarytool submit "$DMG" \ | |
--key "$ASC_KEY_PATH" \ | |
--key-id "$ASC_KEY_ID" \ | |
--issuer "$ASC_ISSUER_ID" \ | |
--wait \ | |
--output-format json | tee "$PWD/build/dmg/notarytool-log.json" | |
# Staple the ticket into the DMG and validate | |
xcrun stapler staple "$DMG" | |
xcrun stapler validate "$DMG" | |
echo "dmg_path=$DMG" >> "$GITHUB_OUTPUT" | |
# # ---------- Artifacts (helpful for QA/Ship) ---------- | |
# - name: Upload artifacts | |
# if: always() | |
# uses: actions/upload-artifact@v4 | |
# with: | |
# name: apple-builds-${{ env.RELEASE_TYPE }}-${{ github.run_id }} | |
# path: | | |
# nym-vpn-apple/build/**/*.xcarchive | |
# nym-vpn-apple/build/**/export-qa/*.ipa | |
# nym-vpn-apple/build/**/export-ship/*.ipa | |
# nym-vpn-apple/.DerivedData*/Build/Products/**/*.app | |
# if-no-files-found: ignore | |
# retention-days: 5 |