Files
paseo/packages/app/fastlane/Fastfile
T
Mohamed Boudra 2d97d04805 Publish store screenshots from the repo on each stable release (#8186)
* feat(release): publish store screenshots from the repo

Stable releases now upload the committed store renders: the App Store
review lane attaches the iPhone and iPad sets to the version it submits,
and a new Play lane replaces the phone and tablet screenshots after the
Android submit. F-Droid reads the same Android files, which now link to
the store renders instead of the website images.

The Play lane needs its own copy of the submit service-account key
(GOOGLE_PLAY_JSON_KEY in the EAS production environment) because the key
Expo stores for eas submit is not available to custom jobs, and EAS
Metadata does not handle screenshots for either store.

* fix(release): rewrite the Android screenshot links on every render

Google Play and F-Droid read numbered links to the Google renders. The
renderer now rewrites them from the panels it exported, so adding or
renaming a panel reaches all three stores.

(cherry picked from commit 7c88a48be4)
2026-10-29 08:42:59 +01:00

146 lines
5.4 KiB
Ruby

require "fileutils"
require "tmpdir"
default_platform(:ios)
APP_IDENTIFIER = "sh.paseo"
# Store screenshots are rendered into assets/store-screenshots/output; see the
# README there. Google Play and F-Droid both read fastlane/metadata/android at
# the repo root, whose screenshot files link to those renders.
REPO_ROOT = File.expand_path("../../..", __dir__)
STORE_SCREENSHOTS = File.join(REPO_ROOT, "assets/store-screenshots/output")
ANDROID_METADATA = File.join(REPO_ROOT, "fastlane/metadata/android")
# deliver reads screenshots from one folder per language and picks the device
# slot from each image's size, so both Apple sets go into en-US together.
def stage_app_store_screenshots
root = Dir.mktmpdir("app-store-screenshots")
locale = File.join(root, "en-US")
FileUtils.mkdir_p(locale)
{ "apple" => "iphone", "apple-ipad" => "ipad" }.each do |set, device|
Dir.glob(File.join(STORE_SCREENSHOTS, set, "*.png")).sort.each do |file|
FileUtils.cp(file, File.join(locale, "#{device}-#{File.basename(file)}"))
end
end
root
end
platform :ios do
desc "Submit the latest TestFlight build to App Store Review"
lane :submit_review do
api_key = app_store_connect_api_key(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
key_filepath: ENV.fetch("ASC_KEY_P8"),
)
require "spaceship"
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
filepath: ENV.fetch("ASC_KEY_P8"),
)
app = Spaceship::ConnectAPI::App.find(APP_IDENTIFIER)
UI.user_error!("Could not find app #{APP_IDENTIFIER} on App Store Connect") if app.nil?
# The release workflow forwards the marketing version and CFBundleVersion of
# the binary it just built. Use them to identify the exact build, since
# CFBundleVersion is not unique across marketing versions in this project
# (the production profile resets it per app_version). The resubmit workflow
# leaves these unset and falls back to "most recently uploaded iOS build".
target_version = ENV["APP_VERSION"].to_s.strip
target_build = ENV["APP_BUILD_VERSION"].to_s.strip
target_version = nil if target_version.empty?
target_build = nil if target_build.empty?
if target_version
UI.message("Targeting App Store version #{target_version}#{target_build ? " build #{target_build}" : ""} from workflow input")
else
UI.message("No APP_VERSION provided; selecting the most recently uploaded iOS build")
end
valid_build = nil
deadline = Time.now + (30 * 60)
loop do
builds = Spaceship::ConnectAPI::Build.all(
app_id: app.id,
sort: "-uploadedDate",
limit: 100,
).select { |b| b.pre_release_version&.platform == "IOS" }
builds = builds.select { |b| b.pre_release_version&.version == target_version } if target_version
builds = builds.select { |b| b.version == target_build } if target_build
build = builds.first
if build.nil?
UI.user_error!("Timed out waiting for build to appear on App Store Connect.") if Time.now > deadline
UI.message("No matching iOS build visible on App Store Connect yet; waiting...")
sleep(30)
next
end
state = build.processing_state
UI.message("Build #{build.pre_release_version.version} (#{build.version}) processing state: #{state || 'unknown'}")
if state == "VALID"
valid_build = build
break
end
if state == "INVALID" || state == "FAILED"
UI.user_error!("Build #{build.version} failed App Store processing (state: #{state}).")
end
if Time.now > deadline
UI.user_error!("Timed out waiting for build #{build.version} to finish processing on App Store Connect.")
end
sleep(30)
end
app_version = valid_build.pre_release_version.version
build_number = valid_build.version
UI.message("Submitting build #{build_number} as App Store version #{app_version} for review")
deliver(
api_key: api_key,
app_identifier: APP_IDENTIFIER,
app_version: app_version,
build_number: build_number.to_s,
submit_for_review: true,
automatic_release: true,
reject_if_possible: true,
release_notes: {
"en-US" => "Bug fixes and improvements.",
},
force: true,
skip_binary_upload: true,
screenshots_path: stage_app_store_screenshots,
overwrite_screenshots: true,
precheck_include_in_app_purchases: false,
submission_information: {
add_id_info_uses_idfa: false,
export_compliance_uses_encryption: false,
},
)
end
end
platform :android do
desc "Replace the Google Play store screenshots with the ones in the repo"
lane :upload_screenshots do
# supply attaches listing changes to a release on the production track.
# The release workflow passes the version code it just submitted; without
# it, supply uses the track's only release.
version_code = ENV["ANDROID_VERSION_CODE"].to_s.strip
supply(
package_name: APP_IDENTIFIER,
json_key: ENV.fetch("GOOGLE_PLAY_JSON_KEY"),
metadata_path: ANDROID_METADATA,
version_code: version_code.empty? ? nil : version_code.to_i,
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
sync_image_upload: true,
)
end
end