diff --git a/.gitignore b/.gitignore index 72458b8..dab163e 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ windows/ # Subagent persistent memory (local only) .claude/agent-memory/ + +# Release artifacts stashed by fastlane +/dist/ diff --git a/RELEASING.md b/RELEASING.md index 516a17d..a8c7053 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -54,6 +54,11 @@ backstop, not as an invitation. Use `fastlane build` on either platform to produce the artifact without uploading anything. +Every lane starts with `flutter clean`, which wipes `build/` for *both* +platforms. So each `build` lane copies its finished artifact into +`dist//` (gitignored) and the upload lanes read from there -- +otherwise building Android and then iOS would leave you holding only the IPA. + Both upload lanes deliberately stop short of shipping: Play uploads land as a **draft** release and `ios release` passes `submit_for_review: false`. Store listing copy, screenshots, "What's New", and the actual submit stay manual diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index ba64a7e..73eb7ff 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -38,15 +38,15 @@ platform :android do UI.user_error!("AAB not found at #{AAB_PATH}") unless File.exist?(AAB_PATH) UI.success("Built #{AAB_PATH}") - AAB_PATH + stash_artifact(PROJECT_ROOT, AAB_PATH, pubspec_version(PROJECT_ROOT)) end desc "Build and upload to the Internal testing track" lane :internal do - build + aab = build upload_to_play_store( track: "internal", - aab: AAB_PATH, + aab: aab, release_status: "draft", skip_upload_apk: true, skip_upload_metadata: true, @@ -57,10 +57,10 @@ platform :android do desc "Build and upload to Production as a draft release" lane :production do - build + aab = build upload_to_play_store( track: "production", - aab: AAB_PATH, + aab: aab, # Draft so the rollout is still a deliberate click in the Play Console. release_status: "draft", skip_upload_apk: true, diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 1f72098..415b255 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -51,7 +51,7 @@ platform :ios do ipa = Dir[File.join(IPA_DIR, "*.ipa")].max_by { |f| File.mtime(f) } UI.user_error!("No IPA found in #{IPA_DIR}") if ipa.nil? UI.success("Built #{ipa}") - ipa + stash_artifact(PROJECT_ROOT, ipa, pubspec_version(PROJECT_ROOT)) end desc "Build and upload to TestFlight" diff --git a/tool/fastlane_flutter.rb b/tool/fastlane_flutter.rb index 9031ad6..a226d63 100644 --- a/tool/fastlane_flutter.rb +++ b/tool/fastlane_flutter.rb @@ -10,12 +10,37 @@ # Stripping the Ruby/Bundler variables for the child process makes `flutter` # behave exactly as it does in a plain shell. +require "fileutils" + # Passing nil as a value to Kernel#system removes the variable from the child # environment rather than setting it to an empty string. CLEAN_RUBY_ENV = ENV.keys.select { |k| k.start_with?("GEM_", "RUBY", "BUNDLE_", "BUNDLER_") }.to_h { |k| [k, nil] }.freeze +# Copies a freshly built artifact into dist/, returning the new path. +# +# Every lane starts with `flutter clean`, which wipes build/ for *both* +# platforms -- so building Android and then iOS would otherwise leave only the +# IPA behind. dist/ sits outside build/, so artifacts accumulate there across +# platforms and survive the next lane's clean. +def stash_artifact(project_root, artifact, version) + dist = File.join(project_root, "dist", version) + FileUtils.mkdir_p(dist) + dest = File.join(dist, File.basename(artifact)) + FileUtils.cp(artifact, dest) + FastlaneCore::UI.success("Stashed #{File.basename(dest)} in dist/#{version}/") + dest +end + +# Reads `version: +` out of pubspec.yaml, e.g. "1.1.0+7". +def pubspec_version(project_root) + pubspec = File.read(File.join(project_root, "pubspec.yaml")) + match = pubspec[/^version:\s*(\S+)/, 1] + FastlaneCore::UI.user_error!("No version: line in pubspec.yaml") if match.nil? + match +end + # Runs `flutter ` from the Flutter project root with a clean Ruby # environment, raising a fastlane error if it exits non-zero. def flutter(project_root, *args)