This project provides a base Fastfile to minimize the amount of configuration required to build a project on CI using manual signing with fastlane.
The base Fastfile provides basic model classes to make it easier to work with. These classes includes:
- Project
- AppExtension
- Configuration
- Certificate
- ProvisioningProfile
A build is produced by providing a Project object with a Configuration object. To sign a build using Mirego's enterprise certificate, a betaConfiguration object is declared for you containing all the needed information about the certificate and the provisioning profile.
Important to note that this Fastfile is just a starting point, if you need more flexibility or more advanced features I encourage you to use fastlane as it pleases you.
Start by importing the toolkit at the very top of your Fastfile and defining your project by creating a Project instance describing what's contained in your repository.
import_from_git(url: "[email protected]:mirego/fastlane-toolkit.git")
# ...
sampleProject = Model::Project.new(
workspacePath: "Sample.xcworkspace",
projectPath: "Sample.xcodeproj",
infoPlistPath: "Sample/Info.plist",
scheme: "Sample",
target: "Sample",
bundleIdentifier: "com.mirego.Sample"
)Once it's done, create a lane that import the toolkit and that calls the provided build_ios_app_with_toolkit lane with your project. You can either explicitly specify the enterprise configuration by calling build_ios_app_with_toolkit(project: sampleProject, configuration: enterprise_configuration()) or simply omit the configuration parameter as it is the default value when none is supplied.
desc "Build using the enterprise certificate and publish on AppCenter"
lane :beta do
cocoapods(use_bundle_exec: true, try_repo_update_on_error: true)
build_ios_app_with_toolkit(project: sampleProject)
changelog_from_git_commits(commits_count: 10)
appcenter_upload(
api_token: strip_quotes(ENV["APP_CENTER_API_TOKEN"]),
owner_type: "organization",
owner_name: "ORG_NAME",
app_name: "APP_NAME",
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
dsym: lane_context[SharedValues::DSYM_OUTPUT_PATH],
destination_type: "group"
destinations: ENV["APP_CENTER_DISTRIBUTION_GROUPS"],
)
endIf you need to sign your build using a custom signing certificate, create your custom configuration object and call the build_ios_app_with_toolkit lane with it.
appStoreProvisioningProfile = Model::ProvisioningProfile.new(
path: "./fastlane/provisioning/AppStore.mobileprovision"
)
appStoreCertificate = Model::Certificate.new(
path: "./fastlane/provisioning/AppStore.p12",
name: "iPhone Distribution: Sample (????????)",
password: "SuperStrongPassword"
)
appStoreConfiguration = Model::Configuration.new(
certificate: appStoreCertificate,
provisioningProfile: appStoreProvisioningProfile,
buildConfiguration: "Release",
exportMethod: "app-store"
)
lane :release do
build_ios_app_with_toolkit(project: sampleProject, configuration: appStoreConfiguration)
upload_to_app_store(force: true)
slack(message: "Successfully submitted #{sampleProject.target} to AppStore", slack_url: "https://hooks.slack.com/services/T025F65SP/AAW2V1FC3/rgMjwWCk21ag79rjdhbfDS78G")
endIf you need to change the bundle identifier of your app before building it, simply assign the bundleIdentifierOverride property of your configuration object prior to calling the build_ios_app_with_toolkit lane.
betaConfiguration.bundleIdentifierOverride = "com.mirego.Sample.beta"You can also simply re-assign the bundle identifier of your Project instance.
sampleProject.bundleIdentifier = "com.mirego.Sample.beta"If your app contains app extensions, you must provide them via your Project instance.
notificationExtension = Model::AppExtension.new(
target: "SampleNotifications",
bundleIdentifier: "com.mirego.Sample.notifications",
infoPlistPath: "SampleNotifications/Info.plist"
)
sampleProject.extensions = [notificationExtension]You also need to provide the provisioning profile to use for each of the registered app extensions in your configuration. The property takes a Hash (key value pair) of the extension bundle identifier to a ProvisioningProfile instance.
notificationExtensionProvisioningProfile = Model::ProvisioningProfile.new(
path: "./fastlane/provisioning/AppStoreNotifications.mobileprovision"
)
configuration.extensionProvisioningProfiles = {
notificationExtension.bundleIdentifier => notificationExtensionProvisioningProfile
}Bitcode is enabled by default but if for some reason you need it disabled, you can do so with the include_bitcode option.
build_ios_app_with_toolkit(project: sampleProject, configuration: configuration, include_bitcode: false)If you need to provide Xcode extra environment variables, you can do so using the xcargs option of the build_ios_app_with_toolkit action.
build_ios_app_with_toolkit(project: sampleProject, configuration: configuration, xcargs: "ENABLE_CONFIG_PANEL=true")The project also includes some custom actions described here.
Upload an IPA build to App Store Connect using the modern Build Uploads API (App Store Connect API 4.1). This action provides a more reliable and future-proof alternative to upload_to_testflight, with better error handling and progress tracking.
- Uses Apple's latest Build Uploads API endpoints
- Automatically extracts build metadata (bundle ID, version, build number) from IPA
- Chunked file uploads with automatic retry logic and exponential backoff
- Optional build processing status tracking
- Supports all Apple platforms (iOS, macOS, tvOS, watchOS, visionOS)
| Parameter | Environment Variable | Description | Default | Required |
|---|---|---|---|---|
ipa |
UPLOAD_BUILD_IPA |
Path to the IPA file to upload | IPA output path from lane context | Yes |
api_key_path |
APP_STORE_CONNECT_API_KEY_PATH |
Path to your App Store Connect API Key JSON file | - | No* |
api_key |
APP_STORE_CONNECT_API_KEY |
App Store Connect API Key information (Hash) | - | No* |
app_identifier |
UPLOAD_BUILD_APP_IDENTIFIER |
The bundle identifier of your app | Value from Appfile | No** |
apple_id |
UPLOAD_BUILD_APPLE_ID |
The Apple ID of your app | - | No** |
platform |
UPLOAD_BUILD_PLATFORM |
Platform of the build | IOS |
No |
skip_waiting_for_build_processing |
UPLOAD_BUILD_SKIP_WAITING_FOR_PROCESSING |
Skip waiting for build processing to complete | false |
No |
processing_timeout |
UPLOAD_BUILD_PROCESSING_TIMEOUT |
Timeout for build processing in seconds | 3600 (1 hour) |
No |
max_upload_retries |
UPLOAD_BUILD_MAX_UPLOAD_RETRIES |
Maximum number of retries for uploading chunks | 10 |
No |
* Either api_key_path, api_key, or a prior call to app_store_connect_api_key is required
** Either app_identifier or apple_id is required
Valid values for the platform parameter:
IOS(default)MAC_OSTV_OSWATCH_OSVISION_OS
Returns a Hash containing:
build_upload_id: The ID of the build uploadupload_file_id: The ID of the uploaded fileapp_id: The App Store Connect app IDversion: The app version from the IPAbuild_number: The build number from the IPA
With custom timeout and retry settings:
upload_build_to_app_store_connect(
ipa: "./MyApp.ipa",
api_key_path: "./AuthKey.json",
app_identifier: "com.example.myapp",
processing_timeout: 7200, # 2 hours
max_upload_retries: 10
)Using with app_store_connect_api_key action:
app_store_connect_api_key(
key_id: "D383AB000",
issuer_id: "6053b7fe-68a8-6acb-00be-165aa0000000",
key_filepath: "./AuthKey_D383SF000.p8"
)
# build_ios_app ...
upload_build_to_app_store_connect(
app_identifier: "com.example.myapp"
)Create a configuration containing a generic provisioning profile and the enterprise certificate. This action take care of extracting informations in environment variables and must be run on Jenkins in order to work.
Internally required by the build_ios_app_with_toolkit private lane, the install_provisioning_profile action take care of parsing the provisioning profile and install it in the proper location so that Xcode can use it.
Use icon_badge plugin to add badge icon to your application icon.
To install:
bundle exec fastlane add_plugin icon_banner
-
Prepare an environment for the run
- Keep Jenkins Environment Variables
- Keep Jenkins Build Variables
- Properties File Path:
${HOME}/.build_ios_env
-
Build
- Execute shell
bundle install bundle exec fastlane beta
- Execute shell
- Color ANSI Console Output:
xterm
String clientName = 'client'
String projectDisplayName = 'Sample'
String projectName = 'sample'
String folderName = 'Client Display Name'
String slackNotificationChannel = '#project-channel'
folder("$folderName") {
description('Jobs related to ' + clientName.capitalize())
}
job("$folderName/$clientName-$projectName-watcher") {
description("Repository watcher for master branch of the $projectDisplayName mobile app")
scm {
git {
branch('origin/master')
remote {
name('origin')
url("${GIT_URL}")
credentials('github')
}
extensions {
submoduleOptions {
recursive()
}
}
}
}
triggers {
scm('H/5 * * * *')
}
steps {
triggerBuilder {
configs {
blockableBuildTriggerConfig {
projects("$folderName/$clientName-$projectName-ios-fastlane")
block {
buildStepFailureThreshold("never")
unstableThreshold("never")
failureThreshold("never")
}
configs {
predefinedBuildParameters {
textParamValueOnNewLine(false)
properties('''Branch=${GIT_BRANCH}
Lane=beta''')
}
}
}
}
}
}
}
job("$folderName/$clientName-$projectName-ios-fastlane") {
description("Builds $projectDisplayName Sample iOS app")
logRotator {
numToKeep(5)
}
parameters {
stringParam {
name('Branch')
defaultValue('origin/master')
description('The git branch to be built')
trim(true)
}
choiceParam('Lane', ['beta', 'app_store'], 'Name of the lane to run in fastlane')
}
environmentVariables {
keepBuildVariables(true)
keepSystemVariables(true)
propertiesFile('${HOME}/.build_ios_env')
}
scm {
git {
branch('${Branch}')
remote {
name('origin')
url("${GIT_URL}")
credentials('github')
}
extensions {
submoduleOptions {
recursive(true)
}
wipeOutWorkspace()
}
}
}
steps {
shell('''bundle install
bundle exec fastlane ${Lane}''')
}
wrappers {
colorizeOutput()
}
publishers {
jUnitResultArchiver {
testResults('fastlane/test_output/report.junit')
}
cobertura('cobertura.xml') {
failNoReports(false)
sourceEncoding('ASCII')
methodTarget(80, 0, 0)
lineTarget(80, 0, 0)
conditionalTarget(70, 0, 0)
}
slackNotifier {
notifyBackToNormal(true)
notifyFailure(true)
room(slackNotificationChannel)
}
}
}If you find that something else could be useful or if your use case is not covered and you feel that it could benefit others, please take the time to contribute by opening a pull request or open an issue asking for it very very kindly ;)