From 5a246d1ced645eb6d135e6c5ef9761ccc6060e03 Mon Sep 17 00:00:00 2001 From: JeffG <202880+erskingardner@users.noreply.github.com> Date: Thu, 7 May 2026 15:52:04 +0200 Subject: [PATCH] fix: create android release checksums (#634) --- RELEASE.md | 1 + fastlane/Fastfile | 25 ++++++- .../scripts/release_automation_config_test.sh | 70 +++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 673132b..2ae3724 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,7 @@ The first release automation layer is build-only: - builds staging iOS IPA for `dev.ipf.whitenoise.staging` - builds production iOS IPA for `org.parres.whitenoise` - stages artifacts under `build/releases/v+//` +- creates `.sha256` sidecar files for staged Android APK and AAB artifacts Staging and production are separate apps on both Android and iOS. Current iOS publishing target for both apps is App Store Connect/TestFlight. Store release diff --git a/fastlane/Fastfile b/fastlane/Fastfile index de072b3..dd0b839 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,4 +1,5 @@ require 'fileutils' +require 'digest' require 'open3' require 'shellwords' @@ -69,13 +70,21 @@ def release_output_dir(info, flavor, *parts) File.join(ROOT, 'build', 'releases', "v#{info['full_version']}", flavor.to_s, *parts) end -def stage_artifact(source, destination_dir, destination_name) +def stage_artifact(source, destination_dir, destination_name, checksum: false) UI.user_error!("Missing artifact: #{source}") unless File.file?(source) FileUtils.mkdir_p(destination_dir) destination = File.join(destination_dir, destination_name) FileUtils.cp(source, destination) UI.message("Staged #{destination}") + + if checksum + digest = Digest::SHA256.file(destination).hexdigest + checksum_path = "#{destination}.sha256" + File.write(checksum_path, "#{digest} #{destination_name}\n") + UI.message("Staged #{checksum_path}") + end + destination end @@ -112,13 +121,23 @@ def build_android_flavor(flavor, options, build_native: true) UI.user_error!("Required APK was not found: #{source}") end - stage_artifact(source, output_dir, "#{config[:artifact_prefix]}-#{info['version_name']}-#{abi}.apk") + stage_artifact( + source, + output_dir, + "#{config[:artifact_prefix]}-#{info['version_name']}-#{abi}.apk", + checksum: true, + ) end source = android_aab(flavor) UI.user_error!("#{flavor.capitalize} AAB was not found") unless source - stage_artifact(source, output_dir, "#{config[:artifact_prefix]}-#{info['version_name']}.aab") + stage_artifact( + source, + output_dir, + "#{config[:artifact_prefix]}-#{info['version_name']}.aab", + checksum: true, + ) end def build_ios_flavor(flavor, options, build_native: true) diff --git a/test/scripts/release_automation_config_test.sh b/test/scripts/release_automation_config_test.sh index 7533bde..78ad723 100755 --- a/test/scripts/release_automation_config_test.sh +++ b/test/scripts/release_automation_config_test.sh @@ -61,4 +61,74 @@ raise 'Zap Store match must include production arm64 APK' unless regex.match?(pr raise 'Zap Store match must exclude staging APK' if regex.match?(staging_apk) RUBY +ruby <<'RUBY' +require 'digest' +require 'fileutils' +require 'tmpdir' + +def opt_out_usage; end +def desc(_text); end +def lane(_name); end + +module UI + def self.user_error!(message) + raise message + end + + def self.important(_message); end + def self.message(_message); end +end + +load 'fastlane/Fastfile' + +def release_info(_options = {}) + { + 'full_version' => '2026.5.7+24', + 'version_name' => '2026.5.7', + 'build_number' => '24', + } +end + +def run_from_root(_parts); end + +def assert_sidecar_matches(artifact_path) + checksum_path = "#{artifact_path}.sha256" + raise "missing checksum sidecar for #{artifact_path}" unless File.file?(checksum_path) + + expected_digest = Digest::SHA256.file(artifact_path).hexdigest + expected_contents = "#{expected_digest} #{File.basename(artifact_path)}\n" + actual_contents = File.read(checksum_path) + + raise "unexpected checksum contents: #{actual_contents.inspect}" unless actual_contents == expected_contents +end + +Dir.mktmpdir do |dir| + Object.send(:remove_const, :ROOT) + Object.const_set(:ROOT, dir) + + apk_dir = File.join(ROOT, 'build', 'app', 'outputs', 'flutter-apk') + aab_dir = File.join(ROOT, 'build', 'app', 'outputs', 'bundle', 'productionRelease') + FileUtils.mkdir_p(apk_dir) + FileUtils.mkdir_p(aab_dir) + + File.write(File.join(apk_dir, 'app-arm64-v8a-production-release.apk'), 'arm64 apk bytes') + File.write(File.join(apk_dir, 'app-x86_64-production-release.apk'), 'x86 apk bytes') + File.write(File.join(aab_dir, 'app-production-release.aab'), 'aab bytes') + + build_android_flavor('production', {}, build_native: true) + + output_dir = File.join(ROOT, 'build', 'releases', 'v2026.5.7+24', 'production', 'android') + [ + 'whitenoise-2026.5.7-arm64-v8a.apk', + 'whitenoise-2026.5.7-x86_64.apk', + 'whitenoise-2026.5.7.aab', + ].each do |artifact_name| + artifact_path = File.join(output_dir, artifact_name) + raise "missing staged artifact #{artifact_path}" unless File.file?(artifact_path) + + assert_sidecar_matches(artifact_path) + end +end +RUBY + echo "release_automation_config_test passed"