a6dcb5605c
El envio de la 2.0.2 fallo con "could not find an editable version for 'IOS'": deliver no crea el registro de version, y con la 2.0.1 ya en READY_FOR_SALE no habia nada editable. Con la 2.0.1 no se noto porque la version ya existia. ensure_editable_version usa app.ensure_version! de spaceship y avisa antes de renumerar un borrador ajeno. Login de Connect API extraido a connect_api_app, que ya compartia status. Ademas shipped_app_version leia un path relativo y el directorio de trabajo de la lane no es la raiz del repo, asi que devolvia vacio y ASC rechazaba el POST por versionString ausente. Ahora es absoluto respecto al Fastfile y falla ruidoso si no puede leerlo. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
197 lines
7.0 KiB
Ruby
197 lines
7.0 KiB
Ruby
default_platform(:ios)
|
|
|
|
# Credentials: App Store Connect API key via `pass`.
|
|
# The api_key block uses the key inline — no session/2FA needed.
|
|
|
|
platform :ios do
|
|
EXPORT_OPTIONS = {
|
|
method: "app-store",
|
|
signingStyle: "manual",
|
|
manageAppVersionAndBuildNumber: false,
|
|
provisioningProfiles: {
|
|
"com.alexandrevazquez.mealmood" => "com.alexandrevazquez.mealmood AppStore",
|
|
"com.alexandrevazquez.mealmood.widget" => "com.alexandrevazquez.mealmood.widget AppStore"
|
|
}
|
|
}
|
|
|
|
def asc_api_key
|
|
app_store_connect_api_key(
|
|
key_id: `pass show appstore/api-key-id`.strip,
|
|
issuer_id: `pass show appstore/issuer-id`.strip,
|
|
key_content: `pass show appstore/api-key-p8`.strip,
|
|
in_house: false
|
|
)
|
|
end
|
|
|
|
desc "Push a new beta build to TestFlight"
|
|
lane :beta do
|
|
increment_build_number(xcodeproj: "MealMood.xcodeproj")
|
|
build_app(
|
|
scheme: "MealMood",
|
|
export_method: "app-store",
|
|
sdk: "iphoneos26.5",
|
|
export_options: EXPORT_OPTIONS
|
|
)
|
|
upload_to_testflight(
|
|
api_key: asc_api_key,
|
|
skip_waiting_for_build_processing: true
|
|
)
|
|
end
|
|
|
|
# Firebase arrives via SPM, so upload-symbols sits in DerivedData instead of
|
|
# the ./Pods path fastlane looks in by default.
|
|
def crashlytics_upload_symbols_binary
|
|
pattern = File.expand_path(
|
|
"~/Library/Developer/Xcode/DerivedData/MealMood-*/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols"
|
|
)
|
|
found = Dir.glob(pattern)
|
|
if found.empty?
|
|
UI.user_error!("upload-symbols not found. Build the project once so SPM checks out firebase-ios-sdk.")
|
|
end
|
|
found.max_by { |path| File.mtime(path) }
|
|
end
|
|
|
|
# Every dSYM (app + widget) of the most recently archived build.
|
|
def latest_archive_dsyms
|
|
pattern = File.expand_path("~/Library/Developer/Xcode/Archives/*/MealMood*.xcarchive/dSYMs/*.dSYM")
|
|
by_archive = Dir.glob(pattern).group_by { |path| path[/.*\.xcarchive/] }
|
|
newest = by_archive.max_by { |archive, _| File.mtime(archive) }
|
|
newest ? newest.last : []
|
|
end
|
|
|
|
# The shipped version lives in the Info.plist, not in MARKETING_VERSION.
|
|
# Absolute path: the lane's working directory is not guaranteed to be the repo root.
|
|
def shipped_app_version
|
|
plist = File.expand_path("../MealMood/Resources/Info.plist", __dir__)
|
|
value = `/usr/bin/plutil -extract CFBundleShortVersionString raw "#{plist}"`.strip
|
|
UI.user_error!("Could not read the app version from #{plist}") if value.empty?
|
|
value
|
|
end
|
|
|
|
# asc_api_key returns fastlane's option hash, not a Spaceship token.
|
|
def connect_api_app
|
|
require 'spaceship'
|
|
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create(
|
|
key_id: `pass show appstore/api-key-id`.strip,
|
|
issuer_id: `pass show appstore/issuer-id`.strip,
|
|
key: `pass show appstore/api-key-p8`.strip
|
|
)
|
|
Spaceship::ConnectAPI::App.find("com.alexandrevazquez.mealmood")
|
|
end
|
|
|
|
# `deliver` does not create the App Store version record — it fails with
|
|
# "could not find an editable version for 'IOS'" when the previous version is
|
|
# already READY_FOR_SALE and nothing is open for editing.
|
|
def ensure_editable_version(version)
|
|
app = connect_api_app
|
|
editable = app.get_edit_app_store_version(platform: "IOS")
|
|
|
|
if editable && editable.version_string != version
|
|
# ensure_version! would silently renumber it — say so before it happens.
|
|
UI.important("App Store Connect had #{editable.version_string} open for editing; renaming it to #{version}")
|
|
end
|
|
|
|
created = app.ensure_version!(version, platform: "IOS")
|
|
UI.success(created ? "App Store version #{version} ready" : "App Store version #{version} already editable")
|
|
end
|
|
|
|
desc "Show the App Store state of each version (review status, release type)"
|
|
lane :status do
|
|
app = connect_api_app
|
|
app.get_app_store_versions(includes: "build").each do |v|
|
|
UI.message("#{v.version_string} — #{v.app_store_state} — release: #{v.release_type} — build: #{v.build&.version}")
|
|
end
|
|
end
|
|
|
|
# For a build that is already on TestFlight: `release` would rebuild it and
|
|
# `publish` never submits. Screenshots are left alone — they carry over.
|
|
desc "Submit an already-uploaded build for App Store review (automatic release)"
|
|
lane :submit do |options|
|
|
UI.user_error!("pass build:<number>") unless options[:build]
|
|
version = options[:version] || shipped_app_version
|
|
ensure_editable_version(version)
|
|
upload_to_app_store(
|
|
api_key: asc_api_key,
|
|
app_version: version,
|
|
build_number: options[:build],
|
|
skip_binary_upload: true,
|
|
skip_metadata: false,
|
|
skip_screenshots: true,
|
|
overwrite_screenshots: false,
|
|
submit_for_review: true,
|
|
automatic_release: true,
|
|
force: true,
|
|
precheck_include_in_app_purchases: false,
|
|
run_precheck_before_submit: false
|
|
)
|
|
end
|
|
|
|
# Safety net for the Xcode build phase, which only uploads the dSYM produced
|
|
# locally. Run it once App Store Connect has finished processing the build —
|
|
# `beta` uses skip_waiting_for_build_processing, so right after an upload the
|
|
# dSYMs are not there yet and this would download nothing.
|
|
desc "Download processed dSYMs from App Store Connect and send them to Crashlytics"
|
|
lane :refresh_dsyms do |options|
|
|
download_dsyms(
|
|
api_key: asc_api_key,
|
|
version: options[:version] || "latest",
|
|
build_number: options[:build]
|
|
)
|
|
|
|
dsyms = lane_context[SharedValues::DSYM_PATHS] || []
|
|
if dsyms.empty?
|
|
# Expected for a post-bitcode app: Apple recompiles nothing, so it keeps
|
|
# no dSYM of its own. Fall back to the ones the local archive produced.
|
|
dsyms = latest_archive_dsyms
|
|
UI.important("App Store Connect has no dSYMs for this build; using the latest local archive.")
|
|
end
|
|
|
|
if dsyms.empty?
|
|
UI.important("No dSYMs found either — nothing to upload.")
|
|
next
|
|
end
|
|
|
|
upload_symbols_to_crashlytics(
|
|
gsp_path: "MealMood/Resources/GoogleService-Info.plist",
|
|
binary_path: crashlytics_upload_symbols_binary,
|
|
dsym_paths: dsyms
|
|
)
|
|
clean_build_artifacts
|
|
end
|
|
|
|
desc "Upload metadata + localized screenshots for an existing TestFlight build (no rebuild, no submit)"
|
|
lane :publish do |options|
|
|
version = options[:version] || "1.2.0"
|
|
build = options[:build] || "55"
|
|
upload_to_app_store(
|
|
api_key: asc_api_key,
|
|
app_version: version,
|
|
build_number: build,
|
|
skip_binary_upload: true,
|
|
skip_metadata: false,
|
|
skip_screenshots: false,
|
|
overwrite_screenshots: true,
|
|
submit_for_review: false,
|
|
automatic_release: false,
|
|
precheck_include_in_app_purchases: false,
|
|
force: true,
|
|
run_precheck_before_submit: false
|
|
)
|
|
end
|
|
|
|
desc "Push a new release build to the App Store"
|
|
lane :release do
|
|
increment_build_number(xcodeproj: "MealMood.xcodeproj")
|
|
build_app(
|
|
scheme: "MealMood",
|
|
sdk: "iphoneos26.5",
|
|
export_options: EXPORT_OPTIONS
|
|
)
|
|
upload_to_app_store(
|
|
api_key: asc_api_key,
|
|
automatic_release: true,
|
|
force: true
|
|
)
|
|
end
|
|
end
|