diff --git a/README.md b/README.md index b7a986b..2637a7a 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,87 @@ fastlane add_plugin instabug-stores-upload ## About instabug-stores-upload -Wrapper plugin for uploading builds to App Store and Play Store with Instabug-specific metadata reporting. +Wrapper plugin for uploading builds to App Store and Play Store with Instabug-specific metadata reporting. This plugin provides custom actions that wrap the standard Fastlane actions and automatically report build and upload events to Instabug systems for better observability and integration into internal pipelines. -**Note to author:** Add a more detailed description about this plugin here. If your plugin contains multiple actions, make sure to mention them here. +### Available Actions + +- `instabug_build_ios_app` - Build iOS apps with Instabug reporting +- `instabug_build_android_app` - Build Android apps with Instabug reporting +- `instabug_upload_to_app_store` - Upload iOS builds to App Store with Instabug reporting +- `instabug_upload_to_play_store` - Upload Android builds to Play Store with Instabug reporting + +### Features + +- Automatic reporting of build and upload events to Instabug +- Branch-based tracking for Instabug Agents observability +- Integration with existing Fastlane workflows +- Support for both iOS and Android platforms +- Secure API communication with Instabug services ## Example Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`. -**Note to author:** Please set up a sample project to make it easy for users to explore what your plugin does. Provide everything that is necessary to try out the plugin in this project (including a sample Xcode/Android project if necessary) +### Usage Examples + +#### iOS Build +```ruby +lane :build_ios do + instabug_build_ios_app( + branch_name: "main", + instabug_api_key: ENV["INSTABUG_API_KEY"], + workspace: "MyApp.xcworkspace", + scheme: "MyApp", + export_method: "app-store", + configuration: "Release" + ) +end +``` + +#### Android Build +```ruby +lane :build_android do + instabug_build_android_app( + branch_name: "main", + instabug_api_key: ENV["INSTABUG_API_KEY"], + task: "assembleRelease", + project_dir: "android/", + properties: { + "android.injected.signing.store.file" => "keystore.jks", + "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => "key0", + "android.injected.signing.key.password" => ENV["KEY_PASSWORD"] + } + ) +end +``` + +#### iOS Upload +```ruby +lane :upload_ios do + instabug_upload_to_app_store( + branch_name: "main", + instabug_api_key: ENV["INSTABUG_API_KEY"], + ipa: "path/to/your/app.ipa", + skip_screenshots: true, + skip_metadata: true + ) +end +``` + +#### Android Upload +```ruby +lane :upload_android do + instabug_upload_to_play_store( + branch_name: "main", + instabug_api_key: ENV["INSTABUG_API_KEY"], + package_name: "com.example.app", + aab: "path/to/your/app.aab", + track: "internal", + skip_upload_screenshots: true + ) +end +``` ## Run tests for this plugin diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5515952..d02d45a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,5 +1,39 @@ # Fastfile for Instabug Stores Upload Plugin +# Example lane for building iOS app with Instabug reporting +lane :build_ios_app do |options| + branch_name = options[:branch_name] + + instabug_build_ios_app( + branch_name: branch_name, + instabug_api_key: ENV["INSTABUG_API_KEY"], + # All standard build_ios_app parameters are supported + workspace: "MyApp.xcworkspace", + scheme: "MyApp", + export_method: "app-store", + configuration: "Release" + ) +end + +# Example lane for building Android app with Instabug reporting +lane :build_android_app do |options| + branch_name = options[:branch_name] + + instabug_build_android_app( + branch_name: branch_name, + instabug_api_key: ENV["INSTABUG_API_KEY"], + # All standard gradle parameters are supported + task: "assembleRelease", + project_dir: "android/", + properties: { + "android.injected.signing.store.file" => "keystore.jks", + "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => "key0", + "android.injected.signing.key.password" => ENV["KEY_PASSWORD"] + } + ) +end + # Example lane for uploading to App Store with Instabug reporting lane :upload_to_app_store do |options| branch_name = options[:branch_name] @@ -24,4 +58,4 @@ lane :upload_to_play_store do |options| aab: "path/to/your/app.aab", track: "internal" ) -end \ No newline at end of file +end diff --git a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_android_app_action.rb b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_android_app_action.rb new file mode 100644 index 0000000..db9e8f1 --- /dev/null +++ b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_android_app_action.rb @@ -0,0 +1,124 @@ +require 'fastlane/action' +require_relative '../helper/instabug_stores_upload_helper' + +module Fastlane + module Actions + class InstabugBuildAndroidAppAction < Action + def self.run(params) + UI.message("Starting Instabug Android build...") + + # Extract Instabug-specific parameters + branch_name = params.delete(:branch_name) + instabug_api_key = params.delete(:instabug_api_key) + + # Validate required parameters + if branch_name.nil? || branch_name.empty? + UI.user_error!("branch_name is required for Instabug reporting") + end + + begin + # Report build start to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "inprogress", + step: "build_app" + ) + + # Execute the actual Android build using gradle + result = Actions::GradleAction.run(params) + + # Report build success to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "success", + step: "build_app" + ) + + UI.success("Android build completed successfully!") + result + rescue => e + UI.error("Android build failed: #{e.message}") + + # Report build failure to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "failure", + step: "build_app" + ) + raise e + end + end + + def self.description + "Build Android app with Instabug metadata reporting" + end + + def self.authors + ["Instabug Company"] + end + + def self.return_value + "Returns the result from gradle action" + end + + def self.details + "This action wraps the standard gradle action and adds Instabug-specific metadata reporting. It tracks build events per branch and provides better observability for engineering teams." + end + + def self.available_options + # Start with the original gradle options + options = Actions::GradleAction.available_options + + # Add Instabug-specific options + instabug_options = [ + FastlaneCore::ConfigItem.new( + key: :branch_name, + env_name: "INSTABUG_BRANCH_NAME", + description: "The branch name for tracking builds", + optional: false, + type: String + ), + FastlaneCore::ConfigItem.new( + key: :instabug_api_key, + env_name: "INSTABUG_API_KEY", + description: "Instabug API key for reporting build events", + optional: false, + type: String, + sensitive: true + ) + ] + + # Combine both sets of options + options + instabug_options + end + + def self.is_supported?(platform) + platform == :android + end + + def self.example_code + [ + 'instabug_build_android_app( + branch_name: "main", + instabug_api_key: "your-api-key", + task: "assembleRelease", + project_dir: "android/", + properties: { + "android.injected.signing.store.file" => "keystore.jks", + "android.injected.signing.store.password" => "password", + "android.injected.signing.key.alias" => "key0", + "android.injected.signing.key.password" => "password" + } + )' + ] + end + + def self.category + :building + end + end + end +end \ No newline at end of file diff --git a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_ios_app_action.rb b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_ios_app_action.rb new file mode 100644 index 0000000..54c2707 --- /dev/null +++ b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_build_ios_app_action.rb @@ -0,0 +1,120 @@ +require 'fastlane/action' +require_relative '../helper/instabug_stores_upload_helper' + +module Fastlane + module Actions + class InstabugBuildIosAppAction < Action + def self.run(params) + UI.message("Starting Instabug iOS build...") + + # Extract Instabug-specific parameters + branch_name = params.delete(:branch_name) + instabug_api_key = params.delete(:instabug_api_key) + + # Validate required parameters + if branch_name.nil? || branch_name.empty? + UI.user_error!("branch_name is required for Instabug reporting") + end + + begin + # Report build start to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "inprogress", + step: "build_app" + ) + + # Execute the actual iOS build + result = Actions::BuildIosAppAction.run(params) + + # Report build success to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "success", + step: "build_app" + ) + + UI.success("iOS build completed successfully!") + result + rescue => e + UI.error("iOS build failed: #{e.message}") + + # Report build failure to Instabug + Helper::InstabugStoresUploadHelper.report_status( + branch_name: branch_name, + api_key: instabug_api_key, + status: "failure", + step: "build_app" + ) + raise e + end + end + + def self.description + "Build iOS app with Instabug metadata reporting" + end + + def self.authors + ["Instabug Company"] + end + + def self.return_value + "Returns the result from build_ios_app action" + end + + def self.details + "This action wraps the standard build_ios_app action and adds Instabug-specific metadata reporting. It tracks build events per branch and provides better observability for engineering teams." + end + + def self.available_options + # Start with the original build_ios_app options + options = Actions::BuildIosAppAction.available_options + + # Add Instabug-specific options + instabug_options = [ + FastlaneCore::ConfigItem.new( + key: :branch_name, + env_name: "INSTABUG_BRANCH_NAME", + description: "The branch name for tracking builds", + optional: false, + type: String + ), + FastlaneCore::ConfigItem.new( + key: :instabug_api_key, + env_name: "INSTABUG_API_KEY", + description: "Instabug API key for reporting build events", + optional: false, + type: String, + sensitive: true + ) + ] + + # Combine both sets of options + options + instabug_options + end + + def self.is_supported?(platform) + [:ios, :mac].include?(platform) + end + + def self.example_code + [ + 'instabug_build_ios_app( + branch_name: "main", + instabug_api_key: "your-api-key", + workspace: "MyApp.xcworkspace", + scheme: "MyApp", + export_method: "app-store", + configuration: "Release" + )' + ] + end + + def self.category + :building + end + end + end +end \ No newline at end of file diff --git a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_app_store_action.rb b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_app_store_action.rb index f3e7fe2..c1b512f 100644 --- a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_app_store_action.rb +++ b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_app_store_action.rb @@ -21,7 +21,8 @@ def self.run(params) Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "inprogress" + status: "inprogress", + step: "upload_to_the_store" ) # Execute the actual upload to App Store @@ -31,7 +32,8 @@ def self.run(params) Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "success" + status: "success", + step: "upload_to_the_store" ) UI.success("App Store upload completed successfully!") @@ -43,7 +45,8 @@ def self.run(params) Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "failure" + status: "failure", + step: "upload_to_the_store" ) raise e end diff --git a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_play_store_action.rb b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_play_store_action.rb index 661e9e8..c13d788 100644 --- a/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_play_store_action.rb +++ b/lib/fastlane/plugin/instabug_stores_upload/actions/instabug_upload_to_play_store_action.rb @@ -21,7 +21,8 @@ def self.run(params) Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "inprogress" + status: "inprogress", + step: "upload_to_the_store" ) # Execute the actual upload to Play Store @@ -31,21 +32,23 @@ def self.run(params) Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "success" + status: "success", + step: "upload_to_the_store" ) UI.success("Play Store upload completed successfully!") result rescue => e + UI.error("Play Store upload failed: #{e.message}") + # Report upload failure to Instabug Helper::InstabugStoresUploadHelper.report_status( branch_name: branch_name, api_key: instabug_api_key, - status: "failure" + status: "failure", + step: "upload_to_the_store" ) - - UI.error("Play Store upload failed: #{e.message}") raise e end end diff --git a/lib/fastlane/plugin/instabug_stores_upload/helper/instabug_stores_upload_helper.rb b/lib/fastlane/plugin/instabug_stores_upload/helper/instabug_stores_upload_helper.rb index bd203c3..25a772e 100644 --- a/lib/fastlane/plugin/instabug_stores_upload/helper/instabug_stores_upload_helper.rb +++ b/lib/fastlane/plugin/instabug_stores_upload/helper/instabug_stores_upload_helper.rb @@ -15,16 +15,16 @@ def self.show_message UI.message("Hello from the instabug_stores_upload plugin helper!") end - def self.report_status(branch_name:, api_key:, status:) + def self.report_status(branch_name:, api_key:, status:, step:) return unless branch_name.start_with?('crash-fix/instabug-crash-') - UI.message("📡 Reporting upload status to Instabug for #{branch_name}/#{status}") + UI.message("📡 Reporting #{step} status to Instabug for #{branch_name}/#{status}") make_api_request( branch_name: branch_name, status: status, api_key: api_key, - step: "upload_to_the_store" + step: step ) end diff --git a/spec/instabug_build_android_app_action_spec.rb b/spec/instabug_build_android_app_action_spec.rb new file mode 100644 index 0000000..e4e22aa --- /dev/null +++ b/spec/instabug_build_android_app_action_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe Fastlane::Actions::InstabugBuildAndroidAppAction do + let(:valid_params) do + { + branch_name: 'crash-fix/instabug-crash-456', + instabug_api_key: 'test-api-key', + task: 'assembleRelease', + project_dir: 'android/', + properties: { + 'android.injected.signing.store.file' => 'keystore.jks', + 'android.injected.signing.store.password' => 'password' + } + } + end + + let(:api_endpoint) { 'https://api.instabug.com/api/web/public/agent_fastlane/status' } + + before do + stub_request(:patch, api_endpoint) + .to_return(status: 200, body: '{}', headers: {}) + end + + describe '#run' do + context 'when build succeeds' do + it 'reports inprogress, calls build action, and reports success' do + expect(Fastlane::Actions::GradleAction).to receive(:run) + .with(hash_including(task: 'assembleRelease', project_dir: 'android/')) + .and_return('build_result') + + result = described_class.run(valid_params) + + expect(result).to eq('build_result') + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-456', + status: 'inprogress', + step: 'build_app' + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer test-api-key', + 'User-Agent' => 'fastlane-plugin-instabug-stores-upload' + } + ).once + + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-456', + status: 'success', + step: 'build_app' + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer test-api-key', + 'User-Agent' => 'fastlane-plugin-instabug-stores-upload' + } + ).once + end + end + + context 'when build fails' do + it 'reports failure and re-raises the error' do + error = StandardError.new('Build failed') + expect(Fastlane::Actions::GradleAction).to receive(:run) + .and_raise(error) + + expect { + described_class.run(valid_params) + }.to raise_error(StandardError, 'Build failed') + + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-456', + status: 'failure', + step: 'build_app' + }.to_json + ) + end + end + + context 'when branch_name is missing' do + it 'raises user error' do + params = valid_params.merge(branch_name: nil) + + expect { + described_class.run(params) + }.to raise_error(FastlaneCore::Interface::FastlaneError, 'branch_name is required for Instabug reporting') + end + end + + context 'when branch_name is empty' do + it 'raises user error' do + params = valid_params.merge(branch_name: '') + + expect { + described_class.run(params) + }.to raise_error(FastlaneCore::Interface::FastlaneError, 'branch_name is required for Instabug reporting') + end + end + + context 'when branch name does not match instabug pattern' do + it 'does not make API calls but still runs build' do + params = valid_params.merge(branch_name: 'feature/new-feature') + + expect(Fastlane::Actions::GradleAction).to receive(:run) + .and_return('build_result') + + result = described_class.run(params) + + expect(result).to eq('build_result') + expect(WebMock).not_to have_requested(:patch, api_endpoint) + end + end + end + + describe 'metadata' do + it 'has correct description' do + expect(described_class.description).to eq('Build Android app with Instabug metadata reporting') + end + + it 'supports Android platform only' do + expect(described_class.is_supported?(:android)).to be true + expect(described_class.is_supported?(:ios)).to be false + expect(described_class.is_supported?(:mac)).to be false + end + + it 'has correct category' do + expect(described_class.category).to eq(:building) + end + end +end \ No newline at end of file diff --git a/spec/instabug_build_ios_app_action_spec.rb b/spec/instabug_build_ios_app_action_spec.rb new file mode 100644 index 0000000..e7e6337 --- /dev/null +++ b/spec/instabug_build_ios_app_action_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe Fastlane::Actions::InstabugBuildIosAppAction do + let(:valid_params) do + { + branch_name: 'crash-fix/instabug-crash-123', + instabug_api_key: 'test-api-key', + workspace: 'Test.xcworkspace', + scheme: 'Test', + export_method: 'app-store' + } + end + + let(:api_endpoint) { 'https://api.instabug.com/api/web/public/agent_fastlane/status' } + + before do + stub_request(:patch, api_endpoint) + .to_return(status: 200, body: '{}', headers: {}) + end + + describe '#run' do + context 'when build succeeds' do + it 'reports inprogress, calls build action, and reports success' do + expect(Fastlane::Actions::BuildIosAppAction).to receive(:run) + .with(hash_including(workspace: 'Test.xcworkspace', scheme: 'Test', export_method: 'app-store')) + .and_return('build_result') + + result = described_class.run(valid_params) + + expect(result).to eq('build_result') + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-123', + status: 'inprogress', + step: 'build_app' + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer test-api-key', + 'User-Agent' => 'fastlane-plugin-instabug-stores-upload' + } + ).once + + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-123', + status: 'success', + step: 'build_app' + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer test-api-key', + 'User-Agent' => 'fastlane-plugin-instabug-stores-upload' + } + ).once + end + end + + context 'when build fails' do + it 'reports failure and re-raises the error' do + error = StandardError.new('Build failed') + expect(Fastlane::Actions::BuildIosAppAction).to receive(:run) + .and_raise(error) + + expect { + described_class.run(valid_params) + }.to raise_error(StandardError, 'Build failed') + + expect(WebMock).to have_requested(:patch, api_endpoint) + .with( + body: { + branch_name: 'crash-fix/instabug-crash-123', + status: 'failure', + step: 'build_app' + }.to_json + ) + end + end + + context 'when branch_name is missing' do + it 'raises user error' do + params = valid_params.merge(branch_name: nil) + + expect { + described_class.run(params) + }.to raise_error(FastlaneCore::Interface::FastlaneError, 'branch_name is required for Instabug reporting') + end + end + + context 'when branch_name is empty' do + it 'raises user error' do + params = valid_params.merge(branch_name: '') + + expect { + described_class.run(params) + }.to raise_error(FastlaneCore::Interface::FastlaneError, 'branch_name is required for Instabug reporting') + end + end + + context 'when branch name does not match instabug pattern' do + it 'does not make API calls but still runs build' do + params = valid_params.merge(branch_name: 'feature/new-feature') + + expect(Fastlane::Actions::BuildIosAppAction).to receive(:run) + .and_return('build_result') + + result = described_class.run(params) + + expect(result).to eq('build_result') + expect(WebMock).not_to have_requested(:patch, api_endpoint) + end + end + end + + describe 'metadata' do + it 'has correct description' do + expect(described_class.description).to eq('Build iOS app with Instabug metadata reporting') + end + + it 'supports iOS and Mac platforms' do + expect(described_class.is_supported?(:ios)).to be true + expect(described_class.is_supported?(:mac)).to be true + expect(described_class.is_supported?(:android)).to be false + end + + it 'has correct category' do + expect(described_class.category).to eq(:building) + end + end +end \ No newline at end of file