ci(mobile): iOS + Android release jobs and PR build smoke#3970
Merged
Conversation
houko
added a commit
that referenced
this pull request
Apr 28, 2026
Two must-fix issues from review of #3970: 1. iOS runner mismatch: mobile-smoke.yml is on macos-15 / Xcode 16.2 (with a comment explaining cargo-mobile2 emits objectVersion=77 pbxproj that 15.4 cannot parse), but release.yml mobile_ios was still on macos-14 / 15.4 — meaning the very first signed iOS release would fail at `tauri ios init`. Bump release.yml to match. 2. Shell injection in upload + teardown steps: `steps.art.outputs.*` and `steps.keychain.outputs.keychain` were inlined directly into shell scripts, same pattern hardened in #3938. Pass the values through env vars and quote $VAR inside the shell instead. Also add restore-keys to the per-tag rust-cache so each release does not start from a cold cache.
Deploying librefang with
|
| Latest commit: |
a3c0958
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://3017e5eb.librefang.pages.dev |
| Branch Preview URL: | https://feat-mobile-ci-builds.librefang.pages.dev |
Adds the missing mobile leg of the release pipeline: - `release.yml: mobile_android` — ubuntu-latest, JDK 17, Android SDK + NDK r27, Rust aarch64 + armv7 targets. Decodes ANDROID_KEYSTORE_B64, builds signed `.aab` + `.apk` via `cargo tauri android build`, attaches to the GitHub release. Falls back to an unsigned debug APK when signing secrets are absent so forks / first-run repos still validate the toolchain. - `release.yml: mobile_ios` — macos-14, Xcode 15.4, Rust aarch64-ios. Imports APPLE_CERT_P12 + APPLE_PROVISIONING_PROFILE_B64 into a one-shot keychain, builds signed `.ipa` via `cargo tauri ios build`, attaches to the release. Falls back to an unsigned simulator build when secrets are missing. - Both jobs run with `continue-on-error: true` — a flaky cert refresh on Apple's side or an Android signing snag must NOT block the desktop release matrix. PR-time smoke coverage: - New workflow `mobile-smoke.yml` runs unsigned debug builds on PRs that touch `crates/librefang-desktop/**`, the dashboard, `gen/**`, the workflow itself, or root Cargo files. No secrets, no release upload — just "did the toolchain still produce a binary?" - Cargo + Gradle caches are scoped per-job so cache hits get each smoke leg under the issue's 12-min target. Documentation: - `.github/SECRETS.md` is new — every workflow secret now has a named entry with format, rotation cadence, and a concrete runbook for the yearly Apple cert refresh. Mobile signing keys, desktop signing, package registries, and internal infra are all covered, with operational rules at the bottom (never echo, always wipe runner copies, forks must degrade gracefully). Closes #3345.
cargo-mobile2 (used by `cargo tauri ios init`) emits a pbxproj with
objectVersion = 77, which Xcode 15.4 on macos-14 refuses to open
("future Xcode project file format"). Bump the iOS smoke job to
macos-15 with Xcode 16.2 so the generated project can build.
Two must-fix issues from review of #3970: 1. iOS runner mismatch: mobile-smoke.yml is on macos-15 / Xcode 16.2 (with a comment explaining cargo-mobile2 emits objectVersion=77 pbxproj that 15.4 cannot parse), but release.yml mobile_ios was still on macos-14 / 15.4 — meaning the very first signed iOS release would fail at `tauri ios init`. Bump release.yml to match. 2. Shell injection in upload + teardown steps: `steps.art.outputs.*` and `steps.keychain.outputs.keychain` were inlined directly into shell scripts, same pattern hardened in #3938. Pass the values through env vars and quote $VAR inside the shell instead. Also add restore-keys to the per-tag rust-cache so each release does not start from a cold cache.
Xcode 16 on macos-15 ships only the macOS SDK by default; without an explicit `xcodebuild -downloadPlatform iOS`, xcodebuild aborts with "Found no destinations for the scheme 'librefang-desktop_iOS'". Add a download step before the rust-toolchain action so the simulator runtime is present when tauri builds the project.
Mirrors 8fb5e36 — the smoke job now does xcodebuild -downloadPlatform iOS because macos-15 ships only the macOS SDK by default. The signed release job hits the same 'Found no destinations' failure when xcodebuild can't find an iOS SDK to target, so add the same pre-download step here.
Pinning Xcode 16.2 via setup-xcode@v1 lands on an Xcode install whose iOS Simulator runtime isn't pre-staged, and `xcodebuild -downloadPlatform iOS` then fails on hosted runners with "Unable to connect to simulator" (exit 70). The macos-15 image's default Xcode (16.x) understands the objectVersion=77 pbxproj and ships with iOS Simulator runtime already populated, so just use that and add a diagnostic step that prints the chosen Xcode and available runtimes.
The default capability requested shell:default, global-shortcut:*, autostart:default and updater:default — none of which are bundled on iOS/Android, so the iOS smoke build failed with "Permission shell:default not found". Add `platforms: ["macOS", "windows", "linux"]` to default.json so it only applies on desktop, and add a mobile.json capability that grants the subset of permissions iOS and Android actually expose (core, notification, dialog).
Three iOS build errors after the capability fix:
- WebviewWindowBuilder::title() and friends are desktop-only methods
in tauri 2 (mobile builders manage the OS-owned root window
differently). Wrap the entire window-creation block in
#[cfg(desktop)] and gate the WebviewUrl/WebviewWindowBuilder
imports the same way.
- tauri::mobile_entry_point requires a 0-arg fn; `run` takes
(Option<String>, bool). Introduce a 0-arg `mobile_main` shim
behind #[cfg(mobile)] that calls run(None, false), and drop the
cfg_attr on `run` itself.
iOS/Android still get the full app — they just rely on
tauri.conf.json's window entry instead of programmatically creating
one.
…_code KEYRING_SERVICE and KEYRING_ACCOUNT are only referenced inside store/get/clear_credentials, which are themselves gated to iOS/Android. The consts were unconditional, so desktop and nix builds tripped `#[warn(dead_code)]` (which becomes -D warnings in some downstream checks).
Each smoke leg pulls a cold Android NDK or iOS SDK and a full Rust mobile rebuild — ~25 min per side on a cache miss. Running that on every desktop or dashboard PR burns a lot of CI time for breakage that gets caught on the post-merge run anyway. Switch the trigger to push:main with the same path filter, plus keep workflow_dispatch so a maintainer can validate a branch before merge when they want to. Same path scoping (desktop crate / dashboard / gen / this workflow / root Cargo files) — non-mobile changes stay free.
cargo tauri ios build expects liblibrefang_desktop.a in the target dir,
which Cargo only produces when the crate-type list includes staticlib.
Without an explicit [lib] block the default is just rlib, which is why
the iOS smoke fails post-rust-build with
Library not found at target/aarch64-apple-ios-sim/debug/liblibrefang_desktop.a
Add staticlib + cdylib for mobile, keep lib so the desktop bin in
src/main.rs still depends on it as an rlib.
- actions/setup-java was pinned to a SHA I made up (7a445ee0e10ddc8ddedd4e85cee2b5a8a8a3afba); replaced with the real v4 ref c1e323688fd81a25caa38c78aa6df2d33d3e20d9. The android_smoke + mobile_android jobs were dying at "Set up job" with "Unable to resolve action actions/setup-java@<bogus-sha>". Both workflows shared the same bogus pin, so both legs were blocked. - release.yml mobile_ios now matches the smoke job: drop setup-xcode + xcode-version pin + xcodebuild -downloadPlatform iOS, use the macos-15 default Xcode (which ships an iOS Simulator runtime pre-staged). Pinning to 16.2 lands on a build whose simulator runtime isn't pre-installed and the download itself fails on hosted runners with 'Unable to connect to simulator'.
The repo ships gen/android/{.gitkeep,README.md} so the directory exists
in fresh clones (helpful for IDEs / docs). tauri-cli reads this as
"already populated" and refuses to scaffold the missing
gen/android/app/src/main/java/<bundle-path> layout — init fails with
Error Project directory ... gen/android/app/src/main/java/ai/librefang/app
does not exist. Did you update the package name ... ?
It even tells you to "delete the gen/android folder and run tauri android
init to recreate". Both mobile workflows now do exactly that, so init
starts from clean every run.
iOS does not show the same symptom (cargo-mobile2 happily overwrites the
README placeholder under gen/apple/), so leave that path alone.
The dev machine runs Xcode 26.4.1 / iOS 26.4 SDK. macos-15 + Xcode 16 shipped binaries against iOS 18.x — different SDK, different ABI guarantees, different simulator runtime. Worse, an iOS 26-built local artifact diverging from a CI-built iOS 18 artifact would hide regressions. macos-26 hosted runners are now available and ship Xcode 26.4.1 with iOS 26.4 Simulator runtime pre-staged — no on-demand downloadPlatform dance needed. Pin both ios_smoke and mobile_ios to that runner + Xcode version so CI matches local. Also confirms a real setup-xcode@v1 SHA (the previous reference here was left over from a different action and only worked because the step that needed it was still cleaned up by the user mid-iteration).
Pinning to 26.4.1 forced setup-xcode to swap toolchains on every run, ~1–2 min overhead, for a Tauri smoke / release build that only needs *some* iOS 26 SDK. The SDK delta inside the 26.x line is not material. Default Xcode on macos-26 is 26.2 with iOS 26.2 Simulator pre-staged — plenty for verifying the toolchain still produces a binary, and the release build still produces a 26.x ipa.
Xcode 26 (the toolchain on macos-26 hosted runners) no longer ships the
swiftCompatibility56 or swiftCompatibilityConcurrency back-deploy static
libs — those existed to back-port Swift 5.6 features to iOS 13–14.x and
are unnecessary on iOS 15+. Targeting "14.0" with Xcode 26 makes the
linker emit references to those symbols and then fail with
Undefined symbols for architecture arm64:
"__swift_FORCE_LOAD_$_swiftCompatibility56"
"__swift_FORCE_LOAD_$_swiftCompatibilityConcurrency"
ld: symbol(s) not found for architecture arm64
iOS 16 was released in September 2022, has ~95%+ adoption in 2026, and
keeps full Tauri 2 mobile feature compatibility. The bump is invisible
to anyone running iOS 16+; iOS 14/15 users were already a rounding
error and would have hit ABI issues with newer Tauri features anyway.
`cargo tauri ios init` regenerates the Xcode project from this config,
so CI picks up the change without further edits. Locally, run
rm -rf gen/apple && cargo tauri ios init
to refresh an existing checkout.
…raise iOS min in changelog
Address review feedback on this PR.
- **Cache `tauri-cli` binary across runs.** All four mobile jobs
(release.yml × 2, mobile-smoke.yml × 2) wrap the `cargo install
tauri-cli@^2 --locked` step in an `actions/cache` lookup keyed on
`tauri-cli-${runner.os}-v2`. Swatinem/rust-cache caches `target/`
+ cargo registries but not `~/.cargo/bin/`, so without this the
1–2 min recompile happened cold every job. Cache hit = `command -v`
short-circuit and skip the install entirely.
- **Prepend (not replace) the macOS keychain search list** during
iOS code-signing. The original `security list-keychain -d user -s
"$KEYCHAIN_PATH"` form *replaces* the list — fine on a clean
GitHub-hosted runner but would hide existing entries on a future
self-hosted macOS runner for the duration of the build. Now reads
the prior list, then re-sets it with the new one prepended.
- **Document the `crate-type = ["staticlib", "cdylib", "lib"]`
overhead on desktop.** The original comment explained why mobile
needs the extra crate-types but didn't mention the side effect:
desktop builds also produce the `.a` and `.so/.dylib` outputs,
adding ~10–20% link time on a clean target/. Worth flagging so a
future reader investigating slow desktop builds sees this as a
known cost rather than guessing.
- **CHANGELOG entry for iOS 14 → 16 bump.** Tauri 2 mobile requires
iOS 16+, which silently dropped support for iPhone 6s, original
iPhone SE, iPad Air 2 and similar 14/15-only devices. Added under
Unreleased / Changed with the affected device classes spelled out
so it's findable when a user reports "the new mobile app won't
install on my iPad".
- **Update PR description and test plan** to reflect that
`mobile-smoke.yml` is post-merge (push to main + workflow_dispatch),
not PR-time. The yaml comment was always correct; the PR body and
test plan disagreed with the implementation.
houko
force-pushed
the
feat/mobile-ci-builds
branch
from
April 28, 2026 15:13
aa49520 to
e1fbc40
Compare
Same bug as the mobile_ios block: `security list-keychain -d user -s $KEYCHAIN_PATH` replaces the entire user search list rather than prepending the signing keychain to it. Harmless on a clean GitHub-hosted runner, but on a future self-hosted macOS runner it would hide the login keychain (and anything else) for the duration of the build. The mobile_ios block was fixed in the previous commit; the desktop signing block had the same issue from when it was originally written and remained unfixed because of an over-cautious 'this is pre-existing, leave it' read on my part. Same fix applies.
This was referenced Apr 28, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #3345 — the CI side of the mobile epic. Desktop matrix already produces all six native bundles; this fills in the missing iOS + Android leg.
release.ymlmobile_androidonubuntu-latest: JDK 17, Android SDK + NDK r27, Rustaarch64-linux-android+armv7-linux-androideabi. DecodesANDROID_KEYSTORE_B64, builds signed.aab+.apkviacargo tauri android build --apk --aab, attaches to the GitHub release.mobile_iosonmacos-26: default Xcode 26, Rustaarch64-apple-ios. ImportsAPPLE_CERT_P12+APPLE_PROVISIONING_PROFILE_B64into a one-shot keychain (prepended to the user search list, not replacing it), builds signed.ipaviacargo tauri ios build, attaches to the release.continue-on-error: true— a flaky Apple cert refresh or Android signing snag must NOT block the desktop matrix above.cargo install tauri-cliis now wrapped in anactions/cachelookup keyed ontauri-cli-${runner.os}-v2, so the 1–2 min recompile happens once per cache lifetime instead of every job.mobile-smoke.yml(new)Post-merge smoke test (push to
mainonly — not per-PR), no secrets touched. Triggers on pushes tomainthat changecrates/librefang-desktop/**, the dashboard,gen/**, the workflow itself, or rootCargo.toml/Cargo.lock. Cargo + Gradle caches are keyed per-job so a warm rerun stays under the 12-min target from the issue.The "post-merge instead of per-PR" choice is deliberate: each leg pulls a cold Android NDK or Xcode iOS SDK and a full Rust mobile rebuild — ~25 min each on a cache miss, which is too heavy to pay for every desktop / dashboard PR. Maintainers can still trigger it on a branch via
workflow_dispatchfrom the Actions tab when they want pre-merge validation..github/SECRETS.md(new)Every Actions secret the repo currently uses is now documented with format, rotation cadence, and a concrete runbook for the yearly Apple cert refresh. Mobile signing, desktop signing, package registries, and internal infra (Homebrew tap PAT, deploy previews) are all covered. Operational rules at the bottom: never echo, always wipe runner copies, forks must degrade gracefully.
Desktop crate changes
[lib]now declarescrate-type = ["staticlib", "cdylib", "lib"]socargo tauri ios buildcan find the static library. Comment inCargo.tomlnotes the desktop side now also produces those outputs (~10–20% extra link time on a clean target/).run()no longer carries#[tauri::mobile_entry_point]; a separate 0-argmobile_main()wraps it. Window construction is gated#[cfg(desktop)]becauseWebviewWindowBuilder::title/inner_size/centeraren't available on mobile.default.json(platforms: ["macOS", "windows", "linux"]) and a newmobile.json(platforms: ["iOS", "android"]) with onlycore / notification / dialog— desktop-only plugins (shell, global-shortcut, autostart, updater) are not bundled on mobile.CHANGELOG.mdunder Unreleased / Changed.Notes for maintainers
The first signed mobile release will fail until the eight new Android/Apple secrets are populated — see
.github/SECRETS.mdfor the exact set and thekeytool/ Apple Developer portal recipes. Until then, both jobs land their unsigned fallbacks, so the rest of the release pipeline keeps working.Test plan
release.ymlviaworkflow_dispatchagainst a tag with no mobile secrets set — confirm desktop bundles still upload, mobile jobs land unsigned debug artifacts, neither fails the workflow.workflow_dispatch— confirm signed.aab/.apk/.ipaattach to the release.crates/librefang-desktop/Cargo.tomltomain— confirmmobile-smoke.ymlruns and finishes within ~12 min on a warm cache.crates/librefang-api/src/lib.rstomain— confirmmobile-smoke.ymldoes not run (path filter exclusion).crates/librefang-desktop/**changes, triggermobile-smoke.ymlviaworkflow_dispatch— confirm it can be invoked manually for pre-merge validation.