diff --git a/fastlane/lib/fastlane/actions/clipboard.rb b/fastlane/lib/fastlane/actions/clipboard.rb index eb4d77b2975..14e96abfdd3 100644 --- a/fastlane/lib/fastlane/actions/clipboard.rb +++ b/fastlane/lib/fastlane/actions/clipboard.rb @@ -7,10 +7,7 @@ def self.run(params) truncated_value = value[0..800].gsub(/\s\w+\s*$/, '...') UI.message("Storing '#{truncated_value}' in the clipboard 🎨") - if FastlaneCore::Helper.mac? - require 'open3' - Open3.popen3('pbcopy') { |input, _, _| input << value } - end + FastlaneCore::Clipboard.copy(content: value) end ##################################################### @@ -30,11 +27,11 @@ def self.available_options end def self.authors - ["KrauseFx", "joshdholtz"] + ["KrauseFx", "joshdholtz", "rogerluan"] end def self.is_supported?(platform) - true + FastlaneCore::Clipboard.is_supported? end def self.example_code diff --git a/fastlane_core/lib/fastlane_core.rb b/fastlane_core/lib/fastlane_core.rb index fc14e561d1e..ae3a8dd5caf 100644 --- a/fastlane_core/lib/fastlane_core.rb +++ b/fastlane_core/lib/fastlane_core.rb @@ -3,39 +3,40 @@ require_relative 'fastlane_core/core_ext/string' require_relative 'fastlane_core/core_ext/shellwords' +require_relative 'fastlane_core/analytics/action_completion_context' +require_relative 'fastlane_core/analytics/action_launch_context' +require_relative 'fastlane_core/analytics/analytics_event_builder' +require_relative 'fastlane_core/analytics/analytics_ingester_client' +require_relative 'fastlane_core/analytics/analytics_session' +require_relative 'fastlane_core/build_watcher' +require_relative 'fastlane_core/cert_checker' +require_relative 'fastlane_core/clipboard' +require_relative 'fastlane_core/command_executor' +require_relative 'fastlane_core/configuration/configuration' +require_relative 'fastlane_core/device_manager' require_relative 'fastlane_core/env' +require_relative 'fastlane_core/fastlane_folder' +require_relative 'fastlane_core/fastlane_pty' require_relative 'fastlane_core/feature/feature' require_relative 'fastlane_core/features' require_relative 'fastlane_core/helper' -require_relative 'fastlane_core/configuration/configuration' -require_relative 'fastlane_core/update_checker/update_checker' -require_relative 'fastlane_core/languages' -require_relative 'fastlane_core/cert_checker' require_relative 'fastlane_core/ipa_file_analyser' +require_relative 'fastlane_core/ipa_upload_package_builder' require_relative 'fastlane_core/itunes_transporter' -require_relative 'fastlane_core/provisioning_profile' +require_relative 'fastlane_core/keychain_importer' +require_relative 'fastlane_core/languages' require_relative 'fastlane_core/pkg_file_analyser' require_relative 'fastlane_core/pkg_upload_package_builder' -require_relative 'fastlane_core/command_executor' -require_relative 'fastlane_core/ipa_upload_package_builder' require_relative 'fastlane_core/print_table' require_relative 'fastlane_core/project' -require_relative 'fastlane_core/device_manager' -require_relative 'fastlane_core/ui/ui' -require_relative 'fastlane_core/fastlane_folder' -require_relative 'fastlane_core/keychain_importer' +require_relative 'fastlane_core/provisioning_profile' +require_relative 'fastlane_core/queue_worker' require_relative 'fastlane_core/swag' -require_relative 'fastlane_core/build_watcher' -require_relative 'fastlane_core/ui/errors' -require_relative 'fastlane_core/test_parser' -require_relative 'fastlane_core/analytics/action_completion_context' -require_relative 'fastlane_core/analytics/action_launch_context' -require_relative 'fastlane_core/analytics/analytics_event_builder' -require_relative 'fastlane_core/analytics/analytics_ingester_client' -require_relative 'fastlane_core/analytics/analytics_session' require_relative 'fastlane_core/tag_version' -require_relative 'fastlane_core/fastlane_pty' -require_relative 'fastlane_core/queue_worker' +require_relative 'fastlane_core/test_parser' +require_relative 'fastlane_core/ui/errors' +require_relative 'fastlane_core/ui/ui' +require_relative 'fastlane_core/update_checker/update_checker' # Third Party code require 'colored' diff --git a/fastlane_core/lib/fastlane_core/clipboard.rb b/fastlane_core/lib/fastlane_core/clipboard.rb new file mode 100644 index 00000000000..6f6dea19447 --- /dev/null +++ b/fastlane_core/lib/fastlane_core/clipboard.rb @@ -0,0 +1,20 @@ +require 'fastlane_core' +require 'open3' + +module FastlaneCore + class Clipboard + def self.copy(content: nil) + return UI.crash!("'pbcopy' or 'pbpaste' command not found.") unless is_supported? + Open3.popen3('pbcopy') { |input, _, _| input << content } + end + + def self.paste + return UI.crash!("'pbcopy' or 'pbpaste' command not found.") unless is_supported? + return `pbpaste` + end + + def self.is_supported? + return `which pbcopy`.length > 0 && `which pbpaste`.length > 0 + end + end +end diff --git a/fastlane_core/spec/clipboard_spec.rb b/fastlane_core/spec/clipboard_spec.rb new file mode 100644 index 00000000000..483a720b64c --- /dev/null +++ b/fastlane_core/spec/clipboard_spec.rb @@ -0,0 +1,27 @@ +describe FastlaneCore do + describe FastlaneCore::Clipboard do + describe '#copy and paste' do + before(:each) do + @test_message = "_fastlane_ is awesome" + end + + it 'should work on supported environments', if: FastlaneCore::Clipboard.is_supported? do + # Save clipboard + clipboard = FastlaneCore::Clipboard.paste + + # Test copy and paste + FastlaneCore::Clipboard.copy(content: @test_message) + expect(FastlaneCore::Clipboard.paste).to eq(@test_message) + + # Restore clipboard + FastlaneCore::Clipboard.copy(content: clipboard) + expect(FastlaneCore::Clipboard.paste).to eq(clipboard) + end + + it 'should throw on non-supported environment', if: !FastlaneCore::Clipboard.is_supported? do + expect { FastlaneCore::Clipboard.copy(content: @test_message) }.to raise_error("'pbcopy' or 'pbpaste' command not found.") + expect { FastlaneCore::Clipboard.paste }.to raise_error("'pbcopy' or 'pbpaste' command not found.") + end + end + end +end diff --git a/spaceship/README.md b/spaceship/README.md index d32f47b30f9..38b73f2de78 100644 --- a/spaceship/README.md +++ b/spaceship/README.md @@ -111,21 +111,11 @@ When your Apple account has 2 factor verification enabled, you'll automatically #### Web sessions -To generate a web session for your CI machine, use - -```sh -fastlane spaceauth -u user@example.org -``` - -This will authenticate you and provide a string that can be transferred to your CI system. Copy everything from `---\n` to your CI server and provide it as environment variable named `FASTLANE_SESSION`. For example: - -``` -export FASTLANE_SESSION='---\n- !ruby/object:HTTP::Cookie\n name: DES5c148586dfd451e55afbaaa5f62418f91\n value: HSARMTKNSRVTWFla1+yO4gVPowH17VaaaxPFnUdMUegQZxqy1Ie1c2v6bM1vSOzIbuOmrl/FNenlScsd/NbF7/Lw4cpnL15jsyg0TOJwP32tC/NguPiyOaaaU+jrj4tf4uKdIywVaaaFSRVT\n domain: idmsa.apple.com\n for_domain: true\n path: "/"\n secure: true\n httponly: true\n expires: 2016-04-27 23:55:56.000000000 Z\n max_age: \n created_at: 2016-03-28 16:55:57.032086000 -07:00\n accessed_at: 2016-03-28 19:11:17.828141000 -07:00\n' -``` +See [Continuous Integration > Authenticating with Apple services > Method 2: Two-step or two-factor authentication > Storing a manually verified session using spaceauth](https://docs.fastlane.tools/best-practices/continuous-integration/#storing-a-manually-verified-session-using-spaceauth) #### Transporter -See [Continuous Integration > Authentication with Apple services > Application specific passwords](https://docs.fastlane.tools/best-practices/continuous-integration/#application-specific-passwords) +See [Getting Started > iOS > Authentication > Method 3: Application-specific passwords](https://docs.fastlane.tools/getting-started/ios/authentication/#method-3-application-specific-passwords) ## _spaceship_ in use diff --git a/spaceship/lib/spaceship/commands_generator.rb b/spaceship/lib/spaceship/commands_generator.rb index 9ea1ba4874d..22b0f4f98a0 100644 --- a/spaceship/lib/spaceship/commands_generator.rb +++ b/spaceship/lib/spaceship/commands_generator.rb @@ -39,9 +39,10 @@ def run command :spaceauth do |c| c.syntax = 'fastlane spaceship spaceauth' c.description = 'Authentication helper for spaceship/fastlane to work with Apple 2-Step/2FA' + c.option('--copy_to_clipboard', 'Whether the session string should be copied to clipboard. For more info see https://docs.fastlane.tools/best-practices/continuous-integration/#storing-a-manually-verified-session-using-spaceauth`') c.action do |args, options| - Spaceship::SpaceauthRunner.new(username: options.user).run + Spaceship::SpaceauthRunner.new(username: options.user, copy_to_clipboard: options.copy_to_clipboard).run end end diff --git a/spaceship/lib/spaceship/spaceauth_runner.rb b/spaceship/lib/spaceship/spaceauth_runner.rb index b5f3772d737..241a26fec38 100644 --- a/spaceship/lib/spaceship/spaceauth_runner.rb +++ b/spaceship/lib/spaceship/spaceauth_runner.rb @@ -1,15 +1,17 @@ require 'colored' require 'credentials_manager/appfile_config' require 'yaml' +require 'fastlane_core' require_relative 'tunes/tunes_client' module Spaceship class SpaceauthRunner - def initialize(username: nil) + def initialize(username: nil, copy_to_clipboard: nil) @username = username @username ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id) @username ||= ask("Username: ") + @copy_to_clipboard = copy_to_clipboard end def run @@ -22,7 +24,7 @@ def run puts("Could not login to App Store Connect".red) puts("Please check your credentials and try again.".yellow) puts("This could be an issue with App Store Connect,".yellow) - puts("Please try unsetting the FASTLANE_SESSION environment variable".yellow) + puts("Please try unsetting the FASTLANE_SESSION environment variable by calling 'unset FASTLANE_SESSION'".yellow) puts("(if it is set) and re-run `fastlane spaceauth`".yellow) puts("") puts("Exception type: #{ex.class}") @@ -49,22 +51,30 @@ def run cookie.name.start_with?("myacinfo") || cookie.name == "dqsid" || cookie.name.start_with?("DES") end - yaml = cookies.to_yaml.gsub("\n", "\\n") + @yaml = cookies.to_yaml.gsub("\n", "\\n") puts("---") puts("") puts("Pass the following via the FASTLANE_SESSION environment variable:") - puts(yaml.cyan.underline) + puts(@yaml.cyan.underline) puts("") puts("") puts("Example:") - puts("export FASTLANE_SESSION='#{yaml}'".cyan.underline) + puts("export FASTLANE_SESSION='#{@yaml}'".cyan.underline) - if mac? && Spaceship::Client::UserInterface.interactive? && agree("🙄 Should fastlane copy the cookie into your clipboard, so you can easily paste it? (y/n)", true) - require 'open3' - Open3.popen3('pbcopy') { |input, _, _| input << yaml } - puts("Successfully copied text into your clipboard 🎨".green) + if @copy_to_clipboard == false + puts("Skipped asking to copy the session string into your clipboard ⏭️".green) + elsif @copy_to_clipboard || (mac? && Spaceship::Client::UserInterface.interactive? && agree("🙄 Should fastlane copy the cookie into your clipboard, so you can easily paste it? (y/n)", true)) + FastlaneCore::Clipboard.copy(content: @yaml) + puts("Successfully copied the session string into your clipboard 🎨".green) end + + return self + end + + def session_string + FastlaneCore::UI.user_error!("`#{__method__}` method called before calling `run` in `SpaceauthRunner`") unless @yaml + @yaml end def mac? diff --git a/spaceship/spec/spaceauth_spec.rb b/spaceship/spec/spaceauth_spec.rb index ec064288320..15f7acb59f9 100644 --- a/spaceship/spec/spaceauth_spec.rb +++ b/spaceship/spec/spaceauth_spec.rb @@ -1,4 +1,5 @@ require_relative 'tunes/tunes_stubbing' +require 'fastlane_core/clipboard' describe Spaceship::SpaceauthRunner do let(:user_cookie) { TunesStubbing.itc_read_fixture_file('spaceauth_cookie.yml') } @@ -13,4 +14,39 @@ Spaceship::SpaceauthRunner.new.run end.to output(/export FASTLANE_SESSION=.*name: DES.*name: myacinfo.*name: dqsid.*/).to_stdout end + + describe 'copy_to_clipboard option', if: FastlaneCore::Clipboard.is_supported? do + before :each do + # Save clipboard + @clipboard = FastlaneCore::Clipboard.paste + end + + after :each do + # Restore clipboard + FastlaneCore::Clipboard.copy(content: @clipboard) + end + + it 'when true, it should copy the session to clipboard' do + Spaceship::SpaceauthRunner.new(copy_to_clipboard: true).run + expect(FastlaneCore::Clipboard.paste).to match(%r{.*domain: idmsa.apple.com.*path: \"\/appleauth\/auth\/\".*}) + end + + it 'when false, it should not copy the session to clipboard' do + Spaceship::SpaceauthRunner.new(copy_to_clipboard: false).run + expect(FastlaneCore::Clipboard.paste).to eq(@clipboard) + end + end + + describe '#session_string' do + it 'should return the session when called after run' do + expect(Spaceship::SpaceauthRunner.new.run.session_string).to match(%r{.*domain: idmsa.apple.com.*path: \"\/appleauth\/auth\/\".*}) + end + + it 'should throw when called before run' do + expect(FastlaneCore::UI).to receive(:user_error!).with(/method called before calling `run` in `SpaceauthRunner`/).and_raise("boom") + expect do + Spaceship::SpaceauthRunner.new.session_string + end.to raise_error("boom") + end + end end