From 9e2441a104496420646f9ed3401c0595e84b77d5 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 16:44:42 -0700 Subject: [PATCH 1/7] feat: improve deployment tooling --- app/fastlane/DEV.md | 8 +++- app/fastlane/Fastfile | 11 +++-- app/fastlane/helpers/slack.rb | 1 + app/fastlane/test/helpers_test.rb | 14 +++++++ app/scripts/cleanup-ios-build.sh | 24 +++++++++++ app/scripts/tests/cleanup-ios-build.test.cjs | 43 ++++++++++++++++++++ 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100755 app/scripts/cleanup-ios-build.sh create mode 100644 app/scripts/tests/cleanup-ios-build.test.cjs diff --git a/app/fastlane/DEV.md b/app/fastlane/DEV.md index 996e9bab0..002411a0c 100644 --- a/app/fastlane/DEV.md +++ b/app/fastlane/DEV.md @@ -116,6 +116,12 @@ yarn mobile-local-deploy:android # Deploy Android to Google Play Internal Testi **Why internal testing?** This provides the same safety as GitHub runner deployments while allowing you to use your local machine for building. +After running a local iOS deploy, reset the Xcode project to avoid committing build artifacts: + +```bash +./scripts/cleanup-ios-build.sh +``` + ### Direct Fastlane Commands (Not Recommended) ⚠️ **Use the confirmation script above instead of these direct commands.** @@ -218,7 +224,7 @@ Fastlane requires various secrets to interact with the app stores and sign appli | `IOS_P12_PASSWORD` | Password for the p12 certificate file | | `IOS_TEAM_ID` | Apple Developer Team ID | | `IOS_TEAM_NAME` | Apple Developer Team name | -| `IOS_TESTFLIGHT_GROUPS` | Comma-separated list of TestFlight groups to distribute the app to | +| `IOS_TESTFLIGHT_GROUPS` | Comma-separated list of **external** TestFlight groups to distribute the app to | #### Slack Integration Secrets 📱 diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index f8dfd6742..71f86c14c 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -22,6 +22,11 @@ android_has_permissions = false # Project configuration PROJECT_NAME = ENV["IOS_PROJECT_NAME"] +APP_NAME = ENV["IOS_PROJECT_NAME"] || begin + JSON.parse(File.read("../app.json"))["displayName"] +rescue + nil +end || "MobileApp" PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"] SIGNING_CERTIFICATE = ENV["IOS_SIGNING_CERTIFICATE"] @@ -54,7 +59,7 @@ platform :ios do upload_to_testflight( api_key: result[:api_key], distribute_external: true, - # TODO: fix error about the groups not being set correctly, fwiw groups are set in the app store connect + # Only external TestFlight groups are valid here groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), changelog: "", skip_waiting_for_build_processing: false, @@ -66,7 +71,7 @@ platform :ios do file_path: result[:ipa_path], channel_id: ENV["SLACK_CHANNEL_ID"], initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight", - title: "#{PROJECT_NAME}-#{package_version}-#{result[:build_number]}.ipa", + title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa", ) else UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.") @@ -266,7 +271,7 @@ platform :android do file_path: android_aab_path, channel_id: ENV["SLACK_CHANNEL_ID"], initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform}", - title: "#{PROJECT_NAME}-#{package_version}-#{version_code}.aab", + title: "#{APP_NAME}-#{package_version}-#{version_code}.aab", ) else UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.") diff --git a/app/fastlane/helpers/slack.rb b/app/fastlane/helpers/slack.rb index 56e202556..f396295f0 100644 --- a/app/fastlane/helpers/slack.rb +++ b/app/fastlane/helpers/slack.rb @@ -5,6 +5,7 @@ module Slack def upload_file_to_slack(file_path:, channel_id:, initial_comment: nil, thread_ts: nil, title: nil) slack_token = ENV["SLACK_API_TOKEN"] report_error("Missing SLACK_API_TOKEN environment variable.", nil, "Slack Upload Failed") if slack_token.to_s.strip.empty? + report_error("Missing SLACK_CHANNEL_ID environment variable.", nil, "Slack Upload Failed") if channel_id.to_s.strip.empty? report_error("File not found at path: #{file_path}", nil, "Slack Upload Failed") unless File.exist?(file_path) file_name = File.basename(file_path) diff --git a/app/fastlane/test/helpers_test.rb b/app/fastlane/test/helpers_test.rb index 0310803a2..b0cfb7bf2 100644 --- a/app/fastlane/test/helpers_test.rb +++ b/app/fastlane/test/helpers_test.rb @@ -328,6 +328,20 @@ def test_verify_env_vars_some_missing assert_equal ["MISSING_VAR1", "EMPTY_VAR", "WHITESPACE_VAR"], missing end + def test_upload_file_to_slack_missing_channel + ENV["SLACK_API_TOKEN"] = "token" + file = Tempfile.new(["artifact", ".txt"]) + file.write("data") + file.close + + assert_raises(FastlaneCore::Interface::FastlaneCommonException) do + Fastlane::Helpers.upload_file_to_slack(file_path: file.path, channel_id: "") + end + ensure + file.unlink + ENV.delete("SLACK_API_TOKEN") + end + private def clear_test_env_vars diff --git a/app/scripts/cleanup-ios-build.sh b/app/scripts/cleanup-ios-build.sh new file mode 100755 index 000000000..c4f2be01a --- /dev/null +++ b/app/scripts/cleanup-ios-build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Reset Xcode project after local fastlane builds +set -euo pipefail + +PROJECT_NAME="${IOS_PROJECT_NAME:-Self}" +PBXPROJ="ios/${PROJECT_NAME}.xcodeproj/project.pbxproj" + +if [ ! -f "$PBXPROJ" ]; then + echo "Project file not found: $PBXPROJ" >&2 + exit 1 +fi + +MARKETING_VERSION=$(grep -m1 "MARKETING_VERSION =" "$PBXPROJ" | awk '{print $3}' | tr -d ';') +CURRENT_VERSION=$(grep -m1 "CURRENT_PROJECT_VERSION =" "$PBXPROJ" | awk '{print $3}' | tr -d ';') + +git checkout -- "$PBXPROJ" + +if sed --version >/dev/null 2>&1; then + sed -i -e "s/\(MARKETING_VERSION = \).*/\1$MARKETING_VERSION;/" -e "s/\(CURRENT_PROJECT_VERSION = \).*/\1$CURRENT_VERSION;/" "$PBXPROJ" +else + sed -i '' -e "s/\(MARKETING_VERSION = \).*/\1$MARKETING_VERSION;/" -e "s/\(CURRENT_PROJECT_VERSION = \).*/\1$CURRENT_VERSION;/" "$PBXPROJ" +fi + +echo "Reset $PBXPROJ" diff --git a/app/scripts/tests/cleanup-ios-build.test.cjs b/app/scripts/tests/cleanup-ios-build.test.cjs new file mode 100644 index 000000000..47cc451a2 --- /dev/null +++ b/app/scripts/tests/cleanup-ios-build.test.cjs @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { execSync } = require('child_process'); +const { describe, it } = require('node:test'); +const assert = require('node:assert'); + +const SCRIPT = path.join(__dirname, '../cleanup-ios-build.sh'); + +describe('cleanup-ios-build.sh', () => { + it('resets pbxproj and reapplies versions', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cleanup-test-')); + const projectName = 'MyApp'; + const iosDir = path.join(tmp, 'ios', `${projectName}.xcodeproj`); + fs.mkdirSync(iosDir, { recursive: true }); + const pbxPath = path.join(iosDir, 'project.pbxproj'); + fs.writeFileSync( + pbxPath, + 'CURRENT_PROJECT_VERSION = 1;\nMARKETING_VERSION = 1.0.0;\n', + ); + + const cwd = process.cwd(); + process.chdir(tmp); + execSync('git init -q'); + execSync('git config user.email "test@example.com"'); + execSync('git config user.name "Test"'); + execSync(`git add ${pbxPath}`); + execSync('git commit -m init -q'); + + fs.writeFileSync( + pbxPath, + 'CURRENT_PROJECT_VERSION = 2;\nMARKETING_VERSION = 2.0.0;\nSomeArtifact = 123;\n', + ); + + execSync(`IOS_PROJECT_NAME=${projectName} bash ${SCRIPT}`); + process.chdir(cwd); + + const result = fs.readFileSync(pbxPath, 'utf8'); + assert(result.includes('CURRENT_PROJECT_VERSION = 2;')); + assert(result.includes('MARKETING_VERSION = 2.0.0;')); + assert(!result.includes('SomeArtifact')); + }); +}); From 5a8347e12889b04c2224831b273cb3f21c4a1adb Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 16:52:54 -0700 Subject: [PATCH 2/7] cr feedback --- app/fastlane/Fastfile | 9 +++++++-- app/scripts/cleanup-ios-build.sh | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 71f86c14c..28342ebd7 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -23,8 +23,13 @@ android_has_permissions = false # Project configuration PROJECT_NAME = ENV["IOS_PROJECT_NAME"] APP_NAME = ENV["IOS_PROJECT_NAME"] || begin - JSON.parse(File.read("../app.json"))["displayName"] -rescue + app_json_path = File.expand_path("../app.json", __dir__) + if File.exist?(app_json_path) + app_config = JSON.parse(File.read(app_json_path)) + app_config["displayName"] if app_config.is_a?(Hash) + end +rescue JSON::ParserError, Errno::ENOENT + UI.important("Could not read app.json or invalid JSON format, using default app name") nil end || "MobileApp" PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"] diff --git a/app/scripts/cleanup-ios-build.sh b/app/scripts/cleanup-ios-build.sh index c4f2be01a..9b9239421 100755 --- a/app/scripts/cleanup-ios-build.sh +++ b/app/scripts/cleanup-ios-build.sh @@ -13,6 +13,12 @@ fi MARKETING_VERSION=$(grep -m1 "MARKETING_VERSION =" "$PBXPROJ" | awk '{print $3}' | tr -d ';') CURRENT_VERSION=$(grep -m1 "CURRENT_PROJECT_VERSION =" "$PBXPROJ" | awk '{print $3}' | tr -d ';') +# Validate extracted versions +if [[ -z "$MARKETING_VERSION" || -z "$CURRENT_VERSION" ]]; then + echo "Failed to extract version information from $PBXPROJ" >&2 + exit 1 +fi + git checkout -- "$PBXPROJ" if sed --version >/dev/null 2>&1; then From 3962ab661356bbdca587cdc5bb0509f599192242 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 17:18:25 -0700 Subject: [PATCH 3/7] for temp testing --- app/fastlane/Fastfile | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 28342ebd7..7d3710e98 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -61,14 +61,15 @@ platform :ios do lane :internal_test do result = prepare_ios_build(prod_release: false) - upload_to_testflight( - api_key: result[:api_key], - distribute_external: true, - # Only external TestFlight groups are valid here - groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), - changelog: "", - skip_waiting_for_build_processing: false, - ) if result[:should_upload] + # TODO: temporarily disabled to allow for testing + # upload_to_testflight( + # api_key: result[:api_key], + # distribute_external: true, + # # Only external TestFlight groups are valid here + # groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), + # changelog: "", + # skip_waiting_for_build_processing: false, + # ) if result[:should_upload] # Notify Slack about the new build if ENV["SLACK_CHANNEL_ID"] @@ -128,7 +129,8 @@ platform :ios do build_number = config.build_settings["CURRENT_PROJECT_VERSION"] # Verify build number is higher than TestFlight (but don't auto-increment) - Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path) + # TODO: temporarily disabled to allow for testing + # Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path) Fastlane::Helpers.ios_verify_provisioning_profile api_key = app_store_connect_api_key( From 4f1f5132270e4199f8d76447946d6044bc964c85 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 17:26:00 -0700 Subject: [PATCH 4/7] clean build artifacts after deploy --- app/scripts/mobile-deploy-confirm.cjs | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/app/scripts/mobile-deploy-confirm.cjs b/app/scripts/mobile-deploy-confirm.cjs index 8289060c7..cb72a3bc3 100755 --- a/app/scripts/mobile-deploy-confirm.cjs +++ b/app/scripts/mobile-deploy-confirm.cjs @@ -414,6 +414,36 @@ function getFastlaneCommands(platform) { return commands; } +/** + * Executes iOS build cleanup script + * @param {string} platform - Target platform + */ +function performIOSBuildCleanup(platform) { + // Only run cleanup for iOS deployments + if (platform !== PLATFORMS.IOS && platform !== PLATFORMS.BOTH) { + return; + } + + console.log(`\n${CONSOLE_SYMBOLS.BROOM} Cleaning up iOS build artifacts...`); + + try { + const cleanupScript = path.join(__dirname, 'cleanup-ios-build.sh'); + execSync(`bash "${cleanupScript}"`, { + stdio: 'inherit', + cwd: __dirname, + }); + console.log( + `${CONSOLE_SYMBOLS.SUCCESS} iOS build cleanup completed successfully!`, + ); + } catch (error) { + console.error( + `${CONSOLE_SYMBOLS.WARNING} iOS build cleanup failed (non-fatal):`, + error.message, + ); + // Don't exit on cleanup failure - it's not critical + } +} + /** * Executes local fastlane deployment * @param {string} platform - Target platform @@ -423,6 +453,8 @@ async function executeLocalFastlaneDeployment(platform) { `\n${CONSOLE_SYMBOLS.ROCKET} Starting local fastlane deployment...`, ); + let deploymentSuccessful = false; + try { performYarnReinstall(); @@ -443,6 +475,7 @@ async function executeLocalFastlaneDeployment(platform) { }); } + deploymentSuccessful = true; console.log( `${CONSOLE_SYMBOLS.SUCCESS} Local fastlane deployment completed successfully!`, ); @@ -454,7 +487,14 @@ async function executeLocalFastlaneDeployment(platform) { `${CONSOLE_SYMBOLS.ERROR} Local fastlane deployment failed:`, error.message, ); - process.exit(1); + } finally { + // Always run cleanup after deployment, regardless of success/failure + performIOSBuildCleanup(platform); + + // Only exit with error code if deployment failed + if (!deploymentSuccessful) { + process.exit(1); + } } } From 76aa87270c4deb0fcad465b439ee8c3427e15092 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 17:29:32 -0700 Subject: [PATCH 5/7] add deploy source --- app/fastlane/Fastfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 7d3710e98..3ee926508 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -73,10 +73,11 @@ platform :ios do # Notify Slack about the new build if ENV["SLACK_CHANNEL_ID"] + deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy" Fastlane::Helpers.upload_file_to_slack( file_path: result[:ipa_path], channel_id: ENV["SLACK_CHANNEL_ID"], - initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight", + initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight via #{deploy_source}", title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa", ) else @@ -274,10 +275,11 @@ platform :android do # Notify Slack about the new build if ENV["SLACK_CHANNEL_ID"] + deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy" Fastlane::Helpers.upload_file_to_slack( file_path: android_aab_path, channel_id: ENV["SLACK_CHANNEL_ID"], - initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform}", + initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform} via #{deploy_source}", title: "#{APP_NAME}-#{package_version}-#{version_code}.aab", ) else From 097852778ba791e82f3bfe827be468fc1dd595ed Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 17:54:51 -0700 Subject: [PATCH 6/7] uncomment ios commands --- app/fastlane/Fastfile | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 3ee926508..085bf26a6 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -61,15 +61,14 @@ platform :ios do lane :internal_test do result = prepare_ios_build(prod_release: false) - # TODO: temporarily disabled to allow for testing - # upload_to_testflight( - # api_key: result[:api_key], - # distribute_external: true, - # # Only external TestFlight groups are valid here - # groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), - # changelog: "", - # skip_waiting_for_build_processing: false, - # ) if result[:should_upload] + upload_to_testflight( + api_key: result[:api_key], + distribute_external: true, + # Only external TestFlight groups are valid here + groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","), + changelog: "", + skip_waiting_for_build_processing: false, + ) if result[:should_upload] # Notify Slack about the new build if ENV["SLACK_CHANNEL_ID"] @@ -130,8 +129,7 @@ platform :ios do build_number = config.build_settings["CURRENT_PROJECT_VERSION"] # Verify build number is higher than TestFlight (but don't auto-increment) - # TODO: temporarily disabled to allow for testing - # Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path) + Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path) Fastlane::Helpers.ios_verify_provisioning_profile api_key = app_store_connect_api_key( From 3c1bdb9ca1693088671ac647a5f715ee2b8ffa27 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 6 Jul 2025 18:09:53 -0700 Subject: [PATCH 7/7] Add tests for minor deployment fixes (#750) * Add test coverage for deployment scripts and Fastfile * format * increase github check to 5 minutes --- .coderabbit.yaml | 3 + app/fastlane/test/app_name_test.rb | 58 +++++++++++++++++++ app/fastlane/test/helpers_test.rb | 37 ++++++++++++ app/scripts/mobile-deploy-confirm.cjs | 27 ++++++--- app/scripts/tests/cleanup-ios-build.test.cjs | 47 ++++++++++++++- .../mobile-deploy-confirm-module.test.cjs | 48 +++++++++++++++ 6 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 app/fastlane/test/app_name_test.rb create mode 100644 app/scripts/tests/mobile-deploy-confirm-module.test.cjs diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4d3519c9f..ef89b1017 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -15,6 +15,9 @@ reviews: enabled: true drafts: false base_branches: ["main", "dev"] + tools: + github-checks: + timeout_ms: 300000 path_instructions: - path: "app/src/**/*.{ts,tsx,js,jsx}" instructions: | diff --git a/app/fastlane/test/app_name_test.rb b/app/fastlane/test/app_name_test.rb new file mode 100644 index 000000000..c6222c560 --- /dev/null +++ b/app/fastlane/test/app_name_test.rb @@ -0,0 +1,58 @@ +require "minitest/autorun" +require "json" +require "tmpdir" +require "fileutils" +require_relative "../helpers" + +class AppNameTest < Minitest::Test + def setup + @orig_env = ENV.to_h + @tmp = Dir.mktmpdir + end + + def teardown + FileUtils.remove_entry(@tmp) + ENV.clear + ENV.update(@orig_env) + end + + def write_app_json(content) + File.write(File.join(@tmp, "app.json"), content) + end + + def evaluate_app_name + ENV["IOS_PROJECT_NAME"] || begin + app_json_path = File.join(@tmp, "app.json") + if File.exist?(app_json_path) + app_config = JSON.parse(File.read(app_json_path)) + app_config["displayName"] if app_config.is_a?(Hash) + end + rescue JSON::ParserError, Errno::ENOENT + Fastlane::UI.ui_object.important("Could not read app.json or invalid JSON format, using default app name") + nil + end || "MobileApp" + end + + def test_env_variable_precedence + ENV["IOS_PROJECT_NAME"] = "EnvApp" + assert_equal "EnvApp", evaluate_app_name + end + + def test_display_name_from_app_json + ENV.delete("IOS_PROJECT_NAME") + write_app_json({ displayName: "JsonApp" }.to_json) + assert_equal "JsonApp", evaluate_app_name + end + + def test_default_when_app_json_missing_or_malformed + ENV.delete("IOS_PROJECT_NAME") + write_app_json("{ invalid json") + messages = [] + ui_obj = Fastlane::UI.ui_object + orig = ui_obj.method(:important) + ui_obj.define_singleton_method(:important) { |msg| messages << msg } + assert_equal "MobileApp", evaluate_app_name + assert_includes messages.first, "Could not read app.json" + ui_obj.define_singleton_method(:important, orig) + end +end diff --git a/app/fastlane/test/helpers_test.rb b/app/fastlane/test/helpers_test.rb index b0cfb7bf2..2d01dd862 100644 --- a/app/fastlane/test/helpers_test.rb +++ b/app/fastlane/test/helpers_test.rb @@ -342,6 +342,43 @@ def test_upload_file_to_slack_missing_channel ENV.delete("SLACK_API_TOKEN") end + def test_upload_file_to_slack_missing_token + ENV.delete("SLACK_API_TOKEN") + file = Tempfile.new(["artifact", ".txt"]) + file.write("data") + file.close + + assert_raises(FastlaneCore::Interface::FastlaneCommonException) do + Fastlane::Helpers.upload_file_to_slack(file_path: file.path, channel_id: "C123") + end + ensure + file.unlink + end + + def test_slack_deploy_source_messages + file = Tempfile.new(["artifact", ".txt"]) + file.write("data") + file.close + + %w[true nil].each do |ci_value| + ENV["CI"] = ci_value == "true" ? "true" : nil + captured = nil + Fastlane::Helpers.stub(:upload_file_to_slack, ->(**args) { captured = args }) do + deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy" + Fastlane::Helpers.upload_file_to_slack( + file_path: file.path, + channel_id: "C123", + initial_comment: "Deploy via #{deploy_source}", + ) + end + expected = ci_value == "true" ? "GitHub Workflow" : "Local Deploy" + assert_includes captured[:initial_comment], expected + end + ensure + file.unlink + ENV.delete("CI") + end + private def clear_test_env_vars diff --git a/app/scripts/mobile-deploy-confirm.cjs b/app/scripts/mobile-deploy-confirm.cjs index cb72a3bc3..928c2616e 100755 --- a/app/scripts/mobile-deploy-confirm.cjs +++ b/app/scripts/mobile-deploy-confirm.cjs @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); -const { execSync } = require('child_process'); +let { execSync } = require('child_process'); // Constants const DEPLOYMENT_METHODS = { @@ -418,7 +418,7 @@ function getFastlaneCommands(platform) { * Executes iOS build cleanup script * @param {string} platform - Target platform */ -function performIOSBuildCleanup(platform) { +let performIOSBuildCleanup = function (platform) { // Only run cleanup for iOS deployments if (platform !== PLATFORMS.IOS && platform !== PLATFORMS.BOTH) { return; @@ -442,7 +442,7 @@ function performIOSBuildCleanup(platform) { ); // Don't exit on cleanup failure - it's not critical } -} +}; /** * Executes local fastlane deployment @@ -576,7 +576,20 @@ async function main() { } // Execute main function -main().catch(error => { - console.error(`${CONSOLE_SYMBOLS.ERROR} Error:`, error.message); - process.exit(1); -}); +if (require.main === module) { + main().catch(error => { + console.error(`${CONSOLE_SYMBOLS.ERROR} Error:`, error.message); + process.exit(1); + }); +} else { + module.exports = { + performIOSBuildCleanup, + executeLocalFastlaneDeployment, + _setExecSync: fn => { + execSync = fn; + }, + _setPerformIOSBuildCleanup: fn => { + performIOSBuildCleanup = fn; + }, + }; +} diff --git a/app/scripts/tests/cleanup-ios-build.test.cjs b/app/scripts/tests/cleanup-ios-build.test.cjs index 47cc451a2..358c5f5d9 100644 --- a/app/scripts/tests/cleanup-ios-build.test.cjs +++ b/app/scripts/tests/cleanup-ios-build.test.cjs @@ -2,10 +2,11 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); +const { spawnSync } = require('child_process'); const { describe, it } = require('node:test'); const assert = require('node:assert'); -const SCRIPT = path.join(__dirname, '../cleanup-ios-build.sh'); +const SCRIPT = path.resolve(__dirname, '../cleanup-ios-build.sh'); describe('cleanup-ios-build.sh', () => { it('resets pbxproj and reapplies versions', () => { @@ -40,4 +41,48 @@ describe('cleanup-ios-build.sh', () => { assert(result.includes('MARKETING_VERSION = 2.0.0;')); assert(!result.includes('SomeArtifact')); }); + + it('fails when the pbxproj file does not exist', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cleanup-test-')); + + const result = spawnSync('bash', [SCRIPT], { + cwd: tmp, + env: { ...process.env, IOS_PROJECT_NAME: 'MissingProject' }, + encoding: 'utf8', + }); + + assert.notStrictEqual(result.status, 0); + assert(result.stderr.includes('Project file not found')); + }); + + it('fails when version information cannot be extracted', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cleanup-test-')); + const projectName = 'BadApp'; + const iosDir = path.join(tmp, 'ios', `${projectName}.xcodeproj`); + fs.mkdirSync(iosDir, { recursive: true }); + const pbxPath = path.join(iosDir, 'project.pbxproj'); + fs.writeFileSync( + pbxPath, + 'CURRENT_PROJECT_VERSION = ;\nMARKETING_VERSION = ;\n', + ); + + const cwd = process.cwd(); + process.chdir(tmp); + execSync('git init -q'); + execSync('git config user.email "test@example.com"'); + execSync('git config user.name "Test"'); + execSync(`git add ${pbxPath}`); + execSync('git commit -m init -q'); + + const result = spawnSync('bash', [SCRIPT], { + cwd: tmp, + env: { ...process.env, IOS_PROJECT_NAME: projectName }, + encoding: 'utf8', + }); + + process.chdir(cwd); + + assert.notStrictEqual(result.status, 0); + assert(result.stderr.includes('Failed to extract version information')); + }); }); diff --git a/app/scripts/tests/mobile-deploy-confirm-module.test.cjs b/app/scripts/tests/mobile-deploy-confirm-module.test.cjs new file mode 100644 index 000000000..2e6db3306 --- /dev/null +++ b/app/scripts/tests/mobile-deploy-confirm-module.test.cjs @@ -0,0 +1,48 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const child_process = require('child_process'); + +describe('performIOSBuildCleanup', () => { + it('executes cleanup script for ios', () => { + const deploy = require('../mobile-deploy-confirm.cjs'); + let called = null; + const original = child_process.execSync; + deploy._setExecSync(cmd => { + called = cmd; + }); + deploy.performIOSBuildCleanup('ios'); + deploy._setExecSync(original); + assert(called && called.includes('cleanup-ios-build.sh')); + }); + + it('does nothing for android', () => { + const deploy = require('../mobile-deploy-confirm.cjs'); + let called = false; + const original = child_process.execSync; + deploy._setExecSync(() => { + called = true; + }); + deploy.performIOSBuildCleanup('android'); + deploy._setExecSync(original); + assert.strictEqual(called, false); + }); +}); + +describe('executeLocalFastlaneDeployment', () => { + it('invokes cleanup after deployment', async () => { + const deploy = require('../mobile-deploy-confirm.cjs'); + deploy._setExecSync(() => {}); + + let cleanupCalled = false; + const originalCleanup = deploy.performIOSBuildCleanup; + deploy._setPerformIOSBuildCleanup(() => { + cleanupCalled = true; + }); + + await deploy.executeLocalFastlaneDeployment('ios'); + + deploy._setPerformIOSBuildCleanup(originalCleanup); + deploy._setExecSync(child_process.execSync); + assert.strictEqual(cleanupCalled, true); + }); +});