Skip to content
Draft
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
157 changes: 157 additions & 0 deletions .github/workflows/mobile-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Mobile engineering builds

on:
pull_request:
paths:
- 'desktop/**'
- 'scripts/mobile.py'
- 'tests/test_mobile.py'
- 'tests/mobile_client.test.cjs'
- '.github/workflows/mobile-*.yml'
push:
branches: [main]
paths:
- 'desktop/**'
- 'scripts/mobile.py'
- 'tests/test_mobile.py'
- 'tests/mobile_client.test.cjs'
- '.github/workflows/mobile-*.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: mobile-test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
android:
name: Android debug APK + AAB, connect and return
runs-on: ubuntu-24.04
timeout-minutes: 55
env:
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_DEV_DEBUG: '0'
CARGO_PROFILE_DEV_STRIP: symbols
CARGO_BUILD_JOBS: '2'
NDK_VERSION: 29.0.13846066
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: desktop/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,x86_64-linux-android
- uses: android-actions/setup-android@v3
with:
packages: platform-tools
cmdline-tools-version: '15859902' # 22.0; understands Android 37.0 package IDs
log-accepted-android-sdk-licenses: false
- name: Install pinned Android SDK and NDK
run: |
sdkmanager 'platforms;android-37.0' 'build-tools;37.0.0' "ndk;$NDK_VERSION" 'platform-tools'
echo "NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
- name: Install locked Tauri CLI
run: npm ci
working-directory: desktop
- name: Test build/signing guardrails
run: |
python3 -m unittest discover -s tests -p test_mobile.py -v
node --test tests/mobile_client.test.cjs
- name: Build engineering artifacts (no release secrets)
run: python3 scripts/mobile.py build android --variant debug --target aarch64 --target x86_64 --format both --output artifacts/mobile/android
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Sideload APK and verify connection, isolation, and login return
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
target: google_apis
arch: x86_64
profile: pixel_6
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none
script: python3 scripts/mobile.py smoke-android artifacts/mobile/android/hireferry-android-aarch64-x86_64-debug.apk --output artifacts/mobile/android/smoke
- name: Upload engineering packages and verification evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: hireferry-android-debug-${{ github.sha }}
path: artifacts/mobile/android/
if-no-files-found: error
retention-days: 14

ios-simulator:
name: iOS simulator build (no signing identity)
runs-on: macos-15
timeout-minutes: 50
env:
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_DEV_DEBUG: '0'
CARGO_PROFILE_DEV_STRIP: symbols
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: desktop/package-lock.json
- uses: dtolnay/rust-toolchain@stable
- name: Install iOS project tools
run: |
brew list cocoapods >/dev/null 2>&1 || brew install cocoapods
xcodebuild -version
- name: Install locked Tauri CLI
run: npm ci
working-directory: desktop
- name: Build simulator application without distribution credentials
run: python3 scripts/mobile.py build ios --simulator --variant debug --output artifacts/mobile/ios
- name: Boot simulator and install actual application
run: |
python3 - <<'PY'
import json, pathlib, subprocess, tarfile, time
devices = json.loads(subprocess.check_output(['xcrun', 'simctl', 'list', 'devices', 'available', '-j']))['devices']
phones = [d for runtime, group in devices.items() if 'iOS' in runtime for d in group if d.get('isAvailable') and 'iPhone' in d['name']]
if not phones:
raise SystemExit('No available iPhone simulator runtime on runner')
device = phones[0]
udid = device['udid']
if device['state'] != 'Booted':
subprocess.run(['xcrun', 'simctl', 'boot', udid], check=True)
subprocess.run(['xcrun', 'simctl', 'bootstatus', udid, '-b'], check=True)
archive = next(pathlib.Path('artifacts/mobile/ios').glob('*.app.tar.gz'))
out = pathlib.Path('artifacts/mobile/ios/smoke')
out.mkdir()
with tarfile.open(archive) as source:
source.extractall(out, filter='data')
app = next(out.glob('*.app'))
subprocess.run(['xcrun', 'simctl', 'install', udid, str(app)], check=True)
subprocess.run(['xcrun', 'simctl', 'launch', udid, 'best.lmm.hireferry'], check=True)
time.sleep(5)
subprocess.run(['xcrun', 'simctl', 'io', udid, 'screenshot', str(out / 'ios-launch.png')], check=True)
subprocess.run(['xcrun', 'simctl', 'openurl', udid, 'hireferry://login-return'], check=True)
time.sleep(2)
subprocess.run(['xcrun', 'simctl', 'io', udid, 'screenshot', str(out / 'ios-login-return.png')], check=True)
(out / 'ios-smoke.json').write_text(json.dumps({'simulator':device['name'], 'installed':True, 'launch_command_succeeded':True, 'login_return_openurl_succeeded':True}, indent=2))
import shutil
shutil.rmtree(app)
PY
- name: Upload simulator app and launch evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: hireferry-ios-simulator-${{ github.sha }}
path: artifacts/mobile/ios/
if-no-files-found: error
retention-days: 14
140 changes: 140 additions & 0 deletions .github/workflows/mobile-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Signed mobile packages

# Explicit maintainer dispatch only. PR validation uses mobile-build.yml and
# never reads distribution signing credentials or publishes a store release.
on:
workflow_dispatch:
inputs:
platform:
description: Platform to sign
required: true
type: choice
options: [android, ios]
build_number:
description: Positive iOS build number (ignored by Android)
required: true
default: '1'
type: string
ios_export_method:
description: iOS distribution profile type
required: true
default: app-store-connect
type: choice
options: [app-store-connect, release-testing, debugging]

permissions:
contents: read

jobs:
android:
if: inputs.platform == 'android'
runs-on: ubuntu-24.04
environment: mobile-signing
timeout-minutes: 55
env:
CARGO_INCREMENTAL: '0'
NDK_VERSION: 29.0.13846066
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android
- uses: android-actions/setup-android@v3
with:
packages: platform-tools
cmdline-tools-version: '15859902' # 22.0; understands Android 37.0 package IDs
log-accepted-android-sdk-licenses: false
- name: Install pinned Android SDK and NDK
run: |
sdkmanager 'platforms;android-37.0' 'build-tools;37.0.0' "ndk;$NDK_VERSION" 'platform-tools'
echo "NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
- run: npm ci
working-directory: desktop
- name: Decode maintainer keystore in runner temporary storage
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
python3 - <<'PY'
import base64, os, pathlib
value = os.environ.get('ANDROID_KEYSTORE_BASE64', '')
if not value:
raise SystemExit('Configure ANDROID_KEYSTORE_BASE64 in the mobile-signing environment')
path = pathlib.Path(os.environ['RUNNER_TEMP']) / 'hireferry-release.keystore'
path.write_bytes(base64.b64decode(value, validate=True))
path.chmod(0o600)
with open(os.environ['GITHUB_ENV'], 'a') as output:
output.write('HF_ANDROID_KEYSTORE_PATH=' + str(path) + '\n')
PY
- name: Build and verify signed APK and AAB
env:
HF_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
HF_ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
HF_ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
HF_ANDROID_CERT_SHA256: ${{ vars.ANDROID_CERT_SHA256 }}
run: python3 scripts/mobile.py build android --variant release --target aarch64 --format both --output artifacts/mobile/android
- name: Remove decoded keystore
if: always()
run: rm -f "$RUNNER_TEMP/hireferry-release.keystore"
- uses: actions/upload-artifact@v4
with:
name: hireferry-android-signed-${{ github.sha }}
path: artifacts/mobile/android/
if-no-files-found: error
retention-days: 14

ios:
if: inputs.platform == 'ios'
runs-on: macos-15
environment: mobile-signing
timeout-minutes: 55
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-ios
- name: Install iOS project tools
run: brew list cocoapods >/dev/null 2>&1 || brew install cocoapods
- run: npm ci
working-directory: desktop
- name: Validate signing inputs
env:
APPLE_DEVELOPMENT_TEAM: ${{ vars.APPLE_DEVELOPMENT_TEAM }}
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_MOBILE_PROVISION: ${{ secrets.IOS_MOBILE_PROVISION }}
BUILD_NUMBER: ${{ inputs.build_number }}
run: |
python3 - <<'PY'
import os
keys = ('APPLE_DEVELOPMENT_TEAM', 'IOS_CERTIFICATE', 'IOS_CERTIFICATE_PASSWORD', 'IOS_MOBILE_PROVISION')
missing = [key for key in keys if not os.environ.get(key)]
if missing:
raise SystemExit('Configure real iOS signing inputs: ' + ', '.join(missing))
if not os.environ['BUILD_NUMBER'].isdigit() or int(os.environ['BUILD_NUMBER']) < 1:
raise SystemExit('build_number must be a positive integer')
PY
- name: Build and verify signed IPA
env:
APPLE_DEVELOPMENT_TEAM: ${{ vars.APPLE_DEVELOPMENT_TEAM }}
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_MOBILE_PROVISION: ${{ secrets.IOS_MOBILE_PROVISION }}
BUILD_NUMBER: ${{ inputs.build_number }}
EXPORT_METHOD: ${{ inputs.ios_export_method }}
run: python3 scripts/mobile.py build ios --variant release --target aarch64 --build-number "$BUILD_NUMBER" --export-method "$EXPORT_METHOD" --output artifacts/mobile/ios
- uses: actions/upload-artifact@v4
with:
name: hireferry-ios-signed-${{ github.sha }}
path: artifacts/mobile/ios/
if-no-files-found: error
retention-days: 14
Loading
Loading