From 17500e0567fb51068d9e08671f4515ca2db44cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 29 Jun 2026 13:02:16 +0200 Subject: [PATCH] iOS Build using GH Actions --- .github/workflows/ios-build.yml | 166 ++++++++++++++++++++++++++++++++ NATIVE.md | 99 ++++++++++++++++++- scripts/build-ios.sh | 56 +++++++++++ scripts/export-options.plist | 21 ++++ 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ios-build.yml create mode 100755 scripts/build-ios.sh create mode 100644 scripts/export-options.plist diff --git a/.github/workflows/ios-build.yml b/.github/workflows/ios-build.yml new file mode 100644 index 0000000..945e29a --- /dev/null +++ b/.github/workflows/ios-build.yml @@ -0,0 +1,166 @@ +name: iOS Build & Deploy + +on: + push: + tags: + - "ios-v*" + workflow_dispatch: + inputs: + upload_to_testflight: + description: "Upload to TestFlight after build" + required: false + default: "true" + type: choice + options: + - "true" + - "false" + +concurrency: + group: ios-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install backend dependencies & test + run: cd backend && npm ci && npm test -- --ci + + - name: Install frontend dependencies & test + run: cd frontend && npm ci && npm test -- --run --reporter=basic + + build-ios: + name: Build iOS App + needs: test + runs-on: macos-14 + env: + SCHEME: App + PROJECT: ios/App/App.xcodeproj + WORKSPACE: ios/App/App.xcworkspace + ARCHIVE_PATH: build/App.xcarchive + IPA_PATH: build/App.ipa + EXPORT_OPTIONS: scripts/export-options.plist + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install root dependencies + run: npm ci + + - name: Install frontend dependencies + run: cd frontend && npm ci + + - name: Build web bundle + run: npm run build:web + + - name: Ensure iOS platform & sync Capacitor + run: | + if [ ! -d "ios/App/App.xcodeproj" ]; then + echo "Adding iOS platform..." + npx cap add ios + fi + npx cap sync ios + + - name: Install CocoaPods + run: cd ios/App && pod install --repo-update + + - name: Install Apple certificates & provisioning profile + uses: apple-actions/import-codesign-certs@v2 + with: + p12-file-base64: ${{ secrets.IOS_P12_BASE64 }} + p12-password: ${{ secrets.IOS_P12_PASSWORD }} + + - name: Install provisioning profile + run: | + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + echo "${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/${{ secrets.IOS_PROVISION_PROFILE_UUID }}.mobileprovision + + - name: Set version from tag + if: startsWith(github.ref, 'refs/tags/ios-v') + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/ios-v}" + MAJOR=$(echo "$TAG_VERSION" | cut -d. -f1) + MINOR=$(echo "$TAG_VERSION" | cut -d. -f2) + PATCH=$(echo "$TAG_VERSION" | cut -d. -f3) + BUILD=${{ github.run_number }} + + echo "Setting MARKETING_VERSION=$MAJOR.$MINOR.$PATCH" + echo "Setting CURRENT_PROJECT_VERSION=$BUILD" + + sed -i '' "s/MARKETING_VERSION = .*;/MARKETING_VERSION = $MAJOR.$MINOR.$PATCH;/" ios/App/App.xcodeproj/project.pbxproj + sed -i '' "s/CURRENT_PROJECT_VERSION = .*;/CURRENT_PROJECT_VERSION = $BUILD;/" ios/App/App.xcodeproj/project.pbxproj + + - name: Build Xcode archive + run: | + xcodebuild archive \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration Release \ + -destination "generic/platform=iOS" \ + -archivePath "$ARCHIVE_PATH" \ + -allowProvisioningUpdates \ + CODE_SIGN_STYLE=Manual \ + DEVELOPMENT_TEAM="${{ secrets.IOS_TEAM_ID }}" \ + PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISION_PROFILE_NAME }}" \ + | xcpretty --color || true + + - name: Export IPA + run: | + plutil -replace teamID -string "${{ secrets.IOS_TEAM_ID }}" "$EXPORT_OPTIONS" 2>/dev/null || true + plutil -replace provisioningProfiles.net.hacecalor.farmafinder -string "${{ secrets.IOS_PROVISION_PROFILE_NAME }}" "$EXPORT_OPTIONS" 2>/dev/null || true + + xcodebuild -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportOptionsPlist "$EXPORT_OPTIONS" \ + -exportPath build \ + -allowProvisioningUpdates \ + | xcpretty --color || true + + - name: Upload artifact (IPA) + uses: actions/upload-artifact@v4 + with: + name: FarmaFinder-iOS + path: build/*.ipa + retention-days: 30 + + - name: Upload to TestFlight + if: > + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ios-v')) || + (github.event_name == 'workflow_dispatch' && inputs.upload_to_testflight == 'true') + run: | + xcrun altool \ + --upload-app \ + --type ios \ + --file "$IPA_PATH" \ + --apiKey "${{ secrets.APP_STORE_API_KEY_ID }}" \ + --apiIssuer "${{ secrets.APP_STORE_API_ISSUER_ID }}" + env: + API_PRIVATE_KEY: ${{ secrets.APP_STORE_API_PRIVATE_KEY }} + + - name: Summary + run: | + if [ -f "$IPA_PATH" ]; then + IPA_SIZE=$(du -h "$IPA_PATH" | cut -f1) + echo "### iOS Build Summary" >> $GITHUB_STEP_SUMMARY + echo "| Item | Value |" >> $GITHUB_STEP_SUMMARY + echo "|------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| IPA Size | $IPA_SIZE |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | ${GITHUB_REF#refs/tags/} |" >> $GITHUB_STEP_SUMMARY + echo "| Build # | ${{ github.run_number }} |" >> $GITHUB_STEP_SUMMARY + else + echo "::error::IPA not found at $IPA_PATH" + exit 1 + fi diff --git a/NATIVE.md b/NATIVE.md index 09cfc90..b129d68 100644 --- a/NATIVE.md +++ b/NATIVE.md @@ -1,6 +1,6 @@ # Native Mobile App Build Guide -FarmaFinder ships as a PWA and is wrapped for the App Store / Play Store via [Capacitor 6](https://capacitorjs.com). The web frontend in `frontend/` is bundled into each native shell — there's no separate codebase for Android or iOS. +FarmaFinder ships as a PWA and is wrapped for the App Store / Play Store via [Capacitor 8](https://capacitorjs.com). The web frontend in `frontend/` is bundled into each native shell — there's no separate codebase for Android or iOS. ``` FarmaFinder/ @@ -176,6 +176,103 @@ Both stores require: privacy policy URL, screenshots (multiple device sizes), an --- +## CI/CD: GitHub Actions (iOS) + +The project includes a GitHub Actions workflow for automated iOS builds, replacing the previous Bitrise pipeline. + +### Workflow file + +`.github/workflows/ios-build.yml` — builds the iOS app on every `ios-v*` tag push, or manually via `workflow_dispatch`. + +### Trigger types + +| Trigger | Behaviour | +|---------|-----------| +| `git push ios-v*` tag | Builds, archives, exports IPA, uploads to TestFlight | +| `workflow_dispatch` (manual) | Builds and optionally uploads to TestFlight (configurable) | + +### Required GitHub Secrets + +Set these in **Settings → Secrets and variables → Actions**: + +| Secret | Description | +|--------|-------------| +| `IOS_P12_BASE64` | Base64-encoded `.p12` distribution certificate | +| `IOS_P12_PASSWORD` | Password for the `.p12` file | +| `IOS_PROVISION_PROFILE_BASE64` | Base64-encoded `.mobileprovision` file | +| `IOS_PROVISION_PROFILE_UUID` | UUID of the provisioning profile (filename in `~/Library/MobileDevice/Provisioning Profiles/`) | +| `IOS_PROVISION_PROFILE_NAME` | Name of the provisioning profile (used in Xcode export) | +| `IOS_TEAM_ID` | Apple Developer Team ID (10-char alphanumeric) | +| `APP_STORE_API_KEY_ID` | App Store Connect API key ID | +| `APP_STORE_API_ISSUER_ID` | App Store Connect API issuer ID | +| `APP_STORE_API_PRIVATE_KEY` | App Store Connect API private key (`.p8` content) | + +### Pipeline steps + +1. **Test** (ubuntu-latest) — runs backend + frontend test suites +2. **Build iOS** (macos-14 — Apple Silicon runner): + - Installs Node 20, builds web bundle, syncs Capacitor + - Installs CocoaPods dependencies + - Imports signing certificate + provisioning profile from secrets + - Sets `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` from tag + - Builds `.xcarchive`, exports `.ipa` + - Uploads IPA as GitHub artifact (30-day retention) + - Uploads to TestFlight via `xcrun altool` + +### Local iOS build script + +For local builds on macOS, use the provided script: + +```bash +./scripts/build-ios.sh +``` + +This performs the same steps (build web, sync, pod install, archive, export IPA) without the CI signing/TestFlight steps. Override the export-options path with: + +```bash +EXPORT_OPTIONS=path/to/export-options.plist ./scripts/build-ios.sh +``` + +### Export Options + +`scripts/export-options.plist` configures the IPA export (App Store method, manual signing, team ID, provisioning profile). The workflow replaces placeholder values from secrets at build time. + +### Release flow + +```bash +# 1. Tag a release +git tag ios-v1.2.3 +git push origin ios-v1.2.3 + +# 2. GitHub Actions runs automatically: +# test → build → archive → IPA artifact → TestFlight upload + +# 3. Download the IPA from the Actions Artifacts section if needed + +# 4. Submit via App Store Connect after TestFlight review +``` + +### Migrating from Bitrise + +The `bitrise.yml` is still present but no longer needed for iOS. To remove it: + +```bash +rm bitrise.yml +``` + +Key differences from the Bitrise pipeline: + +| Aspect | Bitrise | GitHub Actions | +|--------|---------|----------------| +| Runner | Bitrise macOS stack | `macos-14` (M1 Apple Silicon) | +| Signing | Bitrise certificate step | `apple-actions/import-codesign-certs` | +| TestFlight upload | `deploy-to-bitrise-io` | `xcrun altool` with App Store Connect API | +| Trigger | Tag `ios-v*` | Tag `ios-v*` or manual dispatch | +| Artifact | Bitrise dashboard | GitHub Actions Artifacts (30-day retention) | +| Cache | Bitrise stack caching | `actions/setup-node` cache | + +--- + ## Updating Capacitor / plugins ```bash diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh new file mode 100755 index 0000000..651125e --- /dev/null +++ b/scripts/build-ios.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCHEME="App" +PROJECT="ios/App/App.xcodeproj" +WORKSPACE="ios/App/App.xcworkspace" +ARCHIVE_PATH="build/App.xcarchive" +IPA_DIR="build" +EXPORT_OPTIONS="${EXPORT_OPTIONS:-scripts/export-options.plist}" + +echo "=== FarmaFinder iOS Build Script ===" + +echo "[1/6] Installing frontend dependencies..." +(cd frontend && npm ci) + +echo "[2/6] Building web bundle..." +npm run build:web + +echo "[3/6] Syncing Capacitor..." +if [ ! -d "ios/App/App.xcodeproj" ]; then + echo " Adding iOS platform..." + npx cap add ios +fi +npx cap sync ios + +echo "[4/6] Installing CocoaPods..." +(cd ios/App && pod install --repo-update) + +echo "[5/6] Archiving..." +xcodebuild archive \ + -workspace "$WORKSPACE" \ + -scheme "$SCHEME" \ + -configuration Release \ + -destination "generic/platform=iOS" \ + -archivePath "$ARCHIVE_PATH" \ + -allowProvisioningUpdates \ + | xcpretty --color || true + +echo "[6/6] Exporting IPA..." +xcodebuild -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportOptionsPlist "$EXPORT_OPTIONS" \ + -exportPath "$IPA_DIR" \ + -allowProvisioningUpdates \ + | xcpretty --color || true + +IPA_FILE=$(find "$IPA_DIR" -name "*.ipa" -maxdepth 1 | head -1) +if [ -n "$IPA_FILE" ]; then + echo "" + echo "=== Build complete ===" + echo "IPA: $IPA_FILE" + echo "Size: $(du -h "$IPA_FILE" | cut -f1)" +else + echo "ERROR: IPA not found in $IPA_DIR/" + exit 1 +fi diff --git a/scripts/export-options.plist b/scripts/export-options.plist new file mode 100644 index 0000000..fe6f841 --- /dev/null +++ b/scripts/export-options.plist @@ -0,0 +1,21 @@ + + + + + method + app-store + teamID + YOUR_TEAM_ID + uploadSymbols + + uploadBitcode + + signingStyle + manual + provisioningProfiles + + net.hacecalor.farmafinder + YOUR_PROVISIONING_PROFILE_NAME + + +