Skip to content
Closed
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
99 changes: 81 additions & 18 deletions .github/workflows/native-build-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@ on:
- src/styles/mobile.css
- src/styles/mobile-ios.css

# Cancel an in-flight run when a newer commit lands on the same ref, so we don't
# burn two parallel macOS iOS builds while iterating.
concurrency:
group: native-build-smoke-${{ github.ref }}
cancel-in-progress: true

# iOS and Android run as independent jobs so a failure in one never masks the
# other's evidence (ERY-108). GitHub schedules them in parallel; neither
# `needs` the other, so an iOS pod-resolution failure no longer skips the
# Android assemble steps.
jobs:
native-build-smoke:
name: iOS bootstrap + Android smoke
ios-smoke:
name: iOS bootstrap + simulator build
runs-on: macos-15
timeout-minutes: 60
timeout-minutes: 45
permissions:
contents: read

Expand All @@ -33,13 +43,7 @@ jobs:
node-version: "20"
cache: npm

- name: Setup Java 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"

- name: Capture native toolchain versions
- name: Capture iOS toolchain versions
shell: bash
run: |
set -euo pipefail
Expand All @@ -49,11 +53,7 @@ jobs:
echo "npm $(npm -v)"
xcodebuild -version
echo "CocoaPods $(pod --version)"
java -version
echo "ANDROID_HOME=${ANDROID_HOME:-unset}"
ls "$ANDROID_HOME/platforms" | grep "android-35"
ls "$ANDROID_HOME/build-tools" | grep "35.0"
} 2>&1 | tee artifacts/toolchain.log
} 2>&1 | tee artifacts/ios-toolchain.log

- name: Bootstrap iOS workspace
shell: bash
Expand All @@ -77,27 +77,90 @@ jobs:
CODE_SIGNING_ALLOWED=NO \
build 2>&1 | tee artifacts/ios-build.log | xcbeautify

- name: Upload iOS smoke artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: native-smoke-ios-${{ github.run_id }}
if-no-files-found: warn
path: |
artifacts/*.log
ios/App/build/**

android-smoke:
name: Android debug + release assemble
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "20"
cache: npm

# Capacitor 7's android module compiles with source/target 21, so the
# Gradle build fails under JDK 17 with "invalid source release: 21"
# (ERY-108). JDK 21 is the supported toolchain for Capacitor 7 Android.
- name: Setup Java 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"

- name: Capture Android toolchain versions
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
{
echo "Node $(node -v)"
echo "npm $(npm -v)"
java -version
echo "ANDROID_HOME=${ANDROID_HOME:-unset}"
ls "$ANDROID_HOME/platforms" | grep "android-35" || echo "android-35 platform missing"
ls "$ANDROID_HOME/build-tools" | grep "35" || echo "build-tools 35 missing"
} 2>&1 | tee artifacts/android-toolchain.log

# Independent job: install JS deps here. In the old single-job layout the
# Android steps inherited node_modules from `npm run ios:init`; the split
# job has no such predecessor, so `vite` (and the rest of the toolchain)
# must be installed before `android:assemble:*` can run `npm run build`.
- name: Install JS dependencies
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
npm ci 2>&1 | tee artifacts/android-npm-ci.log

# `if: always()` on the release step guarantees the release assemble runs
# even if the debug assemble fails, so we always capture pass/fail for both.
- name: Assemble Android debug APK
id: android_debug
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
npm run android:assemble:debug 2>&1 | tee artifacts/android-debug.log
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install Node deps before Android assemble commands

After splitting into android-smoke, this job no longer runs npm ci/npm install, but it still invokes npm run android:assemble:* (which calls npm run build and needs local devDependencies like vite). On a fresh GitHub runner, these steps fail before any APK assemble, so the Android smoke lane is effectively broken and won’t provide the intended debug/release evidence.

Useful? React with 👍 / 👎.


- name: Assemble Android release APK
if: always()
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
npm run android:assemble:release 2>&1 | tee artifacts/android-release.log

- name: Upload native smoke artifacts
- name: Upload Android smoke artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: native-build-smoke-${{ github.run_id }}
name: native-smoke-android-${{ github.run_id }}
if-no-files-found: warn
path: |
artifacts/*.log
android/app/build/outputs/**
ios/App/build/**
49 changes: 49 additions & 0 deletions docs/2026-05-24-ery-71-native-packaging-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,55 @@ First macOS lane run: GitHub Actions run [`26370608416`](https://github.com/Shee

- The iOS failure masked Android validation (steps skipped). Split iOS and Android into independent jobs (or `if: always()` gating) so a failure in one still produces evidence for the other on the same run.

## ERY-108 fix (2026-05-24)

Both blockers from run `26370608416` are addressed on branch `engineer/ery-108-ios-pod-fix`:

1. **iOS deployment target pinned to 15.5.** `scripts/ios-init.sh` now adds the iOS platform _before_ building the web bundle. Capacitor's `cap add ios` only runs `pod install` (via `cap sync`) when `dist/` already exists, so adding the platform first skips the premature pod install that resolved against the default `platform :ios, '14.0'`. A new `patch_ios_deployment_target` helper then pins the target in three places — the Podfile `platform :ios` line, a `post_install` floor that raises every transitive Pods target, and the `App.xcodeproj` `IPHONEOS_DEPLOYMENT_TARGET` — before `cap sync` runs pod install against the corrected Podfile. The helper is idempotent and re-asserts the floor on every sync. Patch logic verified locally against the extracted Capacitor 7 `ios-pods-template` (Podfile → `15.5`, all four pbxproj configs → `15.5`, `ruby -c` clean); the native toolchain itself is unavailable on the engineer host, so pod resolution + simulator build are proven only in CI.
2. **iOS and Android split into independent jobs.** `.github/workflows/native-build-smoke.yml` now has separate `ios-smoke` (macos-15) and `android-smoke` (ubuntu-latest) jobs with no `needs` edge, so an iOS failure no longer skips Android. The Android release assemble carries `if: always()` so it runs even when the debug assemble fails — both APK results are always captured. Each job uploads its own artifact bundle.

### Re-run evidence

**Split-lane run 1** — [26370807000](https://github.com/SheetMetalConnect/eryxon-flow/actions/runs/26370807000) on PR [#600](https://github.com/SheetMetalConnect/eryxon-flow/pull/600), `engineer/ery-108-ios-pod-fix`, 2026-05-24.

| Check | Status | Evidence |
| --- | --- | --- |
| iOS `pod install` / `npm run ios:init` (add ios + pin 15.5 + sync) | **PASS** | iOS job step "Bootstrap iOS workspace" succeeded — the 15.5 pin resolves `GoogleMLKit/BarcodeScanning 7.0.0`. This clears the original ERY-108 blocker. |
| iOS simulator build of `ios/App/App.xcworkspace` | NOT CAPTURED | xcodebuild step was still running; run superseded by the npm-ci fix below (concurrency cancel). Re-proven in run 2. |
| Job split (Android no longer masked by iOS) | **PASS** | Android job ran to completion and produced its own logs while iOS was still building — confirms the masking fix. |
| `npm run android:assemble:debug` | FAIL → fixed | `sh: 1: vite: not found`. Root cause: the independent Android job had no `npm ci` step (the old single job inherited `node_modules` from `ios:init`). Fixed by adding an "Install JS dependencies" step. |
| `npm run android:assemble:release` | FAIL (same cause) | Ran anyway via `if: always()` — confirms the gating works; same `vite: not found` root cause. |

Note: run 1's iOS job actually completed fully green before the concurrency cancel — `Build iOS simulator target` succeeded, so the iOS acceptance criterion (pod install + simulator build) is met as of run 1.

**Split-lane run 2** — [26370892728](https://github.com/SheetMetalConnect/eryxon-flow/actions/runs/26370892728), commit `4967746`, 2026-05-24.

| Check | Status | Evidence |
| --- | --- | --- |
| Android `npm ci` (split-job dependency install) | **PASS** | "Install JS dependencies" step succeeded — clears the `vite: not found` error from run 1. |
| `npm run android:assemble:debug` | FAIL → fixed | Gradle now compiles all Capacitor plugins, then fails at `:capacitor-android:compileDebugJavaWithJavac`: `error: invalid source release: 21`. Capacitor 7's android module targets Java 21; the job pinned JDK 17. Fixed by bumping the Android job to `java-version: "21"`. |
| `npm run android:assemble:release` | FAIL (same cause) | Ran via `if: always()`; same JDK-21 root cause. |

**Split-lane run 3** — [26370991082](https://github.com/SheetMetalConnect/eryxon-flow/actions/runs/26370991082), commit `031ef00`, 2026-05-24. **All jobs green.**

| Check | Status | Evidence |
| --- | --- | --- |
| iOS `pod install` / `npm run ios:init` | **PASS** | `Updating iOS native dependencies with pod install in 20.19s` — GoogleMLKit 7.0.0 resolves under the 15.5 pin. |
| iOS simulator build of `ios/App/App.xcworkspace` | **PASS** | `xcodebuild … build` → `** BUILD SUCCEEDED **`. |
| `npm run android:assemble:debug` | **PASS** | `BUILD SUCCESSFUL in 2m 23s` → `app/build/outputs/apk/debug/app-debug.apk`. |
| `npm run android:assemble:release` | **PASS** | `BUILD SUCCESSFUL in 2m 6s` → `app/build/outputs/apk/release/app-release-unsigned.apk`. |

## Acceptance — met (ERY-108)

- ✅ iOS `pod install` resolves and the simulator build of `ios/App/App.xcworkspace` succeeds.
- ✅ Android debug + release APK smokes run independently of iOS and both produce APKs (`if: always()` + separate jobs).
- ✅ Evidence recorded above. Both native lanes pass green on run [26370991082](https://github.com/SheetMetalConnect/eryxon-flow/actions/runs/26370991082) — ERY-81 native iOS + Android smoke unblocked.

### Fix summary (PR #600)

- `scripts/ios-init.sh`: add iOS platform before the web build (skips premature pod install) + idempotent `patch_ios_deployment_target` pinning iOS 15.5 in the Podfile, post_install floor, and `App.xcodeproj`.
- `.github/workflows/native-build-smoke.yml`: independent `ios-smoke` (macos-15) + `android-smoke` (ubuntu-latest, JDK 21) jobs; Android job runs `npm ci`; release assemble gated `if: always()`; concurrency group cancels superseded runs.

## Remaining Apple-specific gaps

## Remaining Apple-specific gaps
Expand Down
48 changes: 45 additions & 3 deletions scripts/ios-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ ensure_native_build_bindings() {
fi
}

# @capacitor-mlkit/barcode-scanning@7.5.0 pulls in GoogleMLKit/BarcodeScanning
# (= 7.0.0), which declares a minimum deployment target of iOS 15.5. The
# Capacitor 7 template ships a Podfile pinned to `platform :ios, '14.0'`, so a
# stock `cap add ios` fails CocoaPods resolution before any build runs
# (ERY-108). Pin the whole project to 15.5 so pod install resolves.
IOS_DEPLOYMENT_TARGET="15.5"

# Raise the iOS deployment target in the generated (uncommitted) iOS project so
# CocoaPods can resolve GoogleMLKit 7.0.0. Idempotent: safe to re-run on every
# init/sync. We touch three places:
# - Podfile `platform :ios` line (controls the App pod target floor)
# - Podfile post_install (raises every transitive Pods target)
# - App.xcodeproj deployment target (so the app target links the 15.5 pods)
# `cap sync` only rewrites the capacitor_pods block + require_relative line, so
# these edits survive the sync that runs pod install.
patch_ios_deployment_target() {
local podfile="ios/App/Podfile"
local pbxproj="ios/App/App.xcodeproj/project.pbxproj"

if [[ -f "$podfile" ]]; then
echo "▶ Pinning iOS deployment target to ${IOS_DEPLOYMENT_TARGET} (GoogleMLKit 7.0.0 needs >= 15.5)..."
sed -i '' -E "s/^platform :ios, '[0-9.]+'/platform :ios, '${IOS_DEPLOYMENT_TARGET}'/" "$podfile"
if ! grep -q "ERYXON_DEPLOYMENT_FLOOR" "$podfile"; then
perl -0pi -e "s/(post_install do \|installer\|\n)/\$1 # ERYXON_DEPLOYMENT_FLOOR: GoogleMLKit\/BarcodeScanning 7.0.0 requires iOS 15.5+\n installer.pods_project.targets.each do |t|\n t.build_configurations.each do |c|\n if c.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < ${IOS_DEPLOYMENT_TARGET}\n c.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${IOS_DEPLOYMENT_TARGET}'\n end\n end\n end\n/" "$podfile"
fi
fi

if [[ -f "$pbxproj" ]]; then
sed -i '' -E "s/IPHONEOS_DEPLOYMENT_TARGET = [0-9.]+;/IPHONEOS_DEPLOYMENT_TARGET = ${IOS_DEPLOYMENT_TARGET};/g" "$pbxproj"
fi
}

if [[ "${OSTYPE:-}" != darwin* ]]; then
echo "⚠ Capacitor's iOS toolchain only runs on macOS — this script will" >&2
echo " configure the project but you must finish 'cap add ios' on a Mac." >&2
Expand All @@ -54,15 +86,25 @@ else
fi
ensure_native_build_bindings

echo "▶ Building production web bundle..."
npm run build

if [[ ! -d ios ]]; then
echo "▶ Generating native iOS project (npx cap add ios)..."
# `cap add ios` only runs `cap sync` (and therefore `pod install`) when the
# web bundle (dist/) already exists. We deliberately add the platform BEFORE
# building so that first pod install is skipped — it would otherwise resolve
# against the default 'platform :ios, 14.0' Podfile and fail on GoogleMLKit
# 7.0.0. Removing any stale dist/ keeps this deterministic on dev machines.
rm -rf dist
npx cap add ios
patch_ios_deployment_target
fi

echo "▶ Building production web bundle..."
npm run build

echo "▶ Syncing web bundle and native plugins into ios/..."
# Re-assert the deployment floor before sync in case the iOS project predates
# this pin, then sync (which runs pod install against the corrected Podfile).
patch_ios_deployment_target
npx cap sync ios

# Ensure the generated Info.plist carries the strings ML Kit / Biometric
Expand Down
Loading