mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 22:01:21 +00:00
* feat(android): add Wear OS companion Co-authored-by: Sebastian Schubotz <git@sibbl.net> Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> * style(android): format Wear release gate test * test(android): cover Wear release CI contracts * test(android): import Wear JSON fixture type * fix(android): harden Wear proxy event actor * test(android): stabilize Wear proxy actor tests * test(android): isolate Wear proxy actor lifecycles * test(android): own Wear actor dispatchers * test(android): use real time for Wear actor tests * test(android): own Wear actors in test scope * test(android): stop Wear actors explicitly * test(android): drain Wear actor cancellation * test(android): isolate Wear actors from test scheduler * test(android): inject Wear test clock --------- Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
614 lines
21 KiB
Ruby
614 lines
21 KiB
Ruby
require "fileutils"
|
|
require "json"
|
|
require "open3"
|
|
require "shellwords"
|
|
require "supply"
|
|
|
|
default_platform(:android)
|
|
|
|
ANDROID_FASTLANE_ROOT = File.expand_path(__dir__, Dir.pwd)
|
|
DEFAULT_PLAY_PACKAGE_NAME = "ai.openclaw.app"
|
|
DEFAULT_PLAY_TRACK = "internal"
|
|
DEFAULT_PLAY_RELEASE_STATUS = "completed"
|
|
ANDROID_RELEASE_SIGNING_GRADLE_PROPERTIES = [
|
|
"OPENCLAW_ANDROID_STORE_FILE",
|
|
"OPENCLAW_ANDROID_STORE_PASSWORD",
|
|
"OPENCLAW_ANDROID_KEY_ALIAS",
|
|
"OPENCLAW_ANDROID_KEY_PASSWORD"
|
|
].freeze
|
|
|
|
def load_env_file(path)
|
|
return unless File.exist?(path)
|
|
|
|
File.foreach(path) do |line|
|
|
stripped = line.strip
|
|
next if stripped.empty? || stripped.start_with?("#")
|
|
|
|
key, value = stripped.split("=", 2)
|
|
next if key.nil? || key.empty? || value.nil?
|
|
|
|
ENV[key] = value if ENV[key].nil? || ENV[key].strip.empty?
|
|
end
|
|
end
|
|
|
|
def env_present?(value)
|
|
!value.nil? && !value.strip.empty?
|
|
end
|
|
|
|
def android_root
|
|
File.expand_path("..", ANDROID_FASTLANE_ROOT)
|
|
end
|
|
|
|
def repo_root
|
|
File.expand_path("../..", android_root)
|
|
end
|
|
|
|
def android_release_signing_script
|
|
File.join(repo_root, "scripts", "android-release-signing.mjs")
|
|
end
|
|
|
|
def android_release_signing_materialized_properties_path
|
|
File.join(android_root, "build", "release-signing", "gradle.properties")
|
|
end
|
|
|
|
def shell_join(args)
|
|
args.shelljoin
|
|
end
|
|
|
|
def play_package_name
|
|
raw = ENV["GOOGLE_PLAY_PACKAGE_NAME"].to_s.strip
|
|
raw.empty? ? DEFAULT_PLAY_PACKAGE_NAME : raw
|
|
end
|
|
|
|
def play_track
|
|
raw = ENV["GOOGLE_PLAY_TRACK"].to_s.strip
|
|
raw.empty? ? DEFAULT_PLAY_TRACK : raw
|
|
end
|
|
|
|
def wear_play_track
|
|
default_track = play_track == "internal" ? "qa" : play_track
|
|
"wear:#{default_track}"
|
|
end
|
|
|
|
def play_release_status
|
|
raw = ENV["GOOGLE_PLAY_RELEASE_STATUS"].to_s.strip
|
|
raw.empty? ? DEFAULT_PLAY_RELEASE_STATUS : raw
|
|
end
|
|
|
|
def play_validate_only?
|
|
ENV["GOOGLE_PLAY_VALIDATE_ONLY"] == "1"
|
|
end
|
|
|
|
def play_metadata_upload_requested?
|
|
ENV["SUPPLY_UPLOAD_METADATA"] == "1"
|
|
end
|
|
|
|
def play_screenshot_upload_requested?
|
|
ENV["SUPPLY_UPLOAD_SCREENSHOTS"] == "1"
|
|
end
|
|
|
|
def play_image_upload_requested?
|
|
ENV["SUPPLY_UPLOAD_IMAGES"] == "1"
|
|
end
|
|
|
|
def play_auth_options
|
|
json_key = ENV["GOOGLE_PLAY_JSON_KEY"].to_s.strip
|
|
json_key = ENV["SUPPLY_JSON_KEY"].to_s.strip if json_key.empty?
|
|
json_key = ENV["GOOGLE_PLAY_JSON_KEY_PATH"].to_s.strip if json_key.empty?
|
|
return { json_key: json_key } unless json_key.empty?
|
|
|
|
json_key_data = ENV["GOOGLE_PLAY_JSON_KEY_DATA"].to_s.strip
|
|
json_key_data = ENV["SUPPLY_JSON_KEY_DATA"].to_s.strip if json_key_data.empty?
|
|
return { json_key_data: json_key_data } unless json_key_data.empty?
|
|
|
|
UI.user_error!("Missing Google Play API credentials. Set GOOGLE_PLAY_JSON_KEY or GOOGLE_PLAY_JSON_KEY_DATA.")
|
|
end
|
|
|
|
def validate_play_auth!
|
|
client = nil
|
|
begin
|
|
client = Supply::Client.make_from_config(params: play_auth_options)
|
|
client.begin_edit(package_name: play_package_name)
|
|
rescue => e
|
|
UI.user_error!("Google Play API credentials are invalid for #{play_package_name}: #{e.message}")
|
|
ensure
|
|
if client&.current_edit
|
|
begin
|
|
client.abort_current_edit
|
|
rescue => e
|
|
UI.user_error!("Google Play API credentials opened a validation edit but could not close it: #{e.message}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def read_android_version_metadata
|
|
stdout, stderr, status = Open3.capture3(
|
|
"node",
|
|
"--import",
|
|
"tsx",
|
|
File.join(repo_root, "scripts", "android-version.ts"),
|
|
"--json",
|
|
"--root",
|
|
repo_root
|
|
)
|
|
unless status.success?
|
|
detail = stderr.to_s.strip
|
|
detail = stdout.to_s.strip if detail.empty?
|
|
UI.user_error!("Failed to read Android version metadata: #{detail}")
|
|
end
|
|
|
|
parsed = JSON.parse(stdout)
|
|
version = parsed.fetch("canonicalVersion").to_s
|
|
version_code = parsed.fetch("versionCode").to_i
|
|
UI.user_error!("Android version helper returned incomplete metadata.") if version.empty? || version_code <= 0
|
|
|
|
{ version: version, version_code: version_code }
|
|
rescue JSON::ParserError => e
|
|
UI.user_error!("Invalid JSON from Android version helper: #{e.message}")
|
|
end
|
|
|
|
def sync_android_versioning!
|
|
sh(shell_join(["node", "--import", "tsx", File.join(repo_root, "scripts", "android-sync-versioning.ts"), "--check", "--root", repo_root]))
|
|
end
|
|
|
|
def android_release_notes_path
|
|
File.join(ANDROID_FASTLANE_ROOT, "metadata", "android", "en-US", "release_notes.txt")
|
|
end
|
|
|
|
def validate_android_release_notes!
|
|
release_notes_path = android_release_notes_path
|
|
UI.user_error!("Missing Android release notes at #{release_notes_path}. Run `pnpm android:version:sync`.") unless File.exist?(release_notes_path)
|
|
UI.user_error!("Android release notes at #{release_notes_path} are empty.") unless env_present?(File.read(release_notes_path))
|
|
end
|
|
|
|
def android_changelog_path(version_code)
|
|
File.join(ANDROID_FASTLANE_ROOT, "metadata", "android", "en-US", "changelogs", "#{version_code}.txt")
|
|
end
|
|
|
|
def wear_version_code(phone_version_code)
|
|
build_number = phone_version_code % 100
|
|
UI.user_error!("Android phone versionCode build number must be 01 through 49.") unless (1..49).cover?(build_number)
|
|
phone_version_code + 50
|
|
end
|
|
|
|
def sync_android_changelog!(version_code)
|
|
validate_android_release_notes!
|
|
|
|
changelog_path = android_changelog_path(version_code)
|
|
FileUtils.mkdir_p(File.dirname(changelog_path))
|
|
File.write(changelog_path, File.read(android_release_notes_path))
|
|
wear_changelog_path = android_changelog_path(wear_version_code(version_code))
|
|
File.write(wear_changelog_path, File.read(android_release_notes_path))
|
|
[changelog_path, wear_changelog_path]
|
|
end
|
|
|
|
def play_metadata_languages
|
|
Dir.children(play_metadata_path)
|
|
.select { |language| File.directory?(File.join(play_metadata_path, language)) }
|
|
.reject { |language| language.start_with?(".") }
|
|
.sort
|
|
end
|
|
|
|
def play_release_notes(version_code)
|
|
play_metadata_languages.filter_map do |language|
|
|
changelog_path = File.join(play_metadata_path, language, "changelogs", "#{version_code}.txt")
|
|
fallback_path = File.join(play_metadata_path, language, "changelogs", "default.txt")
|
|
source_path = File.exist?(changelog_path) ? changelog_path : fallback_path
|
|
next unless File.exist?(source_path)
|
|
|
|
AndroidPublisher::LocalizedText.new(language: language, text: File.read(source_path, encoding: "UTF-8"))
|
|
end
|
|
end
|
|
|
|
def update_play_track!(client, track_name:, version_code:, version_name:)
|
|
release = AndroidPublisher::TrackRelease.new(
|
|
name: version_name,
|
|
status: play_release_status,
|
|
version_codes: [version_code],
|
|
release_notes: play_release_notes(version_code)
|
|
)
|
|
track = client.tracks(track_name).first || AndroidPublisher::Track.new(track: track_name)
|
|
# Google preserves older releases when the edit replaces this list with the newest release.
|
|
track.releases = [release]
|
|
# Supply::Client owns the active edit; its public API takes (track_name, track_object).
|
|
client.update_track(track_name, track)
|
|
end
|
|
|
|
def upload_play_listing_assets!(client, upload_metadata:, upload_images:, upload_screenshots:)
|
|
play_metadata_languages.each do |language|
|
|
language_path = File.join(play_metadata_path, language)
|
|
if upload_metadata
|
|
metadata_fields = Supply::AVAILABLE_METADATA_FIELDS.select { |field| File.exist?(File.join(language_path, "#{field}.txt")) }
|
|
unless metadata_fields.empty?
|
|
# This returns Supply::Listing, whose save writes through the same active edit.
|
|
listing = client.listing_for_language(language)
|
|
metadata_fields.each do |field|
|
|
listing.public_send("#{field}=", File.read(File.join(language_path, "#{field}.txt"), encoding: "UTF-8"))
|
|
end
|
|
listing.save
|
|
end
|
|
end
|
|
|
|
if upload_images
|
|
Supply::IMAGES_TYPES.each do |image_type|
|
|
path = Dir.glob(File.join(language_path, "images", "#{image_type}.{png,jpg,jpeg}"), File::FNM_CASEFOLD).sort.last
|
|
client.upload_image(image_path: path, image_type: image_type, language: language) if path
|
|
end
|
|
end
|
|
|
|
next unless upload_screenshots
|
|
|
|
Supply::SCREENSHOT_TYPES.each do |screenshot_type|
|
|
paths = Dir.glob(File.join(language_path, "images", screenshot_type, "*.{png,jpg,jpeg}"), File::FNM_CASEFOLD).sort
|
|
next if paths.empty?
|
|
|
|
client.clear_screenshots(image_type: screenshot_type, language: language)
|
|
paths.each { |path| client.upload_image(image_path: path, image_type: screenshot_type, language: language) }
|
|
end
|
|
end
|
|
end
|
|
|
|
def fastlane_boolean_env(name, default:)
|
|
value = ENV[name]
|
|
return default if value.nil?
|
|
|
|
normalized = value.downcase
|
|
return true if ["1", "yes", "true", "on"].include?(normalized)
|
|
return false if ["0", "no", "false", "off"].include?(normalized)
|
|
|
|
UI.user_error!("#{name} must be true/false, yes/no, on/off, or 1/0.")
|
|
end
|
|
|
|
def upload_play_builds_atomically!(phone_artifact_path:, wear_artifact_path:, version_metadata:, upload_metadata:, upload_images:, upload_screenshots:)
|
|
previous_supply_config = Supply.config
|
|
client = nil
|
|
begin
|
|
Supply.config = {
|
|
ack_bundle_installation_warning: fastlane_boolean_env("ACK_BUNDLE_INSTALLATION_WARNING", default: false),
|
|
changes_not_sent_for_review: fastlane_boolean_env("SUPPLY_CHANGES_NOT_SENT_FOR_REVIEW", default: false),
|
|
rescue_changes_not_sent_for_review: fastlane_boolean_env("SUPPLY_RESCUE_CHANGES_NOT_SENT_FOR_REVIEW", default: true)
|
|
}
|
|
client = Supply::Client.make_from_config(params: play_auth_options.merge(timeout: (ENV["SUPPLY_TIMEOUT"] || "300").to_i))
|
|
client.begin_edit(package_name: play_package_name)
|
|
|
|
phone_version_code = client.upload_bundle(phone_artifact_path)
|
|
wear_version_code_value = client.upload_bundle(wear_artifact_path)
|
|
expected_phone_version_code = version_metadata.fetch(:version_code)
|
|
expected_wear_version_code = wear_version_code(expected_phone_version_code)
|
|
UI.user_error!("Uploaded phone AAB versionCode #{phone_version_code}, expected #{expected_phone_version_code}.") unless phone_version_code.to_i == expected_phone_version_code
|
|
UI.user_error!("Uploaded Wear AAB versionCode #{wear_version_code_value}, expected #{expected_wear_version_code}.") unless wear_version_code_value.to_i == expected_wear_version_code
|
|
|
|
update_play_track!(
|
|
client,
|
|
track_name: play_track,
|
|
version_code: phone_version_code,
|
|
version_name: version_metadata.fetch(:version)
|
|
)
|
|
update_play_track!(
|
|
client,
|
|
track_name: wear_play_track,
|
|
version_code: wear_version_code_value,
|
|
version_name: version_metadata.fetch(:version)
|
|
)
|
|
upload_play_listing_assets!(
|
|
client,
|
|
upload_metadata: upload_metadata,
|
|
upload_images: upload_images,
|
|
upload_screenshots: upload_screenshots
|
|
)
|
|
|
|
if play_validate_only?
|
|
client.validate_current_edit!
|
|
UI.success("Successfully validated the atomic phone and Wear upload.")
|
|
else
|
|
client.commit_current_edit!
|
|
UI.success("Successfully committed the atomic phone and Wear upload.")
|
|
end
|
|
ensure
|
|
if client&.current_edit
|
|
begin
|
|
client.abort_current_edit
|
|
rescue => error
|
|
UI.important("Could not abort Google Play edit after failure: #{error.message}")
|
|
end
|
|
end
|
|
Supply.config = previous_supply_config
|
|
end
|
|
end
|
|
|
|
def play_metadata_path
|
|
File.join(ANDROID_FASTLANE_ROOT, "metadata", "android")
|
|
end
|
|
|
|
def play_screenshot_paths
|
|
Dir[File.join(play_metadata_path, "**", "images", "**", "*.{png,jpg,jpeg}")]
|
|
end
|
|
|
|
def validate_android_screenshots!
|
|
return unless play_screenshot_upload_requested?
|
|
|
|
if play_screenshot_paths.empty?
|
|
UI.user_error!("SUPPLY_UPLOAD_SCREENSHOTS=1 but no screenshots were found under apps/android/fastlane/metadata/android/*/images.")
|
|
end
|
|
end
|
|
|
|
def release_artifact_path(version)
|
|
File.join(android_root, "build", "release-artifacts", "openclaw-#{version}-play-release.aab")
|
|
end
|
|
|
|
def wear_release_artifact_path(version)
|
|
File.join(android_root, "build", "release-artifacts", "openclaw-#{version}-wear-release.aab")
|
|
end
|
|
|
|
def play_release_artifact_paths(version)
|
|
[release_artifact_path(version), wear_release_artifact_path(version)]
|
|
end
|
|
|
|
def build_release_artifacts!
|
|
sh(shell_join(["bun", File.join(android_root, "scripts", "build-release-artifacts.ts")]))
|
|
end
|
|
|
|
def capture_android_screenshots!
|
|
sh(shell_join(["bash", File.join(repo_root, "scripts", "android-screenshots.sh")]))
|
|
end
|
|
|
|
def mobile_release_ref_script
|
|
File.join(repo_root, "scripts", "mobile-release-ref.ts")
|
|
end
|
|
|
|
def release_git_sha
|
|
stdout, stderr, status = Open3.capture3("git", "rev-parse", "HEAD", chdir: repo_root)
|
|
UI.user_error!("Unable to resolve release Git SHA: #{stderr.strip}") unless status.success?
|
|
stdout.strip
|
|
end
|
|
|
|
def mobile_release_ref_command(command, platform:, version:, build: nil, version_code: nil, sha: nil)
|
|
args = [
|
|
"node",
|
|
"--import",
|
|
"tsx",
|
|
mobile_release_ref_script,
|
|
command,
|
|
"--platform",
|
|
platform,
|
|
"--version",
|
|
version,
|
|
"--root",
|
|
repo_root,
|
|
]
|
|
args.push("--build", build.to_s) if build
|
|
args.push("--version-code", version_code.to_s) if version_code
|
|
args.push("--sha", sha.to_s) if sha
|
|
sh(shell_join(args))
|
|
end
|
|
|
|
def ensure_mobile_release_ref_available!(platform:, version:, build: nil, version_code: nil, sha: nil)
|
|
mobile_release_ref_command(
|
|
"preflight",
|
|
platform: platform,
|
|
version: version,
|
|
build: build,
|
|
version_code: version_code,
|
|
sha: sha
|
|
)
|
|
end
|
|
|
|
def record_mobile_release_ref!(platform:, version:, build: nil, version_code: nil, sha: nil)
|
|
mobile_release_ref_command(
|
|
"record",
|
|
platform: platform,
|
|
version: version,
|
|
build: build,
|
|
version_code: version_code,
|
|
sha: sha
|
|
)
|
|
end
|
|
|
|
def read_android_release_signing_properties!(path)
|
|
UI.user_error!("Missing materialized Android release signing properties at #{path}.") unless File.exist?(path)
|
|
|
|
properties = {}
|
|
File.foreach(path) do |line|
|
|
stripped = line.strip
|
|
next if stripped.empty? || stripped.start_with?("#")
|
|
|
|
key, value = stripped.split("=", 2)
|
|
next if key.nil? || key.empty? || value.nil?
|
|
|
|
properties[key] = value.strip
|
|
end
|
|
|
|
missing = ANDROID_RELEASE_SIGNING_GRADLE_PROPERTIES.reject { |key| env_present?(properties[key]) }
|
|
UI.user_error!("Materialized Android release signing properties are missing: #{missing.join(', ')}.") unless missing.empty?
|
|
|
|
properties
|
|
end
|
|
|
|
def export_android_release_signing_properties!(path)
|
|
read_android_release_signing_properties!(path).each do |key, value|
|
|
ENV["ORG_GRADLE_PROJECT_#{key}"] = value
|
|
end
|
|
end
|
|
|
|
def sync_android_release_signing!
|
|
sh(shell_join(["node", android_release_signing_script, "--mode", "sync-pull"]))
|
|
export_android_release_signing_properties!(android_release_signing_materialized_properties_path)
|
|
end
|
|
|
|
def prepare_android_release_signing!
|
|
if env_present?(ENV["MATCH_PASSWORD"])
|
|
sync_android_release_signing!
|
|
elsif File.exist?(android_release_signing_materialized_properties_path)
|
|
export_android_release_signing_properties!(android_release_signing_materialized_properties_path)
|
|
end
|
|
end
|
|
|
|
def validate_android_release_signing!
|
|
Dir.chdir(android_root) do
|
|
sh(shell_join(["./gradlew", ":app:bundlePlayRelease", ":wear:bundleRelease", "--dry-run"]))
|
|
end
|
|
end
|
|
|
|
def print_android_release_plan!(version_metadata)
|
|
UI.message("Android Play release plan:")
|
|
UI.message(" package: #{play_package_name}")
|
|
UI.message(" track: #{play_track}")
|
|
UI.message(" Wear track: #{wear_play_track}")
|
|
UI.message(" release_status: #{play_release_status}")
|
|
UI.message(" validate_only: #{play_validate_only?}")
|
|
UI.message(" versionName: #{version_metadata.fetch(:version)}")
|
|
UI.message(" phone versionCode: #{version_metadata.fetch(:version_code)}")
|
|
UI.message(" Wear versionCode: #{wear_version_code(version_metadata.fetch(:version_code))}")
|
|
end
|
|
|
|
def validate_android_release_preflight!(version_metadata)
|
|
validate_play_auth!
|
|
prepare_android_release_signing!
|
|
validate_android_release_signing!
|
|
validate_android_release_notes!
|
|
print_android_release_plan!(version_metadata)
|
|
end
|
|
|
|
def upload_play_store_metadata!(version_metadata)
|
|
validate_android_screenshots!
|
|
sync_android_changelog!(version_metadata.fetch(:version_code))
|
|
|
|
upload_to_play_store(
|
|
**play_auth_options,
|
|
package_name: play_package_name,
|
|
track: play_track,
|
|
version_code: version_metadata.fetch(:version_code),
|
|
metadata_path: play_metadata_path,
|
|
skip_upload_apk: true,
|
|
skip_upload_aab: true,
|
|
skip_upload_metadata: !play_metadata_upload_requested?,
|
|
skip_upload_changelogs: false,
|
|
skip_upload_images: !play_image_upload_requested?,
|
|
skip_upload_screenshots: !play_screenshot_upload_requested?,
|
|
validate_only: play_validate_only?
|
|
)
|
|
end
|
|
|
|
def upload_play_store_build!(version_metadata, upload_metadata: false, upload_images: false, upload_screenshots: false)
|
|
release_sha = release_git_sha
|
|
ensure_mobile_release_ref_available!(
|
|
platform: "android",
|
|
version: version_metadata.fetch(:version),
|
|
version_code: version_metadata.fetch(:version_code),
|
|
sha: release_sha
|
|
)
|
|
ENV["SUPPLY_UPLOAD_SCREENSHOTS"] = "1" if upload_screenshots
|
|
validate_android_screenshots!
|
|
sync_android_changelog!(version_metadata.fetch(:version_code))
|
|
artifact_paths = play_release_artifact_paths(version_metadata.fetch(:version))
|
|
missing_artifacts = artifact_paths.reject { |path| File.exist?(path) }
|
|
unless missing_artifacts.empty?
|
|
UI.user_error!("Missing Play release artifacts at #{missing_artifacts.join(', ')}. Run pnpm android:release:archive first.")
|
|
end
|
|
|
|
phone_artifact_path, wear_artifact_path = artifact_paths
|
|
upload_play_builds_atomically!(
|
|
phone_artifact_path: phone_artifact_path,
|
|
wear_artifact_path: wear_artifact_path,
|
|
version_metadata: version_metadata,
|
|
upload_metadata: upload_metadata,
|
|
upload_images: upload_images,
|
|
upload_screenshots: upload_screenshots
|
|
)
|
|
|
|
unless play_validate_only?
|
|
record_mobile_release_ref!(
|
|
platform: "android",
|
|
version: version_metadata.fetch(:version),
|
|
version_code: version_metadata.fetch(:version_code),
|
|
sha: release_sha
|
|
)
|
|
end
|
|
end
|
|
|
|
load_env_file(File.join(ANDROID_FASTLANE_ROOT, ".env"))
|
|
|
|
platform :android do
|
|
desc "Validate Google Play API credentials"
|
|
lane :auth_check do
|
|
validate_play_auth!
|
|
UI.success("Google Play API credentials are valid.")
|
|
end
|
|
|
|
desc "Print the Android release signing plan"
|
|
lane :signing_plan do
|
|
sh(shell_join(["node", android_release_signing_script, "--mode", "plan"]))
|
|
end
|
|
|
|
desc "Pull encrypted Android release signing assets and validate Gradle release signing"
|
|
lane :signing_check do
|
|
sync_android_release_signing!
|
|
validate_android_release_signing!
|
|
UI.success("Android release signing assets are available locally.")
|
|
end
|
|
|
|
desc "Pull encrypted Android release signing assets from the shared signing repo"
|
|
lane :signing_sync_pull do
|
|
sync_android_release_signing!
|
|
UI.success("Pulled Android release signing assets.")
|
|
end
|
|
|
|
desc "Create or refresh encrypted Android release signing assets in the shared signing repo"
|
|
lane :signing_sync_push do
|
|
sh(shell_join(["node", android_release_signing_script, "--mode", "sync-push"]))
|
|
UI.success("Pushed Android release signing assets.")
|
|
end
|
|
|
|
desc "Validate Android Play release auth, signing, versioning, and release notes"
|
|
lane :release_preflight do
|
|
sync_android_versioning!
|
|
version_metadata = read_android_version_metadata
|
|
validate_android_release_preflight!(version_metadata)
|
|
UI.success("Android Play release preflight passed for #{version_metadata[:version]} (#{version_metadata[:version_code]}).")
|
|
end
|
|
|
|
desc "Upload Google Play metadata, changelog, and optional screenshots"
|
|
lane :metadata do
|
|
sync_android_versioning!
|
|
version_metadata = read_android_version_metadata
|
|
ENV["SUPPLY_UPLOAD_METADATA"] = "1" unless ENV.key?("SUPPLY_UPLOAD_METADATA")
|
|
upload_play_store_metadata!(version_metadata)
|
|
UI.success("Uploaded Android Play metadata for #{version_metadata[:version]} (#{version_metadata[:version_code]}).")
|
|
end
|
|
|
|
desc "Build signed Android release artifacts locally without uploading"
|
|
lane :play_store_archive do
|
|
sync_android_versioning!
|
|
prepare_android_release_signing!
|
|
build_release_artifacts!
|
|
end
|
|
|
|
desc "Generate deterministic Android screenshots for Google Play metadata"
|
|
lane :screenshots do
|
|
capture_android_screenshots!
|
|
end
|
|
|
|
desc "Upload the signed Play AAB to Google Play"
|
|
lane :play_store do
|
|
sync_android_versioning!
|
|
version_metadata = read_android_version_metadata
|
|
upload_play_store_build!(version_metadata)
|
|
UI.success("Uploaded Android Play build to #{play_track}: version=#{version_metadata[:version]} code=#{version_metadata[:version_code]}")
|
|
end
|
|
|
|
desc "Upload Android metadata, archive release artifacts, then upload the Play AAB"
|
|
lane :release_upload do
|
|
sync_android_versioning!
|
|
version_metadata = read_android_version_metadata
|
|
validate_android_release_preflight!(version_metadata)
|
|
screenshots
|
|
ENV["SUPPLY_UPLOAD_METADATA"] = "1"
|
|
ENV["SUPPLY_UPLOAD_SCREENSHOTS"] = "1"
|
|
build_release_artifacts!
|
|
upload_play_store_build!(version_metadata, upload_metadata: true, upload_screenshots: true)
|
|
UI.success("Uploaded Android Play build to #{play_track}: version=#{version_metadata[:version]} code=#{version_metadata[:version_code]}")
|
|
UI.important("Production promotion remains manual in Google Play Console.")
|
|
end
|
|
end
|