Files
pantry-flutter/fastlane/Fastfile

324 lines
9.5 KiB
Ruby

require "digest"
require "json"
# -- Shared helpers --
def version_info
pubspec = File.read(File.expand_path("../pubspec.yaml", __dir__))
version = pubspec.match(/^version:\s*(.+)$/)[1].strip
name, build = version.split("+")
{ name: name, build: build }
end
def version_name
v = version_info
"#{v[:build]} (#{v[:name]})"
end
def changelog_notes
version = version_info[:name]
changelog_path = File.expand_path("../CHANGELOG.md", __dir__)
return "Release #{version}" unless File.exist?(changelog_path)
changelog = File.read(changelog_path)
escaped = Regexp.escape(version)
pattern = /^## (?:\[#{escaped}\]|#{escaped}[\s(]).*?\n(.*?)(?=^## |\z)/m
match = changelog.match(pattern)
return "Release #{version}" unless match
match[1].strip
.gsub(/\s*\(\[[\da-f]+\]\([^)]+\)\)/, "")
.gsub(/^\*\s+\*\*[^*]+:\*\*\s*/m, "- ")
.gsub(/^\*\s+/, "- ")
.gsub(/^### /, "")
.gsub(/\n{3,}/, "\n\n")
.strip
.then { |n| n.empty? ? "Release #{version}" : n }
end
# -- Android --
default_platform(:android)
platform :android do
# Google Play enforces a 500-char limit on release notes per language.
PLAY_NOTES_MAX = 500
PLAY_NOTES_TRAILER = "\n\n… see full notes on GitHub."
def play_changelog
notes = changelog_notes
return notes if notes.length <= PLAY_NOTES_MAX
budget = PLAY_NOTES_MAX - PLAY_NOTES_TRAILER.length
truncated = notes[0, budget]
last_newline = truncated.rindex("\n")
truncated = truncated[0, last_newline] if last_newline && last_newline > budget / 2
truncated.rstrip + PLAY_NOTES_TRAILER
end
# -- Image change detection --
IMAGE_HASH_CACHE = File.expand_path(".image_hashes.json", __dir__)
def current_image_hashes
root = File.expand_path("metadata/android", __dir__)
return {} unless Dir.exist?(root)
files = Dir.glob(File.join(root, "**/images/**/*")).select { |f| File.file?(f) }
files.sort.each_with_object({}) do |path, acc|
rel = path.sub("#{root}/", "")
acc[rel] = Digest::SHA256.file(path).hexdigest
end
end
def cached_image_hashes
return {} unless File.exist?(IMAGE_HASH_CACHE)
JSON.parse(File.read(IMAGE_HASH_CACHE))
rescue StandardError
{}
end
def images_changed?
current_image_hashes != cached_image_hashes
end
def save_image_hashes
File.write(IMAGE_HASH_CACHE, JSON.pretty_generate(current_image_hashes))
end
desc "Upload AAB to Google Play"
lane :deploy do |options|
# Write changelog if not already present (CI generates it during the
# release PR, but this covers manual deploys).
version_code = version_info[:build]
changelog_dir = File.expand_path("metadata/android/en-US/changelogs", __dir__)
changelog_file = File.join(changelog_dir, "#{version_code}.txt")
unless File.exist?(changelog_file)
FileUtils.mkdir_p(changelog_dir)
File.write(changelog_file, play_changelog)
end
changed = images_changed?
UI.message(changed ? "Images changed — uploading." : "Images unchanged — skipping.")
upload_to_play_store(
aab: File.expand_path("../build/app/outputs/bundle/release/app-release.aab", __dir__),
track: options[:track] || "internal",
release_status: options[:status] || "draft",
version_name: version_name,
metadata_path: File.expand_path("metadata/android", __dir__),
skip_upload_images: !changed,
skip_upload_screenshots: !changed,
)
save_image_hashes if changed
end
desc "Sync metadata and screenshots only (no AAB upload)"
lane :metadata do
changed = images_changed?
UI.message(changed ? "Images changed — uploading." : "Images unchanged — skipping.")
upload_to_play_store(
skip_upload_aab: true,
skip_upload_apk: true,
skip_upload_changelogs: true,
metadata_path: File.expand_path("metadata/android", __dir__),
skip_upload_images: !changed,
skip_upload_screenshots: !changed,
)
save_image_hashes if changed
end
desc "Promote a release from one track to another"
lane :promote do |options|
from = options[:from] || "internal"
to = options[:to] || "production"
status = options[:status] || "draft"
upload_to_play_store(
track: from,
track_promote_to: to,
release_status: status,
version_name: version_name,
skip_upload_aab: true,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
end
end
# -- iOS --
platform :ios do
def api_key
key_id = ENV.fetch("APP_STORE_API_KEY")
app_store_connect_api_key(
key_id: key_id,
issuer_id: ENV.fetch("APP_STORE_ISSUER_ID"),
key_filepath: File.join(ENV.fetch("APP_STORE_KEY_PATH"), "AuthKey_#{key_id}.p8"),
)
end
def find_ipa
ipa_path = Dir[File.expand_path("../build/ios/ipa/*.ipa", __dir__)].first
UI.user_error!("No IPA found in build/ios/ipa/. Run 'make ios-build' first.") unless ipa_path
ipa_path
end
def sync_release_notes
version_code = version_info[:build]
changelog_file = File.expand_path("metadata/ios/en-US/changelogs/#{version_code}.txt", __dir__)
notes = File.exist?(changelog_file) ? File.read(changelog_file).strip : changelog_notes
release_notes_path = File.expand_path("metadata/ios/en-US/release_notes.txt", __dir__)
File.write(release_notes_path, notes)
UI.message("Synced release notes from build #{version_code} (#{notes.length} chars)")
notes
end
desc "Upload to TestFlight"
lane :beta do
notes = sync_release_notes
upload_to_testflight(
api_key: api_key,
ipa: find_ipa,
changelog: notes,
skip_waiting_for_build_processing: true,
)
end
desc "Upload to App Store"
lane :release do
sync_release_notes
deliver(
api_key: api_key,
ipa: find_ipa,
metadata_path: File.expand_path("metadata/ios", __dir__),
screenshots_path: File.expand_path("metadata/ios/en-US/screenshots", __dir__),
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
desc "Sync iOS metadata only (no IPA upload)"
lane :metadata do
deliver(
api_key: api_key,
metadata_path: File.expand_path("metadata/ios", __dir__),
screenshots_path: File.expand_path("metadata/ios/en-US/screenshots", __dir__),
skip_binary_upload: true,
skip_screenshots: true,
submit_for_review: false,
precheck_include_in_app_purchases: false,
force: true,
)
end
desc "Submit existing App Store build for review (no IPA upload)"
lane :submit do
deliver(
api_key: api_key,
metadata_path: File.expand_path("metadata/ios", __dir__),
screenshots_path: File.expand_path("metadata/ios/en-US/screenshots", __dir__),
skip_binary_upload: true,
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
end
# -- macOS --
platform :mac do
def api_key
key_id = ENV.fetch("APP_STORE_API_KEY")
app_store_connect_api_key(
key_id: key_id,
issuer_id: ENV.fetch("APP_STORE_ISSUER_ID"),
key_filepath: File.join(ENV.fetch("APP_STORE_KEY_PATH"), "AuthKey_#{key_id}.p8"),
)
end
def find_pkg
pkg_path = Dir[File.expand_path("../build/macos/pkg/*.pkg", __dir__)].first
UI.user_error!("No PKG found in build/macos/pkg/. Run 'make macos-build-pkg' first.") unless pkg_path
pkg_path
end
def sync_release_notes
version_code = version_info[:build]
changelog_file = File.expand_path("metadata/macos/en-US/changelogs/#{version_code}.txt", __dir__)
notes = File.exist?(changelog_file) ? File.read(changelog_file).strip : changelog_notes
release_notes_path = File.expand_path("metadata/macos/en-US/release_notes.txt", __dir__)
File.write(release_notes_path, notes)
UI.message("Synced release notes from build #{version_code} (#{notes.length} chars)")
notes
end
desc "Upload to TestFlight (macOS)"
lane :beta do
notes = sync_release_notes
upload_to_testflight(
api_key: api_key,
pkg: find_pkg,
app_platform: "osx",
changelog: notes,
skip_waiting_for_build_processing: true,
)
end
desc "Upload to Mac App Store"
lane :release do
sync_release_notes
deliver(
api_key: api_key,
pkg: find_pkg,
platform: "osx",
metadata_path: File.expand_path("metadata/macos", __dir__),
screenshots_path: File.expand_path("metadata/macos/en-US/screenshots", __dir__),
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
desc "Sync macOS metadata only (no PKG upload)"
lane :metadata do
deliver(
api_key: api_key,
platform: "osx",
metadata_path: File.expand_path("metadata/macos", __dir__),
screenshots_path: File.expand_path("metadata/macos/en-US/screenshots", __dir__),
skip_binary_upload: true,
skip_screenshots: true,
submit_for_review: false,
precheck_include_in_app_purchases: false,
force: true,
)
end
desc "Submit existing Mac App Store build for review (no PKG upload)"
lane :submit do
deliver(
api_key: api_key,
platform: "osx",
metadata_path: File.expand_path("metadata/macos", __dir__),
screenshots_path: File.expand_path("metadata/macos/en-US/screenshots", __dir__),
skip_binary_upload: true,
skip_screenshots: true,
submit_for_review: true,
precheck_include_in_app_purchases: false,
force: true,
)
end
end