mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Add a fastlane pipeline that releases HealthChat to TestFlight (#2)
element-x-ios had no release machinery at all: no fastlane, no Gemfile, and none of its workflows built or uploaded anything. Upstream ships via Xcode Cloud, which needs Element's private Enterprise submodule and so is unavailable here, while the legacy element-ios fork already proves a fastlane pipeline against the same Apple team, certificate and App Store Connect key. Tagging healthchat-v<version> now builds a signed App Store IPA on the org's Mac Studio and uploads it to TestFlight. Release notes come from a new HEALTHCHAT_CHANGES.md, because upstream's own tooling owns CHANGES.md. The lane deliberately does not pin Xcode. The runner carries a single globally selected version, and naming an absent one does not fail fast — it sends xcodes into a multi-GB authenticated download that no CI secret can satisfy. Each run logs xcodebuild -version instead, which is the only record of what compiled a build. Everything here is fork-owned, since upstream has no fastlane directory, so none of it can conflict on rebase.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
## Project configuration
|
||||
|
||||
PROJECT_PATH=ElementX.xcodeproj
|
||||
SCHEME=ElementX
|
||||
|
||||
MAIN_TARGET=ElementX
|
||||
NSE_TARGET=NSE
|
||||
SHARE_EXTENSION_TARGET=ShareExtension
|
||||
|
||||
MAIN_BUNDLE_ID=ch.healthchat.element
|
||||
NSE_BUNDLE_ID=ch.healthchat.element.nse
|
||||
SHARE_EXTENSION_BUNDLE_ID=ch.healthchat.element.shareextension
|
||||
|
||||
## Build configuration
|
||||
|
||||
DERIVED_DATA_PATH=build
|
||||
IPA_NAME=ElementX
|
||||
ARCHIVE_PATH=out/ElementX.xcarchive
|
||||
BUILD_OUTPUT_DIRECTORY=out
|
||||
BUILD_LOG_DIRECTORY=build/Logs/Gym/
|
||||
|
||||
## Provisioning profile files
|
||||
|
||||
PROVISIONING_PROFILES_PATH=./provisioning_profiles/
|
||||
|
||||
MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
|
||||
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
|
||||
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
|
||||
|
||||
## App Store code signing
|
||||
|
||||
APPSTORE_CODESIGNING_IDENTITY="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
|
||||
APPSTORE_SIGNING_CERTIFICATE="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
|
||||
|
||||
APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="HealthChat App Store"
|
||||
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="HealthChat NSE: App Store"
|
||||
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="HealthChat Share Extension: App Store"
|
||||
|
||||
## Account information
|
||||
|
||||
TEAM_ID=6G37K72W82
|
||||
# APPLE_ID is set as a shell environment variable on the machine running fastlane.
|
||||
@@ -0,0 +1,5 @@
|
||||
# For more information about the Appfile, see:
|
||||
# https://docs.fastlane.tools/advanced/#appfile
|
||||
|
||||
apple_id ENV["APPLE_ID"]
|
||||
team_id ENV["TEAM_ID"]
|
||||
@@ -0,0 +1,154 @@
|
||||
default_platform :ios
|
||||
|
||||
# No Xcode version pin: novaloop-mac-studio-001 carries a single, globally-selected Xcode, so
|
||||
# whatever xcode-select points at is by definition the one we build with. Pinning bought nothing
|
||||
# and risked everything — a version the runner lacks sends xcodes into a multi-GB authenticated
|
||||
# download that no CI secret can satisfy, hanging the release. Change the runner's Xcode instead.
|
||||
platform :ios do
|
||||
#### Public ####
|
||||
|
||||
desc "Builds the App Store ipa and uploads it to TestFlight (internal testers only)"
|
||||
# Named release_testflight to avoid colliding with fastlane's built-in :testflight action.
|
||||
lane :release_testflight do |options|
|
||||
# Accept inputs via options (local) or env vars (CI — lets the workflow pass multi-line
|
||||
# release notes without shell-escaping them).
|
||||
marketing_version = options[:marketing_version] || ENV["HEALTHCHAT_MARKETING_VERSION"]
|
||||
release_notes = options[:release_notes] || ENV["HEALTHCHAT_RELEASE_NOTES"]
|
||||
|
||||
UI.user_error!("'marketing_version' is required (pass the option or set HEALTHCHAT_MARKETING_VERSION)") if marketing_version.to_s.empty?
|
||||
|
||||
# Apple caps CFBundleShortVersionString at three period-separated integers. The workflow's
|
||||
# regex is laxer and accepts markers like 26.07.3-rc1, so catch it here — otherwise the run
|
||||
# dies at the upload step after ~30 minutes of signing, looking exactly like a signing failure.
|
||||
unless marketing_version =~ /\A\d+(\.\d+){0,2}\z/
|
||||
UI.user_error!("'marketing_version' must be 1-3 dot-separated integers (got '#{marketing_version}'). " \
|
||||
"Apple rejects anything else at upload. Put markers in the tag or release notes instead.")
|
||||
end
|
||||
|
||||
build_number = options[:build_number] || generate_build_number
|
||||
|
||||
build_release(marketing_version: marketing_version, build_number: build_number)
|
||||
|
||||
upload_to_testflight(
|
||||
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
|
||||
skip_submission: true,
|
||||
skip_waiting_for_build_processing: true,
|
||||
changelog: release_notes,
|
||||
distribute_external: false,
|
||||
)
|
||||
end
|
||||
|
||||
#### Private ####
|
||||
|
||||
desc "Builds the signed App Store ipa"
|
||||
private_lane :build_release do |options|
|
||||
marketing_version = options[:marketing_version]
|
||||
build_number = options[:build_number]
|
||||
UI.user_error!("'build_number' parameter is missing") if build_number.to_s.empty?
|
||||
|
||||
if is_ci
|
||||
%w[APPSTORECONNECT_KEY_ID APPSTORECONNECT_KEY_ISSUER_ID APPSTORECONNECT_KEY_CONTENT].each do |key|
|
||||
UI.user_error!("'#{key}' environment variable should be set to use this lane") if ENV[key].to_s.empty?
|
||||
end
|
||||
|
||||
app_store_connect_api_key(
|
||||
key_id: ENV["APPSTORECONNECT_KEY_ID"],
|
||||
issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"],
|
||||
key_content: ENV["APPSTORECONNECT_KEY_CONTENT"]
|
||||
)
|
||||
else
|
||||
UI.user_error!("'APPLE_ID' environment variable should be set to use this lane") if ENV["APPLE_ID"].to_s.empty?
|
||||
end
|
||||
|
||||
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
|
||||
UI.user_error!("Failed to fetch the team id from the Appfile") if team_id.to_s.empty?
|
||||
|
||||
# Inject the version at build time rather than rewriting project.yml, so no tracked file ever
|
||||
# carries a release-only value. All three targets resolve CFBundleShortVersionString and
|
||||
# CFBundleVersion from these settings, which keeps the app and its extensions consistent —
|
||||
# App Store rejects an upload where they disagree.
|
||||
xcargs_hash = {
|
||||
"MARKETING_VERSION" => marketing_version,
|
||||
"CURRENT_PROJECT_VERSION" => build_number,
|
||||
"CODE_SIGN_STYLE" => "Manual",
|
||||
}
|
||||
xcargs = xcargs_hash.map { |k, v| "#{k}=#{v.to_s.shellescape}" }.join(" ")
|
||||
|
||||
xcodegen(spec: "project.yml")
|
||||
clear_derived_data(derived_data_path: ENV["DERIVED_DATA_PATH"])
|
||||
|
||||
download_provisioning_profiles
|
||||
disable_automatic_code_signing(path: ENV["PROJECT_PATH"])
|
||||
update_project_provisioning_profiles
|
||||
|
||||
build_app(
|
||||
clean: true,
|
||||
scheme: ENV["SCHEME"],
|
||||
xcargs: xcargs,
|
||||
export_method: "app-store",
|
||||
derived_data_path: ENV["DERIVED_DATA_PATH"],
|
||||
archive_path: ENV["ARCHIVE_PATH"],
|
||||
output_directory: ENV["BUILD_OUTPUT_DIRECTORY"],
|
||||
output_name: "#{ENV['IPA_NAME']}.ipa",
|
||||
buildlog_path: ENV["BUILD_LOG_DIRECTORY"],
|
||||
codesigning_identity: ENV["APPSTORE_CODESIGNING_IDENTITY"],
|
||||
skip_profile_detection: true,
|
||||
export_options: {
|
||||
method: "app-store",
|
||||
signingStyle: "manual",
|
||||
teamID: team_id,
|
||||
signingCertificate: ENV["APPSTORE_SIGNING_CERTIFICATE"],
|
||||
provisioningProfiles: {
|
||||
ENV["MAIN_BUNDLE_ID"] => ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"],
|
||||
ENV["NSE_BUNDLE_ID"] => ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"],
|
||||
ENV["SHARE_EXTENSION_BUNDLE_ID"] => ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"],
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
desc "Downloads the three App Store provisioning profiles"
|
||||
private_lane :download_provisioning_profiles do
|
||||
{
|
||||
ENV["MAIN_BUNDLE_ID"] => [ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"], ENV["MAIN_PROVISIONING_PROFILE_FILENAME"]],
|
||||
ENV["NSE_BUNDLE_ID"] => [ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"], ENV["NSE_PROVISIONING_PROFILE_FILENAME"]],
|
||||
ENV["SHARE_EXTENSION_BUNDLE_ID"] => [ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"], ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"]],
|
||||
}.each do |bundle_id, (profile_name, filename)|
|
||||
get_provisioning_profile(
|
||||
app_identifier: bundle_id,
|
||||
provisioning_name: profile_name,
|
||||
ignore_profiles_with_different_name: true,
|
||||
adhoc: false,
|
||||
skip_certificate_verification: false,
|
||||
output_path: ENV["PROVISIONING_PROFILES_PATH"],
|
||||
filename: filename,
|
||||
readonly: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Points each target at its downloaded provisioning profile"
|
||||
private_lane :update_project_provisioning_profiles do
|
||||
{
|
||||
ENV["MAIN_TARGET"] => ENV["MAIN_PROVISIONING_PROFILE_FILENAME"],
|
||||
ENV["NSE_TARGET"] => ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
|
||||
ENV["SHARE_EXTENSION_TARGET"] => ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
|
||||
}.each do |target, filename|
|
||||
update_project_provisioning(
|
||||
xcodeproj: ENV["PROJECT_PATH"],
|
||||
profile: "#{ENV['PROVISIONING_PROFILES_PATH']}#{filename}",
|
||||
# Anchored: target_filter is a regex, and an unanchored "NSE" or "ElementX" would also
|
||||
# match any future target containing that substring.
|
||||
target_filter: "^#{target}$",
|
||||
build_configuration: "Release",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generates a build number from the current timestamp. Monotonic across runs, so a retry never
|
||||
# collides with a build number App Store Connect has already seen.
|
||||
def generate_build_number
|
||||
require 'date'
|
||||
DateTime.now.strftime("%Y%m%d%H%M%S")
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# Autogenerated by fastlane
|
||||
#
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
gem 'fastlane-plugin-xcodegen'
|
||||
Reference in New Issue
Block a user