You are reading the docs for v0.0.1-beta-2. View the latest docs.
Skip to content

Mobile releases with fastlane ​

This is opt-in. kinhin's runners, configuration and commands work exactly as before unless you run kinhin mobile init in a project. Running it only adds files to that project: fastlane lanes, a Gemfile, and a CI workflow. kinhin's own config is not read or changed by it.

Scaffold fastlane into an iOS, Android, Flutter, React Native, Expo, Capacitor, Kotlin Multiplatform or .NET MAUI project, and run store uploads from a kinhin runner. kinhin mobile init writes the lanes, a pinned Gemfile and a manual release workflow. Nothing is uploaded until you trigger the workflow.

← Back to toolchains

Quick start ​

Install kinhin, then run this from anywhere, with the project folder as the argument. The tutorial walks through a first build end to end, the credentials page covers each secret, and store screenshots covers the screenshots lane:

bash
kinhin mobile init /path/to/your/app

It detects the framework, then writes only the files that are missing:

ProjectDetected byWrites
Flutterpubspec.yamlGemfile, ios/fastlane/, android/fastlane/, .github/workflows/mobile-release.yml
React Native (bare)"react-native" in package.jsonsame as Flutter
Capacitor / Ionic"@capacitor/core" in package.jsonsame as Flutter. Each platform's CI job runs npm run build and npx cap sync.
Kotlin MultiplatformiosApp/, Gradle settings, and the multiplatform plugin in a build fileiosApp/fastlane/, androidApp/fastlane/, workflow. The Xcode build phase builds the shared framework.
.NET MAUIa .csproj with <UseMaui>true</UseMaui> (root or one folder down)ios/fastlane/, android/fastlane/, workflow. dotnet publish builds and signs the artifacts.
Expo"expo" in package.jsoneas.json only. Expo uses EAS by default (see Expo).
Native iOSios/ or *.xcodeproj / *.xcworkspaceGemfile, fastlane/ (or ios/fastlane/), workflow
Native Androidandroid/ or settings.gradle(.kts)Gemfile, fastlane/ (or android/fastlane/), workflow

Flags:

  • --framework ios|android|flutter|react-native|expo|capacitor|kmp|maui|native overrides detection.
  • --ci github|gitea|forgejo|gitlab|bitbucket|azure|buildkite picks the CI system the release pipeline is written for. By default it is detected: a .buildkite/ folder or Azure Pipelines file, then the host of the origin remote, then the repository's other CI files, else GitHub. See Other CI systems.
  • --force overwrites files that already exist.
  • --dry-run prints what would be written.

Then, in the project:

bash
bundle install
# Run from the folder that holds fastlane/: ios/ or android/ for Flutter and React Native,
# the project root for a native project with its project file at the root.
cd ios && bundle exec fastlane lanes
cd ../android && bundle exec fastlane lanes

Lanes ​

PlatformLaneWhat it does
iOSbetamatch (readonly) → build_app (App Store export; IPA_PATH or dotnet publish for MAUI replaces it) → upload_to_testflight
iOSreleaseupload_to_app_store with metadata and screenshots skipped; not submitted for review
iOSscreenshotssnapshot (UI tests on a simulator) → optional frameit (FRAME_SCREENSHOTS=1) → optional upload (UPLOAD_SCREENSHOTS=1). See store screenshots.
Androidscreenshotsscreengrab (instrumented tests on an emulator) → optional upload (UPLOAD_SCREENSHOTS=1). See store screenshots.
Androidbetagradle builds a release AAB (or uses AAB_PATH; dotnet publish for MAUI) → upload_to_play_store to the internal track

The templates use the documented fastlane interfaces: app_store_connect_api_key with match, pilot and deliver with API key auth, and supply with a service account JSON. See the sources at the end.

Secrets ​

Nothing secret is committed. Create these as repository secrets, and the workflow maps them into the lanes:

SecretUsed for
ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8App Store Connect API key. The .p8 file is written to a temp path in the job, then deleted.
MATCH_GIT_URL, MATCH_PASSWORDEncrypted certificates repo for match (readonly on CI)
APPLE_TEAM_ID, APP_IDENTIFIER, IOS_SCHEMEiOS app and build settings
PLAY_SERVICE_ACCOUNT_JSONGoogle Play Developer API (supply)
ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORDUpload keystore, passed to Gradle as android.injected.signing.*
ANDROID_PACKAGE_NAMEPlay package name
MAUI_CODESIGN_KEY, MAUI_PROVISIONING_PROFILE (variables, not secrets).NET MAUI iOS signing: the identity and profile name that match installed. See .NET MAUI.

Lane environment variables ​

The lanes read these from the environment. The CI jobs set them; set them yourself for a local run.

VariableLaneMeaning
IOS_PROJECT, IOS_WORKSPACEiOSXcode project or workspace, when it is not at the default place. Passed to build_app and snapshot.
IPA_PATHiOSAn IPA that another step already built and signed. The lane skips build_app and uploads it.
IPA_MAX_AGE_HOURSiOSHow old IPA_PATH may be (default 24). An older IPA is refused.
MAUI_PROJECTiOS, AndroidThe .NET MAUI .csproj, relative to the app folder. The lane runs dotnet publish instead of Gradle or build_app.
ANDROID_GRADLE_DIRAndroidThe Gradle root, relative to the Android folder (Kotlin Multiplatform: ..).
ANDROID_GRADLE_MODULEAndroidThe module that holds the app (default app). Kotlin Multiplatform: androidApp.
AAB_PATHAndroidA bundle that another step already built. The lane skips Gradle.

Create the App Store Connect API key once: the Account Holder requests API access under Integrations, then an Account Holder or Admin creates a team key. You can download the .p8 once only. Team keys cover all apps and cannot be limited per app.

Runner ​

The workflow asks for runs-on: [self-hosted, kinhin-xcode], the Xcode image label. If your pool uses another label, change it in .github/workflows/mobile-release.yml. kinhin does not store any of these secrets.

Versions as variables. The toolchain versions are set in the workflow, but you can override them without editing it: DOTNET_VERSION (MAUI's .NET SDK, default 10.0.x) and JAVA_VERSION (the Java for the Android job; defaults to 21 for Capacitor and 17 otherwise). Set them as repository variables to test a newer SDK or JDK. The Actions stay pinned to commit SHAs, so a newer action needs its SHA updated in the templates.

Ruby. fastlane 2.241, which the ~> 2.240 pin resolves to, needs Ruby 3.2 or newer. The macOS system Ruby is 2.6, so the workflow installs Homebrew's Ruby on the runner before bundle install. Homebrew's Ruby is newer than 3.2 and ships bundler. Locally, use Ruby 3.2 or newer (for example rbenv or Homebrew's ruby@3.3).

Other CI systems ​

GitHub gets the workflow described above. Every other system gets two files: .kinhin/mobile-release.sh in the app folder, which holds the release steps as shell (the framework's setup, the signing files from secrets, bundle install, the lane), and a small pipeline file that starts it by hand, targets the Xcode runner, passes the secrets as environment variables and keeps the screenshots. The script takes the platform and lane: bash .kinhin/mobile-release.sh ios beta.

--ciPipeline fileRunnerStart it from
gitea, forgejo.gitea/workflows/mobile-release.yml, .forgejo/workflows/mobile-release.ymlruns-on: [kinhin-xcode]Actions > mobile-release > Run workflow
gitlab.gitlab/mobile-release.yml, included from .gitlab-ci.ymltags: [kinhin-xcode]Build > Pipelines > Run pipeline (set MOBILE_LANE)
bitbucketbitbucket-pipelines.yml (a custom pipeline)runs-on: [self.hosted, macos, kinhin-xcode]Pipelines > Run pipeline > custom: mobile-release
azure.azure-pipelines/mobile-release.ymlthe kinhin pool, demands: [kinhin_xcode]a pipeline made from that file
buildkite.buildkite/mobile-release.ymlagents: { queue: kinhin }a pipeline whose steps upload that file

kinhin never edits a pipeline file the repository already has. With an existing .gitlab-ci.yml it prints the include: line to add; with an existing bitbucket-pipelines.yml it writes bitbucket-pipelines.mobile-release.yml beside it, for you to copy the mobile-release entry from.

There are no setup actions outside GitHub, so the runner image supplies the tools: Ruby 3.2 or newer (the script installs ruby@3.3 with mise when the image has mise and an older Ruby), and Node, Flutter, Java or .NET as the app needs. Declare them in the repository's .kinhin/config.yml (toolchains; kinhin toolchains detect --write proposes the list). Gitea and Forgejo run actions/checkout and actions/upload-artifact (v3, the artifact protocol those servers implement) by full GitHub URL, so the image also needs Node.js for those two actions.

Framework notes ​

The generated CI jobs run the framework's own setup before the lanes, so a clean checkout builds without manual steps. A monorepo app works too; see Monorepos.

Flutter. The jobs install the Flutter SDK (subosito/flutter-action) and run flutter pub get, which also writes android/local.properties. On iOS they run flutter build ios --config-only --release before CocoaPods, so the generated Flutter config exists when the Xcode project is built. The Android lane then builds the signed bundle with Gradle, using the same signing properties as the native projects, and finds it at build/app/outputs/bundle/release/. Set AAB_PATH only when another step has already built a bundle; then Gradle is skipped. An IPA build needs a distribution certificate, not a development one. The iOS deployment target must be 15.0 or higher for the Xcode 26 toolchain: flutter create still writes 13.0, which fails with "deployment target ... is set to 13.0". The flutter_app example sets 15.0 in ios/Runner.xcodeproj/project.pbxproj.

React Native (bare). The jobs set up Node 22 and install dependencies before CocoaPods and Gradle run: npm ci when there is a package-lock.json, yarn install --frozen-lockfile when there is a yarn.lock, pnpm install --frozen-lockfile when there is a pnpm-lock.yaml, and npm install otherwise. No official fastlane guide exists for React Native. The layout follows fastlane's standard setup. The e2e checks the same steps (npm, pod install, a simulator build and the signed Android bundle), but a real store upload is still unverified. The Android build needs ANDROID_HOME on the runner, or an android/local.properties with sdk.dir.

Native. Works for projects with ios/ and android/ folders, or with the project files at the root.

Capacitor and Ionic ​

Capacitor keeps its iOS project in ios/App/App.xcodeproj, one level below the fastlane folder. The CI job exports IOS_PROJECT=App/App.xcodeproj for that, so the iOS lanes find the project. For a local run, set it yourself:

bash
cd ios
IOS_PROJECT=App/App.xcodeproj bundle exec fastlane beta

Capacitor 8's Android library needs Java 21, so the Android job uses Java 21 for Capacitor projects (the other frameworks use Java 17). Capacitor 8 resolves its iOS packages with Swift Package Manager, so there is no Podfile.

Capacitor generates ios/App/App.xcodeproj, and the kinhin repository ignores *.xcodeproj for its XcodeGen examples, so a Capacitor project that lives in a kinhin repository needs an exception in its .gitignore. examples/capacitor_app shows it.

Kotlin Multiplatform ​

Detected by an iosApp/ folder, Gradle settings, and the multiplatform plugin in a build file (the root or shared/). The Android module is androidApp under a Gradle root one folder above androidApp/. The CI jobs set ANDROID_GRADLE_DIR=.. and ANDROID_GRADLE_MODULE=androidApp, so the lanes build :androidApp:bundle. For a local run, set them yourself. The iOS build runs the Xcode build phase, which calls ./gradlew :shared:embedAndSignAppleFrameworkForXcode. Run Gradle with JDK 17 (JDK 26 fails with Kotlin), and export JAVA_HOME before a plain ./gradlew run.

.NET MAUI ​

Detected by a .csproj with <UseMaui>true</UseMaui> at the root or one folder down. The iOS build needs macOS and the .NET 10 SDK with the MAUI workload (dotnet workload install maui). The CI job installs the SDK with actions/setup-dotnet, but the Linux catalog entry still pins the apt dotnet-sdk-8.0, which cannot build MAUI for .NET 10. MAUI's iOS build runs on macOS only.

fastlane signs nothing for MAUI. The lanes call dotnet publish with MAUI_PROJECT set to the .csproj (the CI jobs set it). The iOS lane runs after match, so it can sign with the certificate match installed. Set MAUI_CODESIGN_KEY (the signing identity) and MAUI_PROVISIONING_PROFILE (the profile name) as repository variables. The Android lane signs the AAB with the upload keystore. Screenshots are not scaffolded for MAUI, because MAUI does not generate UI test projects.

Expo ​

Expo's recommended path is EAS Build and EAS Submit. Expo local builds are supported, but Expo says the toolchain is yours to provide (fastlane included), only one platform builds per run, and eas.json's fastlane and ndk keys are ignored in local builds. kinhin mobile init therefore writes only eas.json for Expo.

To use fastlane with Expo, create the native folders, then scaffold them as a React Native project:

bash
npm install
npx expo prebuild --no-install   # creates ios/ and android/ (both platforms when no --platform is given)
kinhin mobile init --framework react-native .

The e2e runs this route: it builds the Android bundle through the lane with a signing keystore, and runs pod install and a Release device build for iOS. Expo's own --platform flag takes one value, so leave it out to get both platforms.

Caveats to check before relying on this:

  • EAS Submit uploads to TestFlight. The iOS release lane uploads a build to App Store Connect without submitting it for review.
  • The docs do not say whether eas-build-pre-install hooks run in local builds. Test it before depending on it.

Monorepos ​

kinhin mobile init writes the workflow to the git repository's own .github/workflows/ (the only place GitHub reads workflows from), with paths relative to that repository. For an app at apps/mobile in a monorepo, the jobs run working-directory: apps/mobile/ios and the setup steps run in apps/mobile. Run it from the app folder, or pass the path: kinhin mobile init apps/mobile. A project with no git repository above it gets paths from its own root.

Errors you may see ​

The full list, with the cause and the fix for each, is in the troubleshooting page. The most common ones:

MessageWhereWhat to do
no iOS or Android project in ...; pass --frameworkkinhin mobile initThe folder has no detectable project. Pass --framework.
no ios/ or android/ folder in ... for a flutter projectkinhin mobile initRun flutter create (or react-native init) first.
Missing environment variables: ASC_KEY_ID, ...fastlaneSet the listed secrets. The lane stops before any store call.
ASC_KEY_FILEPATH does not existfastlaneThe .p8 was not written. Check the "Write App Store Connect API key" step.
Keystore not found at ...fastlaneCheck ANDROID_KEYSTORE_BASE64 and the "Write upload keystore" step.
No release AAB at ...fastlaneGradle did not produce the bundle. Check AAB_PATH and the Gradle output.
No IPA at ..., IPA at ... is older than N hoursfastlaneIPA_PATH points at a missing or stale IPA. Rebuild it, or raise IPA_MAX_AGE_HOURS.
dotnet publish wrote no IPA / no AABfastlaneThe MAUI build produced no artifact. Read the dotnet publish output above it.
Cannot locate tasks that match ':androidApp:bundleRelease'fastlaneKotlin Multiplatform: the Gradle task name is set by the lane. Check ANDROID_GRADLE_MODULE.

The MobileScaffoldTests Swift tests cover the scaffolding logic. They run in swift test, which CI and scripts/check.sh both run.

Verification ​

Two checks cover the integration:

  • The MobileScaffoldTests Swift tests cover the scaffolding: detection, keep and force, dry run, monorepo paths and the error paths. They run in swift test, which scripts/check.sh and CI both run.
  • scripts/mobile-e2e.sh runs against a clean copy of the committed examples. It checks the templates still generate the committed examples, runs bundle install and lists the lanes, checks the secret errors in an empty environment, lints the generated workflows with actionlint, and confirms every fastlane option the templates use exists. --with-builds adds the native, Flutter and React Native builds and the signed-bundle check. It needs Ruby 3.2+ (set RUBY_BIN if it is not on the usual path). Building the binary needs Swift; the builds need JDK 17, an Android SDK, Xcode, Flutter, XcodeGen, and jarsigner (part of the JDK). The React Native steps need npm and CocoaPods.

The e2e script does not upload anything and does not read credentials. The store and runner steps still need a real project. The checklist in docs/plans/mobile-fastlane-test-checklist.md lists what is covered and what is not.

Run this checklist on a fixture project before you trust a release:

  1. kinhin mobile init --dry-run /path/to/app lists what would be written.
  2. bundle install succeeds with Ruby 3.2 or newer.
  3. bundle exec fastlane lanes lists the lanes, run from each platform's folder (see Quick start).
  4. Trigger mobile-release with lane: beta against a test app and confirm the TestFlight or internal track build.
  5. Re-run kinhin mobile init and confirm existing files are kept.

Sources ​

Checked 2026-10-11. The versions and dates below are what those pages showed.

Unverified: the React Native layout, Play App Signing details, and the Xcode version needed for uploads. Apple's help page and a third-party source disagree on that version, so check Apple's developer news.