├── .rspec ├── .gitignore ├── renovate.json ├── lib ├── autoprovision │ ├── common.rb │ ├── profile_info.rb │ ├── portal │ │ ├── common.rb │ │ ├── auth_client.rb │ │ ├── certificate_client.rb │ │ ├── device_client.rb │ │ ├── app_client.rb │ │ └── profile_client.rb │ ├── certificate_info.rb │ ├── utils.rb │ ├── auth_data.rb │ ├── device.rb │ ├── auth_helper.rb │ ├── keychain_helper.rb │ ├── profile_helper.rb │ ├── certificate_helper.rb │ └── project_helper.rb └── autoprovision.rb ├── Gemfile ├── spec ├── fixtures │ └── project │ │ ├── foo.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── godrei.xcuserdatad │ │ │ │ └── xcschemes │ │ │ │ ├── xcschememanagement.plist │ │ │ │ ├── foo.xcscheme │ │ │ │ ├── fooTests.xcscheme │ │ │ │ └── fooUITests.xcscheme │ │ └── project.pbxproj │ │ ├── foo │ │ ├── ViewController.swift │ │ ├── Info.plist │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── AppDelegate.swift │ │ ├── fooTests │ │ ├── Info.plist │ │ └── fooTests.swift │ │ └── fooUITests │ │ ├── Info.plist │ │ └── fooUITests.swift ├── project_helper_spec.rb ├── device_client_spec.rb └── spec_helper.rb ├── .rubocop.yml ├── docs └── contribution.md ├── release_config.yml ├── step.sh ├── log └── log.rb ├── LICENSE ├── CHANGELOG.md ├── bitrise.yml ├── .github └── workflows │ └── stale-issues-workflow.yml ├── params.rb ├── .rubocop_todo.yml ├── Gemfile.lock ├── step.rb ├── e2e └── bitrise.yml ├── README.md └── step.yml /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bitrise.* 2 | _tmp/ 3 | .idea 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>bitrise-steplib/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/autoprovision/common.rb: -------------------------------------------------------------------------------- 1 | def printable_response(response) 2 | [ 3 | 'response', 4 | "status: #{response.code}", 5 | "body: #{response.body}" 6 | ].join("\n") 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'fastlane' 4 | gem 'openssl' 5 | gem 'plist' 6 | gem 'xcodeproj' 7 | 8 | group :test do 9 | gem 'rspec' 10 | gem 'rubocop', '~> 1.18' 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/autoprovision/profile_info.rb: -------------------------------------------------------------------------------- 1 | # ProfileInfo 2 | class ProfileInfo 3 | attr_reader :path 4 | attr_reader :portal_profile 5 | 6 | def initialize(path, portal_profile) 7 | @path = path 8 | @portal_profile = portal_profile 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | - '_tmp/**/*' 6 | 7 | Style/FrozenStringLiteralComment: 8 | Enabled: False 9 | 10 | Style/NumericPredicate: 11 | Exclude: 12 | - 'lib/autoprovision/portal/profile_client.rb' 13 | -------------------------------------------------------------------------------- /docs/contribution.md: -------------------------------------------------------------------------------- 1 | **Note:** this step's end-to-end tests (defined in `e2e/bitrise.yml`) are working with secrets which are intentionally not stored in this repo. External contributors won't be able to run those tests. Don't worry, if you open a PR with your contribution, we will help with running tests and make sure that they pass. -------------------------------------------------------------------------------- /lib/autoprovision/portal/common.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | def preferred_error_message(ex) 4 | ex.preferred_error_info&.join(' ') || ex.to_s 5 | end 6 | 7 | def run_or_raise_preferred_error_message 8 | yield 9 | rescue Spaceship::Client::UnexpectedResponse => ex 10 | raise preferred_error_message(ex) 11 | end 12 | -------------------------------------------------------------------------------- /lib/autoprovision/certificate_info.rb: -------------------------------------------------------------------------------- 1 | # CertificateInfo 2 | class CertificateInfo 3 | attr_reader :path 4 | attr_reader :passphrase 5 | attr_reader :certificate 6 | attr_accessor :portal_certificate 7 | 8 | def initialize(path, passphrase, certificate) 9 | @path = path 10 | @passphrase = passphrase 11 | @certificate = certificate 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/autoprovision.rb: -------------------------------------------------------------------------------- 1 | require_relative 'autoprovision/auth_helper' 2 | require_relative 'autoprovision/certificate_helper' 3 | require_relative 'autoprovision/profile_helper' 4 | 5 | require_relative 'autoprovision/project_helper' 6 | require_relative 'autoprovision/keychain_helper' 7 | require_relative 'autoprovision/utils' 8 | 9 | require_relative 'autoprovision/portal/device_client' 10 | -------------------------------------------------------------------------------- /lib/autoprovision/utils.rb: -------------------------------------------------------------------------------- 1 | def certificate_common_name(certificate) 2 | common_name = certificate.subject.to_a.find { |name, _, _| name == 'CN' }[1] 3 | common_name = common_name.force_encoding('UTF-8') 4 | common_name 5 | end 6 | 7 | private 8 | 9 | def certificate_name_and_serial(certificate) 10 | "#{certificate_common_name(certificate)} [#{certificate.serial}]" 11 | end 12 | -------------------------------------------------------------------------------- /release_config.yml: -------------------------------------------------------------------------------- 1 | release: 2 | development_branch: master 3 | release_branch: master 4 | changelog: 5 | path: CHANGELOG.md 6 | content_template: |- 7 | {{range .ContentItems}}### {{.EndTaggedCommit.Tag}} ({{.EndTaggedCommit.Date.Format "2006 Jan 02"}}) 8 | 9 | {{range .Commits}}* [{{firstChars .Hash 7}}] {{.Message}} 10 | {{end}} 11 | {{end}} 12 | header_template: '## Changelog (Current version: {{.Version}})' 13 | footer_template: 'Updated: {{.CurrentDate.Format "2006 Jan 02"}}' 14 | -------------------------------------------------------------------------------- /step.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | 7 | gem install bundler -v 2.2.24 --force 8 | 9 | export BUNDLE_GEMFILE="$THIS_SCRIPT_DIR/Gemfile" 10 | 11 | set +e 12 | echo '$' "bundle install" 13 | out=$(bundle install) 14 | if [ $? != 0 ]; then 15 | echo "bundle install failed" 16 | echo $out 17 | exit 1 18 | fi 19 | set -e 20 | 21 | echo '$' "bundle exec ruby "$THIS_SCRIPT_DIR/step.rb"" 22 | bundle exec ruby "$THIS_SCRIPT_DIR/step.rb" -------------------------------------------------------------------------------- /spec/fixtures/project/foo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // foo 4 | // 5 | // Created by Krisztian Godrei on 2018. 03. 27.. 6 | // Copyright © 2018. Bitrise. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /lib/autoprovision/portal/auth_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | module Portal 4 | # AuthClient ... 5 | class AuthClient 6 | def self.login(username, password, two_factor_session = nil, team_id = nil) 7 | ENV['FASTLANE_SESSION'] = two_factor_session unless two_factor_session.to_s.empty? 8 | ENV['SPACESHIP_SKIP_2FA_UPGRADE'] = '1' 9 | 10 | client = Spaceship::Portal.login(username, password) 11 | 12 | if team_id.to_s.empty? 13 | teams = client.teams 14 | raise 'Your developer portal account belongs to multiple teams, please provide the team id to sign in' if teams.to_a.size > 1 15 | else 16 | client.team_id = team_id 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/xcuserdata/godrei.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | foo.xcscheme 8 | 9 | isShown 10 | 11 | 12 | fooTests.xcscheme 13 | 14 | isShown 15 | 16 | 17 | fooUITests.xcscheme 18 | 19 | isShown 20 | 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/fixtures/project/fooTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/fixtures/project/fooUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/autoprovision/auth_data.rb: -------------------------------------------------------------------------------- 1 | require_relative 'device' 2 | 3 | # AuthData 4 | class AuthData 5 | attr_reader :apple_id 6 | attr_reader :password 7 | attr_reader :session_cookies 8 | attr_reader :test_devices 9 | 10 | def initialize(auth_data) 11 | @apple_id = auth_data['apple_id'] 12 | @password = auth_data['password'] 13 | @session_cookies = auth_data['session_cookies'] 14 | 15 | @test_devices = [] 16 | test_devices_json = auth_data['test_devices'] 17 | test_devices_json.each { |device_data| @test_devices.push(Device.new(device_data)) } unless test_devices_json.to_s.empty? 18 | end 19 | 20 | def validate 21 | raise 'developer portal apple id not provided for this build' if @apple_id.to_s.empty? 22 | raise 'developer portal password not provided for this build' if @password.to_s.empty? 23 | @test_devices.each(&:validate) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /log/log.rb: -------------------------------------------------------------------------------- 1 | # Log 2 | class Log 3 | @verbose = true 4 | 5 | class << self 6 | attr_accessor :verbose 7 | end 8 | 9 | def self.info(str) 10 | puts("\n\e[34m#{str}\e[0m") 11 | end 12 | 13 | def self.print(str) 14 | puts(str.to_s) 15 | end 16 | 17 | def self.success(str) 18 | puts("\e[32m#{str}\e[0m") 19 | end 20 | 21 | def self.warn(str) 22 | puts("\e[33m#{str}\e[0m") 23 | end 24 | 25 | def self.error(str) 26 | puts("\e[31m#{str}\e[0m") 27 | end 28 | 29 | def self.debug(str) 30 | puts("\e[90m#{str}\e[0m") if @verbose 31 | end 32 | 33 | def self.debug_exception(exc) 34 | Log.debug('Error:') 35 | Log.debug(exc.to_s) 36 | puts 37 | Log.debug('Stacktrace (for debugging):') 38 | Log.debug(exc.backtrace.join("\n").to_s) 39 | end 40 | 41 | def self.secure_value(value) 42 | return '' if value.empty? 43 | '***' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Bitrise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/fixtures/project/fooTests/fooTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // fooTests.swift 3 | // fooTests 4 | // 5 | // Created by Krisztian Godrei on 2018. 03. 27.. 6 | // Copyright © 2018. Bitrise. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import foo 11 | 12 | class fooTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /spec/fixtures/project/fooUITests/fooUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // fooUITests.swift 3 | // fooUITests 4 | // 5 | // Created by Krisztian Godrei on 2018. 03. 27.. 6 | // Copyright © 2018. Bitrise. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class fooUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/autoprovision/device.rb: -------------------------------------------------------------------------------- 1 | # Device 2 | class Device 3 | attr_reader :udid 4 | attr_reader :name 5 | 6 | def initialize(device_data) 7 | @udid = device_data['device_identifier'] || '' 8 | @name = device_data['title'] || '' 9 | end 10 | 11 | def validate 12 | raise 'device udid not porvided this build' if @udid.empty? 13 | raise 'device title not provided for this build' if @name.empty? 14 | end 15 | 16 | def eql?(other) 17 | substituted_udid = @udid.sub(/[^0-9A-Za-z]/, '') 18 | other_substituted_udid = other.udid.sub(/[^0-9A-Za-z]/, '') 19 | substituted_udid == other_substituted_udid 20 | end 21 | 22 | def ===(other) 23 | substituted_udid = @udid.sub(/[^0-9A-Za-z]/, '') 24 | other_substituted_udid = other.udid.sub(/[^0-9A-Za-z]/, '') 25 | substituted_udid == other_substituted_udid 26 | end 27 | 28 | def ==(other) 29 | substituted_udid = @udid.sub(/[^0-9A-Za-z]/, '') 30 | other_substituted_udid = other.udid.sub(/[^0-9A-Za-z]/, '') 31 | substituted_udid == other_substituted_udid 32 | end 33 | 34 | def self.filter_duplicated_devices(devices) 35 | return devices if devices.to_a.empty? 36 | devices.uniq { |device| device.udid.sub(/[^0-9A-Za-z]/, '') } 37 | end 38 | 39 | def self.duplicated_device_groups(devices) 40 | return devices if devices.to_a.empty? 41 | groups = devices.group_by { |device| device.udid.sub(/[^0-9A-Za-z]/, '') }.values.select { |a| a.length > 1 } 42 | groups 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /lib/autoprovision/portal/certificate_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | require_relative 'common' 4 | 5 | module Portal 6 | # CertificateClient ... 7 | class CertificateClient 8 | def self.download_development_certificates 9 | development_certificates = [] 10 | run_or_raise_preferred_error_message { development_certificates = Spaceship::Portal.certificate.development.all } 11 | run_or_raise_preferred_error_message { development_certificates.concat(Spaceship::Portal.certificate.apple_development.all) } 12 | 13 | certificates = [] 14 | development_certificates.each do |cert| 15 | if cert.can_download 16 | certificates.push(cert) 17 | else 18 | Log.debug("development certificate: #{cert.name} is not downloadable, skipping...") 19 | end 20 | end 21 | 22 | certificates 23 | end 24 | 25 | def self.download_production_certificates 26 | production_certificates = [] 27 | run_or_raise_preferred_error_message { production_certificates = Spaceship::Portal.certificate.production.all } 28 | run_or_raise_preferred_error_message { production_certificates.concat(Spaceship::Portal.certificate.apple_distribution.all) } 29 | 30 | certificates = [] 31 | production_certificates.each do |cert| 32 | if cert.can_download 33 | certificates.push(cert) 34 | else 35 | Log.debug("production certificate: #{cert.name} is not downloadable, skipping...") 36 | end 37 | end 38 | 39 | if production_certificates.to_a.empty? 40 | run_or_raise_preferred_error_message { production_certificates = Spaceship::Portal.certificate.in_house.all } 41 | 42 | production_certificates.each do |cert| 43 | if cert.can_download 44 | certificates.push(cert) 45 | else 46 | Log.debug("production certificate: #{cert.name} is not downloadable, skipping...") 47 | end 48 | end 49 | end 50 | 51 | certificates 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog (Current version: 1.1.3) 2 | 3 | ----------------- 4 | 5 | ### 1.1.3 (2018 Jun 27) 6 | 7 | * [61efc50] Prepare for 1.1.3 8 | * [fe3acd4] target_attributes nil check (#34) 9 | 10 | ### 1.1.2 (2018 May 28) 11 | 12 | * [94e8ceb] prepare for 1.1.2 13 | * [8426a2a] step.yml update (#33) 14 | 15 | ### 1.1.1 (2018 May 28) 16 | 17 | * [d37bb4c] prepare for 1.1.1 18 | * [12e69b6] Gem update (#32) 19 | * [123fc49] added distribution type export (#30) 20 | 21 | ### 1.1.0 (2018 May 10) 22 | 23 | * [b4ff44e] prepare for release 24 | * [700a8a9] allow to generate profiles in case of xcode managed signing (#28) 25 | 26 | ### 1.0.3 (2018 May 08) 27 | 28 | * [bc8b8f1] Prepare for 1.0.3 29 | * [a06084d] check certificate expiration (#27) 30 | 31 | ### 1.0.2 (2018 Apr 23) 32 | 33 | * [86834cf] prepare for 1.0.2 34 | * [33c58d5] Disabled in CLI. (#26) 35 | * [0d0efc9] Team (#25) 36 | * [52249e4] Distinguish between App Store and Ad Hoc profiles (#24) 37 | 38 | ### 1.0.1 (2018 Mar 29) 39 | 40 | * [55f6bb9] prepare for 1.0.1 41 | * [058787c] Update (#22) 42 | 43 | ### 1.0.0 (2018 Mar 21) 44 | 45 | * [07a294e] prepare for 1.0.0 46 | * [4e66b7c] Team profile handling (#17) 47 | 48 | ### 0.9.7 (2018 Jan 24) 49 | 50 | * [73704d1] prepare for 0.9.7 51 | * [aeb6e23] Force settings (#15) 52 | 53 | ### 0.9.6 (2018 Jan 11) 54 | 55 | * [7864183] prepare for 0.9.6 56 | * [5f69688] use DevelopmentTeam target attribute if DEVELOPMENT_TEAM build settin… (#14) 57 | 58 | ### 0.9.5 (2017 Dec 22) 59 | 60 | * [e70b0b7] prepare for 0.9.5 61 | * [752c583] fix custom configuration (#12) 62 | 63 | ### 0.9.4 (2017 Dec 06) 64 | 65 | * [58bee2d] prepare for 0.9.4 66 | * [34345e1] test for custom configuration (#10) 67 | * [d82cd97] Resolved issue where to_a is used on wrong type (#9) 68 | * [803be57] Update (#8) 69 | 70 | ### 0.9.3 (2017 Dec 06) 71 | 72 | * [b79fe32] prepare for 0.9.3 73 | * [9a63bd5] Update (#7) 74 | * [309bb50] $CHILD_STATUS is nil without require 'English' (#6) 75 | 76 | ### 0.9.2 (2017 Nov 15) 77 | 78 | * [e4a08c1] prepare for 0.9.2 79 | * [6b549f6] check if certificate is downloadable (#4) 80 | 81 | ### 0.9.1 (2017 Nov 14) 82 | 83 | * [9f2d90e] gitignore update 84 | * [d2bbf21] prepare for 0.9.1 85 | * [6375881] fix if no 2fa cookies provided (#3) 86 | 87 | ----------------- 88 | 89 | Updated: 2018 Jun 27 -------------------------------------------------------------------------------- /spec/fixtures/project/foo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // foo 4 | // 5 | // Created by Krisztian Godrei on 2018. 03. 27.. 6 | // Copyright © 2018. Bitrise. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/xcuserdata/godrei.xcuserdatad/xcschemes/foo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 52 | 53 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /spec/project_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'xcodeproj/plist' 3 | require 'tmpdir' 4 | require 'fileutils' 5 | 6 | require_relative '../lib/autoprovision/project_helper.rb' 7 | 8 | def recreate_shared_schemes(project) 9 | schemes_dir = Xcodeproj::XCScheme.user_data_dir(project.path) 10 | FileUtils.rm_rf(schemes_dir) 11 | FileUtils.mkdir_p(schemes_dir) 12 | 13 | xcschememanagement = {} 14 | xcschememanagement['SchemeUserState'] = {} 15 | xcschememanagement['SuppressBuildableAutocreation'] = {} 16 | 17 | project.targets.each do |target| 18 | scheme = Xcodeproj::XCScheme.new 19 | scheme.add_build_target(target) 20 | scheme.add_test_target(target) if target.respond_to?(:test_target_type?) && target.test_target_type? 21 | yield scheme, target if block_given? 22 | scheme.save_as(project.path, target.name, true) 23 | xcschememanagement['SchemeUserState']["#{target.name}.xcscheme"] = {} 24 | xcschememanagement['SchemeUserState']["#{target.name}.xcscheme"]['isShown'] = true 25 | end 26 | 27 | xcschememanagement_path = schemes_dir + 'xcschememanagement.plist' 28 | Xcodeproj::Plist.write_to_path(xcschememanagement, xcschememanagement_path) 29 | end 30 | 31 | def test_project_dir 32 | src = './spec/fixtures/project' 33 | dst = Dir.mktmpdir('foo') 34 | FileUtils.copy_entry(src, dst) 35 | dst 36 | end 37 | 38 | RSpec.describe 'ProjectHelper' do 39 | let(:project_with_target_attributes) do 40 | path = File.join(test_project_dir, 'foo.xcodeproj') 41 | project = Xcodeproj::Project.open(path) 42 | recreate_shared_schemes(project) 43 | project 44 | end 45 | 46 | let(:project_without_target_attributes) do 47 | project = project_with_target_attributes 48 | project.root_object.attributes['TargetAttributes'] = nil 49 | project.save 50 | project 51 | end 52 | 53 | describe '#uses_xcode_auto_codesigning?' do 54 | subject { ProjectHelper.new(project.path, 'foo', '').uses_xcode_auto_codesigning? } 55 | 56 | context 'when new Xcode project uses auto signing with TargetAttributes' do 57 | let(:project) { project_with_target_attributes } 58 | it { is_expected.to eq true } 59 | end 60 | 61 | context 'when new Xcode project uses auto signing without TargetAttributes' do 62 | let(:project) { project_without_target_attributes } 63 | it { is_expected.to eq true } 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | format_version: "11" 2 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 3 | 4 | workflows: 5 | check: 6 | steps: 7 | - script: 8 | title: bundle install 9 | inputs: 10 | - content: |- 11 | #!/bin/bash 12 | set -e 13 | gem install bundler -v 2.2.24 --force 14 | bundle install 15 | - script: 16 | title: rubocop 17 | inputs: 18 | - content: |- 19 | #!/bin/bash 20 | set -e 21 | bundle exec rubocop 22 | - script: 23 | title: rspec 24 | inputs: 25 | - content: |- 26 | #!/bin/bash 27 | set -e 28 | bundle exec rspec 29 | 30 | e2e: 31 | steps: 32 | - git::https://github.com/bitrise-steplib/steps-check.git: 33 | inputs: 34 | - workflow: e2e 35 | 36 | sample: 37 | envs: 38 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-simple-objc.git 39 | - TEST_APP_BRANCH: master 40 | - BITRISE_PROJECT_PATH: ios-simple-objc/ios-simple-objc.xcodeproj 41 | - BITRISE_SCHEME: ios-simple-objc 42 | - DISTRIBUTION_TYPE: development 43 | - GENERATE_PROFILES: "no" 44 | - TEAM_ID: $TEAM_ID 45 | - BITRISE_CERTIFICATE_URL: $BITRISE_CERTIFICATE_URL_LIST 46 | - BITRISE_CERTIFICATE_PASSPHRASE: $BITRISE_CERTIFICATE_PASSPHRASE_LIST 47 | steps: 48 | - script: 49 | inputs: 50 | - content: |- 51 | #!/bin/env bash 52 | set -ex 53 | rm -rf ./_tmp 54 | - git::https://github.com/bitrise-steplib/bitrise-step-simple-git-clone.git: 55 | inputs: 56 | - repository_url: $TEST_APP_URL 57 | - branch: $TEST_APP_BRANCH 58 | - clone_into_dir: ./_tmp 59 | - path::./: 60 | title: Step Test 61 | run_if: true 62 | inputs: 63 | - certificate_urls: $BITRISE_CERTIFICATE_URL 64 | - passphrases: $BITRISE_CERTIFICATE_PASSPHRASE 65 | - team_id: $TEAM_ID 66 | - distribution_type: $DISTRIBUTION_TYPE 67 | - project_path: ./_tmp/$BITRISE_PROJECT_PATH 68 | - scheme: $BITRISE_SCHEME 69 | - verbose_log: "yes" 70 | - generate_profiles: $GENERATE_PROFILES 71 | 72 | generate_readme: 73 | steps: 74 | - git::https://github.com/bitrise-steplib/steps-readme-generator.git@main: 75 | inputs: 76 | - contrib_section: docs/contribution.md 77 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Stale Issues Workflow 2 | 3 | on: 4 | # Allows manually running 5 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#manual-events 6 | workflow_dispatch: 7 | 8 | schedule: 9 | # Runs at 08:00 UTC every day 10 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#schedule 11 | - cron: '0 8 * * *' 12 | 13 | jobs: 14 | stale: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # https://github.com/actions/stale 18 | - uses: actions/stale@v3 19 | with: 20 | repo-token: ${{ secrets.CORESTEPS_BOT_GITHUB_TOKEN }} 21 | # do not manage PRs 22 | days-before-pr-stale: -1 23 | days-before-pr-close: -1 24 | # stale issue config 25 | exempt-issue-labels: 'bug' 26 | days-before-issue-stale: 90 27 | days-before-issue-close: 21 28 | stale-issue-message: | 29 | Hello there, I'm a bot. On behalf of the community I thank you for opening this issue. 30 | 31 | To help our human contributors focus on the most relevant reports, I check up on old issues to see if they're still relevant. 32 | This issue has had no activity for 90 days, so I marked it as stale. 33 | 34 | The community would appreciate if you could check if the issue still persists. If it isn't, please close it. 35 | If the issue persists, and you'd like to remove the stale label, you simply need to leave a comment. Your comment can be as simple as "still important to me". 36 | 37 | If no comment left within 21 days, this issue will be closed. 38 | close-issue-message: > 39 | I'll close this issue as it doesn't seem to be relevant anymore. 40 | 41 | We believe an old issue probably has a bunch of context that's no longer relevant, therefore, if the problem still persists, please open a new issue. 42 | stale-issue-label: stale 43 | # https://github.com/jakejarvis/wait-action 44 | # Wait 1m to make sure lock-threads will actually lock the issue where stale just recently left a message. 45 | - uses: jakejarvis/wait-action@master 46 | with: 47 | time: '1m' 48 | # https://github.com/dessant/lock-threads 49 | - uses: dessant/lock-threads@v2 50 | with: 51 | github-token: ${{ secrets.CORESTEPS_BOT_GITHUB_TOKEN }} 52 | # do not manage PRs 53 | process-only: issues 54 | # stale issue config 55 | issue-lock-inactive-days: 0 # immediately lock closed issues 56 | issue-lock-reason: '' 57 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/xcuserdata/godrei.xcuserdatad/xcschemes/fooTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 57 | 58 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/xcuserdata/godrei.xcuserdatad/xcschemes/fooUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 57 | 58 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/autoprovision/auth_helper.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | require_relative 'common' 4 | require_relative 'auth_data' 5 | require_relative 'portal/auth_client' 6 | 7 | # AuthHelper ... 8 | class AuthHelper 9 | COOKIE_TEMPLATE = '- !ruby/object:HTTP::Cookie 10 | name: 11 | value: 12 | domain: 13 | for_domain: 14 | path: "" 15 | '.freeze 16 | 17 | attr_reader :test_devices 18 | 19 | def login(build_url, build_api_token, team_id) 20 | portal_data = get_developer_portal_data(build_url, build_api_token) 21 | portal_data.validate 22 | 23 | @test_devices = portal_data.test_devices 24 | 25 | two_factor_session = convert_des_cookie(portal_data.session_cookies) 26 | Portal::AuthClient.login(portal_data.apple_id, portal_data.password, two_factor_session, team_id) 27 | end 28 | 29 | private 30 | 31 | def get_developer_portal_data(build_url, build_api_token) 32 | portal_data_json = ENV['BITRISE_PORTAL_DATA_JSON'] 33 | unless portal_data_json.nil? 34 | developer_portal_data = JSON.parse(portal_data_json) 35 | return AuthData.new(developer_portal_data) 36 | end 37 | 38 | url = "#{build_url}/apple_developer_portal_data.json" 39 | Log.debug("developer portal data url: #{url}") 40 | Log.debug("build_api_token: #{build_api_token}") 41 | uri = URI.parse(url) 42 | 43 | request = Net::HTTP::Get.new(uri) 44 | request['BUILD_API_TOKEN'] = build_api_token 45 | 46 | http_object = Net::HTTP.new(uri.host, uri.port) 47 | http_object.use_ssl = true 48 | 49 | response = nil 50 | 4.times do |i| 51 | response = http_object.start do |http| 52 | http.request(request) 53 | end 54 | 55 | if response == Net::HTTPServerError 56 | Log.debug("Request failed, retrying. (response: #{response})") 57 | sleep(i**2) 58 | next 59 | end 60 | 61 | break 62 | end 63 | 64 | raise 'failed to get response' unless response 65 | raise 'response has no body' unless response.body 66 | 67 | developer_portal_data = JSON.parse(response.body) 68 | 69 | unless response.code == '200' 70 | error_message = developer_portal_data['error_msg'] 71 | error_message ||= printable_response(response) 72 | raise error_message 73 | end 74 | 75 | AuthData.new(developer_portal_data) 76 | end 77 | 78 | def convert_des_cookie(cookies_json_str) 79 | Log.debug("session cookie: #{cookies_json_str}") 80 | 81 | converted_cookies = '' 82 | 83 | cookies_json_str.each_value do |cookies| 84 | cookies.each do |cookie| 85 | name = cookie['name'].to_s 86 | value = cookie['value'].to_s 87 | domain = cookie['domain'].to_s 88 | for_domain = cookie['for_domain'] || 'true' 89 | path = cookie['path'].to_s 90 | 91 | converted_cookie = COOKIE_TEMPLATE.sub('', name).sub('', value).sub('', domain).sub('', for_domain).sub('', path) 92 | 93 | converted_cookies = '---' + "\n" if converted_cookies.empty? 94 | converted_cookies += converted_cookie + "\n" 95 | end 96 | end 97 | 98 | Log.debug("converted session cookies:\n#{converted_cookies}") 99 | converted_cookies 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /params.rb: -------------------------------------------------------------------------------- 1 | # Params 2 | class Params 3 | attr_accessor :build_url 4 | attr_accessor :build_api_token 5 | attr_accessor :team_id 6 | attr_accessor :certificate_urls_str 7 | attr_accessor :passphrases_str 8 | attr_accessor :distribution_type 9 | attr_accessor :register_test_devices 10 | attr_accessor :min_profile_days_valid 11 | attr_accessor :project_path 12 | attr_accessor :scheme 13 | attr_accessor :configuration 14 | attr_accessor :keychain_path 15 | attr_accessor :keychain_password 16 | attr_accessor :verbose_log 17 | attr_accessor :generate_profiles 18 | attr_accessor :certificate_urls 19 | attr_accessor :passphrases 20 | 21 | def initialize 22 | @build_url = ENV['build_url'] 23 | @build_api_token = ENV['build_api_token'] 24 | @team_id = ENV['team_id'] 25 | @certificate_urls_str = ENV['certificate_urls'] 26 | @register_test_devices = ENV['register_test_devices'] 27 | @min_profile_days_valid = ENV['min_profile_days_valid'].to_i 28 | @passphrases_str = ENV['passphrases'] 29 | @distribution_type = ENV['distribution_type'] 30 | @project_path = ENV['project_path'] 31 | @scheme = ENV['scheme'] 32 | @configuration = ENV['configuration'] 33 | @keychain_path = ENV['keychain_path'] 34 | @keychain_password = ENV['keychain_password'] 35 | @verbose_log = ENV['verbose_log'] 36 | @generate_profiles = ENV['generate_profiles'] 37 | 38 | @certificate_urls = split_pipe_separated_list(@certificate_urls_str) 39 | @passphrases = split_pipe_separated_list(@passphrases_str) 40 | end 41 | 42 | def print 43 | Log.info('Params:') 44 | Log.print("team_id: #{@team_id}") 45 | Log.print("certificate_urls: #{Log.secure_value(@certificate_urls_str)}") 46 | Log.print("register_test_devices: #{@register_test_devices}") 47 | Log.print("min_profile_days_valid: #{@min_profile_days_valid}") 48 | Log.print("passphrases: #{Log.secure_value(@passphrases_str)}") 49 | Log.print("distribution_type: #{@distribution_type}") 50 | Log.print("project_path: #{@project_path}") 51 | Log.print("scheme: #{@scheme}") 52 | Log.print("configuration: #{@configuration}") 53 | Log.print("build_url: #{@build_url}") 54 | Log.print("build_api_token: #{Log.secure_value(@build_api_token)}") 55 | Log.print("keychain_path: #{@keychain_path}") 56 | Log.print("keychain_password: #{Log.secure_value(@keychain_password)}") 57 | Log.print("verbose_log: #{@verbose_log}") 58 | Log.print("generate_profiles: #{@generate_profiles}") 59 | end 60 | 61 | def validate 62 | raise 'missing: build_url' if @build_url.to_s.empty? 63 | raise 'missing: build_api_token' if @build_api_token.to_s.empty? 64 | raise 'missing: certificate_urls' if @certificate_urls_str.to_s.empty? 65 | raise 'missing: distribution_type' if @distribution_type.to_s.empty? 66 | raise 'missing: project_path' if @project_path.to_s.empty? 67 | raise 'missing: scheme' if @scheme.to_s.empty? 68 | raise 'missing: keychain_path' if @keychain_path.to_s.empty? 69 | raise 'missing: keychain_password' if @keychain_password.to_s.empty? 70 | raise 'missing: verbose_log' if @verbose_log.to_s.empty? 71 | raise 'missing: generate_profiles' if @generate_profiles.to_s.empty? 72 | end 73 | 74 | private 75 | 76 | def split_pipe_separated_list(list) 77 | return [''] if list.to_s.empty? 78 | list.split('|', -1) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/autoprovision/keychain_helper.rb: -------------------------------------------------------------------------------- 1 | require 'English' 2 | 3 | # KeychainHelper 4 | class KeychainHelper 5 | def self.create_keychain(keychain_path, keychain_password) 6 | cmd = ['security', '-v', 'create-keychain', '-p', keychain_password, "\"#{keychain_path}\""].join(' ') 7 | Log.debug("$ #{cmd}") 8 | out = `#{cmd}` 9 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 10 | end 11 | 12 | def initialize(keychain_path, keychain_password) 13 | if File.file?(keychain_path) 14 | @keychain_path = keychain_path 15 | @keychain_password = keychain_password 16 | return 17 | end 18 | 19 | new_keychain_path = keychain_path + '-db' 20 | if File.file?(new_keychain_path) 21 | @keychain_path = new_keychain_path 22 | @keychain_password = keychain_password 23 | return 24 | end 25 | 26 | KeychainHelper.create_keychain(keychain_path, keychain_password) 27 | @keychain_path = keychain_path 28 | @keychain_password = keychain_password 29 | end 30 | 31 | def install_certificates(certificate_passphrase_map) 32 | unlock_keychain 33 | 34 | certificate_passphrase_map.each do |path, passphrase| 35 | import_certificate(path, passphrase) 36 | end 37 | 38 | set_key_partition_list_if_needed 39 | set_keychain_settings_default_lock 40 | add_to_keychain_search_path 41 | set_default_keychain 42 | end 43 | 44 | private 45 | 46 | def import_certificate(path, passphrase) 47 | cmd_params = ['security', 'import', "\"#{path}\"", '-k', "\'#{@keychain_path}\'", '-P', "\"#{passphrase}\"", '-A'] 48 | debug_params = cmd_params.dup 49 | debug_params[6] = '"****"' 50 | debug_cmd = debug_params.join(' ') 51 | Log.debug("$ #{debug_cmd}") 52 | cmd = cmd_params.join(' ') 53 | # run command 54 | out = `#{cmd}` 55 | raise "#{debug_cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 56 | end 57 | 58 | def set_key_partition_list_if_needed 59 | # This is new behavior in Sierra, [openradar](https://openradar.appspot.com/28524119) 60 | # You need to use "security set-key-partition-list -S apple-tool:,apple: -k keychainPass keychainName" after importing the item and before attempting to use it via codesign. 61 | cmd = ['sw_vers', '-productVersion'].join(' ') 62 | Log.debug("$ #{cmd}") 63 | current_version = `#{cmd}` 64 | raise "#{cmd} failed, out: #{current_version}" unless $CHILD_STATUS.success? 65 | 66 | return if Gem::Version.new(current_version) < Gem::Version.new('10.12.0') 67 | 68 | cmd = ['security', 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-k', @keychain_password, "\"#{@keychain_path}\""].join(' ') 69 | Log.debug("$ #{cmd}") 70 | out = `#{cmd}` 71 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 72 | end 73 | 74 | def set_keychain_settings_default_lock 75 | cmd = ['security', '-v', 'set-keychain-settings', '-lut', '72000', "\"#{@keychain_path}\""].join(' ') 76 | Log.debug("$ #{cmd}") 77 | out = `#{cmd}` 78 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 79 | end 80 | 81 | def list_keychains 82 | cmd = ['security', 'list-keychains'].join(' ') 83 | Log.debug("$ #{cmd}") 84 | list = `#{cmd}` 85 | raise "#{cmd} failed, out: #{list}" unless $CHILD_STATUS.success? 86 | 87 | list.split("\n").map(&:strip).map { |e| e.gsub!(/\A"|"\Z/, '') } 88 | end 89 | 90 | def add_to_keychain_search_path 91 | keychains = Set.new(list_keychains).add("\"#{@keychain_path}\"").to_a 92 | cmd = ['security', '-v', 'list-keychains', '-s'].concat(keychains).join(' ') 93 | Log.debug("$ #{cmd}") 94 | out = `#{cmd}` 95 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 96 | end 97 | 98 | def set_default_keychain 99 | cmd = ['security', '-v', 'default-keychain', '-s', "\"#{@keychain_path}\""].join(' ') 100 | Log.debug("$ #{cmd}") 101 | out = `#{cmd}` 102 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 103 | end 104 | 105 | def unlock_keychain 106 | cmd = ['security', '-v', 'unlock-keychain', '-p', @keychain_password, "\"#{@keychain_path}\""].join(' ') 107 | Log.debug("$ #{cmd}") 108 | out = `#{cmd}` 109 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/autoprovision/portal/device_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | require_relative 'common' 4 | 5 | module Portal 6 | # DeviceClient ... 7 | class DeviceClient 8 | def self.ensure_test_devices(test_devices, platform, device_client = Spaceship::Portal.device) 9 | Log.info('Fetching Apple Developer Portal devices') 10 | dev_portal_devices = fetch_registered_devices(device_client) 11 | 12 | Log.print("#{dev_portal_devices.length} devices are registered on the Apple Developer Portal") 13 | dev_portal_devices.each do |dev_portal_device| 14 | Log.debug("- #{dev_portal_device.name}, #{dev_portal_device.device_type}, UDID (#{dev_portal_device.udid})") 15 | end 16 | 17 | unless test_devices.empty? 18 | unique_test_devices = Device.filter_duplicated_devices(test_devices) 19 | 20 | Log.info("Checking if #{unique_test_devices.length} Bitrise test device(s) are registered on Developer Portal") 21 | unique_test_devices.each do |test_device| 22 | Log.debug("- #{test_device.name}, UDID (#{test_device.udid})") 23 | end 24 | 25 | duplicated_devices_groups = Device.duplicated_device_groups(test_devices) 26 | unless duplicated_devices_groups.to_a.empty? 27 | Log.warn('Devices with duplicated UDID are registered on Bitrise, will be ignored:') 28 | duplicated_devices_groups.each do |duplicated_devices| 29 | Log.warn("- #{duplicated_devices.map(&:udid).join(' - ')}") 30 | end 31 | end 32 | 33 | new_dev_portal_devices = register_missing_test_devices(device_client, unique_test_devices, dev_portal_devices) 34 | dev_portal_devices = dev_portal_devices.concat(new_dev_portal_devices) 35 | end 36 | 37 | filter_dev_portal_devices(dev_portal_devices, platform) 38 | end 39 | 40 | def self.filter_dev_portal_devices(dev_portal_devices, platform) 41 | filtered_devices = [] 42 | dev_portal_devices.each do |dev_portal_device| 43 | if %i[ios watchos].include?(platform) 44 | filtered_devices = filtered_devices.append(dev_portal_device) if %w[watch ipad iphone ipod].include?(dev_portal_device.device_type) 45 | elsif platform == :tvos 46 | filtered_devices = filtered_devices.append(dev_portal_device) if dev_portal_device.device_type == 'tvOS' 47 | end 48 | end 49 | filtered_devices 50 | end 51 | 52 | def self.find_dev_portal_device(test_device, dev_portal_devices) 53 | device = nil 54 | dev_portal_devices.each do |dev_portal_device| 55 | if test_device.udid == dev_portal_device.udid 56 | device = dev_portal_device 57 | break 58 | end 59 | end 60 | device 61 | end 62 | 63 | def self.register_missing_test_devices(device_client = Spaceship::Portal.device, test_devices, dev_portal_devices) 64 | new_dev_portal_devices = [] 65 | 66 | test_devices.each do |test_device| 67 | Log.print("checking if the device (#{test_device.udid}) is registered") 68 | 69 | dev_portal_device = find_dev_portal_device(test_device, dev_portal_devices) 70 | unless dev_portal_device.nil? 71 | Log.print('device already registered') 72 | next 73 | end 74 | 75 | Log.print('registering device') 76 | new_dev_portal_device = register_test_device_on_dev_portal(device_client, test_device) 77 | new_dev_portal_devices.append(new_dev_portal_device) unless new_dev_portal_device.nil? 78 | end 79 | 80 | new_dev_portal_devices 81 | end 82 | 83 | def self.register_test_device_on_dev_portal(device_client = Spaceship::Portal.device, test_device) 84 | device_client.create!(name: test_device.name, udid: test_device.udid) 85 | rescue Spaceship::Client::UnexpectedResponse => ex 86 | message = preferred_error_message(ex) 87 | Log.warn("Failed to register device with name: #{test_device.name} udid: #{test_device.udid} error: #{message}") 88 | nil 89 | rescue 90 | Log.warn("Failed to register device with name: #{test_device.name} udid: #{test_device.udid}") 91 | nil 92 | end 93 | 94 | def self.fetch_registered_devices(device_client = Spaceship::Portal.device) 95 | devices = nil 96 | run_or_raise_preferred_error_message { devices = device_client.all(mac: false, include_disabled: false) || [] } 97 | devices 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/autoprovision/profile_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative 'profile_info' 2 | require_relative 'portal/profile_client' 3 | require_relative 'portal/app_client' 4 | 5 | # ProfileHelper ... 6 | class ProfileHelper 7 | def initialize(project_helper, certificate_helper) 8 | @project_helper = project_helper 9 | @certificate_helper = certificate_helper 10 | 11 | @profiles = {} 12 | end 13 | 14 | def ensure_profiles(distribution_types, test_devices, generate_profiles = false, min_profile_days_valid = 0) 15 | if @project_helper.uses_xcode_auto_codesigning? && generate_profiles 16 | Log.warn('project uses Xcode managed signing, but generate_profiles set to true, trying to generate Provisioning Profiles') 17 | 18 | begin 19 | distribution_types.each { |distr_type| ensure_manual_profiles(distr_type, @project_helper.platform, min_profile_days_valid, test_devices) } 20 | rescue => ex 21 | Log.error('generate_profiles set to true, but failed to generate Provisioning Profiles with error:') 22 | Log.error(ex.to_s) 23 | Log.info("\nTrying to use Xcode managed Provisioning Profiles") 24 | 25 | ensure_profiles(distribution_types, test_devices, false, min_profile_days_valid) 26 | end 27 | 28 | return false 29 | end 30 | 31 | distribution_types.each do |distr_type| 32 | if @project_helper.uses_xcode_auto_codesigning? 33 | ensure_xcode_managed_profiles(distr_type, @project_helper.platform, test_devices, min_profile_days_valid) 34 | else 35 | ensure_manual_profiles(distr_type, @project_helper.platform, min_profile_days_valid, test_devices) 36 | end 37 | end 38 | 39 | @project_helper.uses_xcode_auto_codesigning? 40 | end 41 | 42 | def profiles_by_bundle_id(distribution_type) 43 | @profiles[distribution_type] 44 | end 45 | 46 | private 47 | 48 | def ensure_xcode_managed_profiles(distribution_type, platform, test_devices, min_profile_days_valid = 0) 49 | certificate = @certificate_helper.certificate_info(distribution_type).portal_certificate 50 | 51 | targets = @project_helper.targets 52 | targets.each do |target| 53 | target_name = target.name 54 | bundle_id = @project_helper.target_bundle_id(target_name) 55 | entitlements = @project_helper.target_entitlements(target_name) || {} 56 | 57 | Log.print("checking xcode managed #{distribution_type} profile for target: #{target_name} (#{bundle_id}) with #{entitlements.length} services on developer portal") 58 | portal_profile = Portal::ProfileClient.ensure_xcode_managed_profile(bundle_id, entitlements, distribution_type, certificate, platform, test_devices, min_profile_days_valid) 59 | 60 | Log.print("downloading #{distribution_type} profile: #{portal_profile.name}") 61 | profile_path = write_profile(portal_profile) 62 | Log.debug("profile path: #{profile_path}") 63 | 64 | profile_info = ProfileInfo.new(profile_path, portal_profile) 65 | @profiles[distribution_type] ||= {} 66 | @profiles[distribution_type][bundle_id] = profile_info 67 | end 68 | end 69 | 70 | def ensure_manual_profiles(distribution_type, platform, min_profile_days_valid, test_devices) 71 | certificate = @certificate_helper.certificate_info(distribution_type).portal_certificate 72 | 73 | targets = @project_helper.targets 74 | targets.each do |target| 75 | target_name = target.name 76 | bundle_id = @project_helper.target_bundle_id(target_name) 77 | entitlements = @project_helper.target_entitlements(target_name) || {} 78 | 79 | Log.print("checking app for target: #{target_name} (#{bundle_id}) on developer portal") 80 | app = Portal::AppClient.ensure_app(bundle_id) 81 | 82 | Log.debug('sync app services') 83 | app = Portal::AppClient.sync_app_services(app, entitlements) 84 | 85 | Log.print("ensure #{distribution_type} profile for target: #{target_name} on developer portal") 86 | portal_profile = Portal::ProfileClient.ensure_manual_profile(certificate, app, entitlements, distribution_type, platform, min_profile_days_valid, test_devices) 87 | 88 | Log.print("downloading #{distribution_type} profile: #{portal_profile.name}") 89 | profile_path = write_profile(portal_profile) 90 | Log.debug("profile path: #{profile_path}") 91 | 92 | profile_info = ProfileInfo.new(profile_path, portal_profile) 93 | @profiles[distribution_type] ||= {} 94 | @profiles[distribution_type][bundle_id] = profile_info 95 | end 96 | end 97 | 98 | def write_profile(profile) 99 | home_dir = ENV['HOME'] 100 | raise 'failed to determine xcode provisioning profiles dir: $HOME not set' if home_dir.to_s.empty? 101 | 102 | profiles_dir = File.join(home_dir, 'Library/MobileDevice/Provisioning Profiles') 103 | FileUtils.mkdir_p(profiles_dir) unless File.directory?(profiles_dir) 104 | 105 | profile_path = File.join(profiles_dir, profile.uuid + '.mobileprovision') 106 | Log.warn("profile already exists at: #{profile_path}, overwriting...") if File.file?(profile_path) 107 | 108 | File.write(profile_path, profile.download) 109 | profile_path 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/device_client_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/autoprovision/portal/device_client' 2 | require_relative '../lib/autoprovision/device' 3 | require_relative '../log/log' 4 | 5 | RSpec.describe '.ensure_test_devices' do 6 | it 'returns empty array if no test devices provided and no registered devices found on Apple developer Portal' do 7 | fake_portal_client = double 8 | allow(fake_portal_client).to receive(:all).and_return(nil) 9 | 10 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices([], :ios, fake_portal_client) 11 | 12 | expect(dev_portal_devices).to eq([]) 13 | end 14 | 15 | it 'returns devices from Apple Developer Portal' do 16 | fake_portal_device = double 17 | allow(fake_portal_device).to receive(:name).and_return('Device on Developer Portal') 18 | allow(fake_portal_device).to receive(:udid).and_return('1234') 19 | allow(fake_portal_device).to receive(:device_type).and_return('iphone') 20 | 21 | fake_portal_client = double 22 | allow(fake_portal_client).to receive(:all).and_return([fake_portal_device]) 23 | 24 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices([], :ios, fake_portal_client) 25 | 26 | expect(dev_portal_devices).to eq([fake_portal_device]) 27 | end 28 | 29 | it 'raises exception if test_devices is nil' do 30 | fake_portal_client = double 31 | allow(fake_portal_client).to receive(:all).and_return(nil) 32 | 33 | expect { Portal::DeviceClient.ensure_test_devices(nil, :ios, fake_portal_client) }.to raise_error(NoMethodError) 34 | end 35 | 36 | it 'registers new device' do 37 | device = Device.new( 38 | 'device_identifier' => '123456', 39 | 'title' => 'New Device' 40 | ) 41 | 42 | fake_portal_device = double 43 | allow(fake_portal_device).to receive(:name).and_return(device.name) 44 | allow(fake_portal_device).to receive(:udid).and_return(device.udid) 45 | allow(fake_portal_device).to receive(:device_type).and_return('iphone') 46 | 47 | fake_portal_client = double 48 | allow(fake_portal_client).to receive(:all).and_return(nil) 49 | allow(fake_portal_client).to receive(:create!).and_return(fake_portal_device) 50 | 51 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices([device], :ios, fake_portal_client) 52 | dev_portal_device_udids = dev_portal_devices.map(&:udid) 53 | test_device_udids = [device].map(&:udid) 54 | 55 | expect(dev_portal_device_udids).to eq(test_device_udids) 56 | end 57 | 58 | it 'suppresses error due to invalid or mac device UDID' do 59 | existing_device = Device.new( 60 | 'device_identifier' => '123456', 61 | 'title' => 'Existing Device' 62 | ) 63 | invalid_device = Device.new( 64 | 'device_identifier' => 'invalid-udid', 65 | 'title' => 'Invalid Device' 66 | ) 67 | 68 | fake_portal_device = double 69 | allow(fake_portal_device).to receive(:name).and_return(existing_device.name) 70 | allow(fake_portal_device).to receive(:udid).and_return(existing_device.udid) 71 | allow(fake_portal_device).to receive(:device_type).and_return('iphone') 72 | 73 | fake_portal_client = double 74 | allow(fake_portal_client).to receive(:all).and_return([fake_portal_device]) 75 | allow(fake_portal_client).to receive(:create!).and_raise('error') 76 | 77 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices([existing_device, invalid_device], :ios, fake_portal_client) 78 | dev_portal_device_udids = dev_portal_devices.map(&:udid) 79 | test_device_udids = [existing_device].map(&:udid) 80 | 81 | expect(dev_portal_device_udids).to eq(test_device_udids) 82 | end 83 | 84 | [ 85 | [:ios, 'watch', 1], 86 | [:ios, 'ipad', 1], 87 | [:ios, 'iphone', 1], 88 | [:ios, 'ipod', 1], 89 | [:ios, 'tvOS', 0], 90 | 91 | [:osx, 'watch', 0], 92 | [:osx, 'ipad', 0], 93 | [:osx, 'iphone', 0], 94 | [:osx, 'ipod', 0], 95 | [:osx, 'tvOS', 0], 96 | 97 | [:tvos, 'watch', 0], 98 | [:tvos, 'ipad', 0], 99 | [:tvos, 'iphone', 0], 100 | [:tvos, 'ipod', 0], 101 | [:tvos, 'tvOS', 1], 102 | 103 | [:watchos, 'watch', 1], 104 | [:watchos, 'ipad', 1], 105 | [:watchos, 'iphone', 1], 106 | [:watchos, 'ipod', 1], 107 | [:watchos, 'tvOS', 0], 108 | ].each do |platform, device_type, len| 109 | it "on #{platform} platform with #{device_type} device valid devices length should be #{len}" do 110 | device = Device.new( 111 | 'device_identifier' => '123456', 112 | 'title' => 'New Device' 113 | ) 114 | 115 | fake_portal_device = double 116 | allow(fake_portal_device).to receive(:name).and_return(device.name) 117 | allow(fake_portal_device).to receive(:udid).and_return(device.udid) 118 | allow(fake_portal_device).to receive(:device_type).and_return(device_type) 119 | 120 | fake_portal_client = double 121 | allow(fake_portal_client).to receive(:all).and_return(nil) 122 | allow(fake_portal_client).to receive(:create!).and_return(fake_portal_device) 123 | 124 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices([device], platform, fake_portal_client) 125 | 126 | expect(dev_portal_devices.length).to eq(len) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # This setting enables warnings. It's recommended, but in some cases may 70 | # be too noisy due to issues in dependencies. 71 | config.warnings = true 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-08-04 07:44:02 UTC using RuboCop version 1.18.4. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 13 10 | # Cop supports --auto-correct. 11 | Layout/EmptyLineAfterGuardClause: 12 | Exclude: 13 | - 'lib/autoprovision/auth_data.rb' 14 | - 'lib/autoprovision/certificate_helper.rb' 15 | - 'lib/autoprovision/device.rb' 16 | - 'lib/autoprovision/portal/app_client.rb' 17 | - 'lib/autoprovision/project_helper.rb' 18 | - 'log/log.rb' 19 | - 'params.rb' 20 | 21 | # Offense count: 42 22 | # Cop supports --auto-correct. 23 | # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. 24 | # SupportedHashRocketStyles: key, separator, table 25 | # SupportedColonStyles: key, separator, table 26 | # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit 27 | Layout/HashAlignment: 28 | Exclude: 29 | - 'lib/autoprovision/portal/app_client.rb' 30 | 31 | # Offense count: 1 32 | # Cop supports --auto-correct. 33 | # Configuration parameters: Width, IgnoredPatterns. 34 | Layout/IndentationWidth: 35 | Exclude: 36 | - 'log/log.rb' 37 | 38 | # Offense count: 1 39 | # Cop supports --auto-correct. 40 | # Configuration parameters: AllowInHeredoc. 41 | Layout/TrailingWhitespace: 42 | Exclude: 43 | - 'spec/device_client_spec.rb' 44 | 45 | # Offense count: 29 46 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. 47 | Metrics/AbcSize: 48 | Max: 64 49 | 50 | # Offense count: 2 51 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 52 | # IgnoredMethods: refine 53 | Metrics/BlockLength: 54 | Max: 97 55 | 56 | # Offense count: 4 57 | # Configuration parameters: CountComments, CountAsOne. 58 | Metrics/ClassLength: 59 | Max: 331 60 | 61 | # Offense count: 15 62 | # Configuration parameters: IgnoredMethods. 63 | Metrics/CyclomaticComplexity: 64 | Max: 22 65 | 66 | # Offense count: 36 67 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 68 | Metrics/MethodLength: 69 | Max: 61 70 | 71 | # Offense count: 2 72 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. 73 | Metrics/ParameterLists: 74 | Max: 8 75 | 76 | # Offense count: 14 77 | # Configuration parameters: IgnoredMethods. 78 | Metrics/PerceivedComplexity: 79 | Max: 24 80 | 81 | # Offense count: 1 82 | # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. 83 | # AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to 84 | Naming/MethodParameterName: 85 | Exclude: 86 | - 'lib/autoprovision/portal/common.rb' 87 | 88 | # Offense count: 7 89 | # Cop supports --auto-correct. 90 | # Configuration parameters: PreferredName. 91 | Naming/RescuedExceptionsVariableName: 92 | Exclude: 93 | - 'lib/autoprovision/portal/common.rb' 94 | - 'lib/autoprovision/portal/device_client.rb' 95 | - 'lib/autoprovision/portal/profile_client.rb' 96 | - 'lib/autoprovision/profile_helper.rb' 97 | - 'step.rb' 98 | 99 | # Offense count: 33 100 | # Cop supports --auto-correct. 101 | # Configuration parameters: EnforcedStyle. 102 | # SupportedStyles: separated, grouped 103 | Style/AccessorGrouping: 104 | Exclude: 105 | - 'lib/autoprovision/auth_data.rb' 106 | - 'lib/autoprovision/certificate_helper.rb' 107 | - 'lib/autoprovision/certificate_info.rb' 108 | - 'lib/autoprovision/device.rb' 109 | - 'lib/autoprovision/profile_info.rb' 110 | - 'lib/autoprovision/project_helper.rb' 111 | - 'params.rb' 112 | 113 | # Offense count: 1 114 | # Cop supports --auto-correct. 115 | Style/BlockComments: 116 | Exclude: 117 | - 'spec/spec_helper.rb' 118 | 119 | # Offense count: 2 120 | # Cop supports --auto-correct. 121 | Style/CaseLikeIf: 122 | Exclude: 123 | - 'lib/autoprovision/portal/app_client.rb' 124 | 125 | # Offense count: 1 126 | # Configuration parameters: MinBodyLength. 127 | Style/GuardClause: 128 | Exclude: 129 | - 'lib/autoprovision/certificate_helper.rb' 130 | 131 | # Offense count: 2 132 | # Cop supports --auto-correct. 133 | Style/IfUnlessModifier: 134 | Exclude: 135 | - 'lib/autoprovision/certificate_helper.rb' 136 | 137 | # Offense count: 3 138 | # Configuration parameters: AllowedMethods. 139 | # AllowedMethods: respond_to_missing? 140 | Style/OptionalBooleanParameter: 141 | Exclude: 142 | - 'lib/autoprovision/portal/profile_client.rb' 143 | - 'lib/autoprovision/profile_helper.rb' 144 | 145 | # Offense count: 2 146 | # Cop supports --auto-correct. 147 | Style/RedundantAssignment: 148 | Exclude: 149 | - 'lib/autoprovision/device.rb' 150 | - 'lib/autoprovision/utils.rb' 151 | 152 | # Offense count: 1 153 | # Cop supports --auto-correct. 154 | Style/RedundantFileExtensionInRequire: 155 | Exclude: 156 | - 'spec/project_helper_spec.rb' 157 | 158 | # Offense count: 3 159 | # Cop supports --auto-correct. 160 | Style/RedundantSelfAssignment: 161 | Exclude: 162 | - 'lib/autoprovision/portal/device_client.rb' 163 | 164 | # Offense count: 6 165 | # Cop supports --auto-correct. 166 | # Configuration parameters: EnforcedStyle. 167 | # SupportedStyles: implicit, explicit 168 | Style/RescueStandardError: 169 | Exclude: 170 | - 'lib/autoprovision/portal/device_client.rb' 171 | - 'lib/autoprovision/portal/profile_client.rb' 172 | - 'lib/autoprovision/profile_helper.rb' 173 | - 'step.rb' 174 | 175 | # Offense count: 6 176 | # Cop supports --auto-correct. 177 | # Configuration parameters: Mode. 178 | Style/StringConcatenation: 179 | Exclude: 180 | - 'lib/autoprovision/auth_helper.rb' 181 | - 'lib/autoprovision/keychain_helper.rb' 182 | - 'lib/autoprovision/profile_helper.rb' 183 | - 'lib/autoprovision/project_helper.rb' 184 | - 'spec/project_helper_spec.rb' 185 | 186 | # Offense count: 1 187 | # Cop supports --auto-correct. 188 | # Configuration parameters: EnforcedStyleForMultiline. 189 | # SupportedStylesForMultiline: comma, consistent_comma, no_comma 190 | Style/TrailingCommaInArrayLiteral: 191 | Exclude: 192 | - 'spec/device_client_spec.rb' 193 | 194 | # Offense count: 2 195 | # Cop supports --auto-correct. 196 | # Configuration parameters: WordRegex. 197 | # SupportedStyles: percent, brackets 198 | Style/WordArray: 199 | EnforcedStyle: percent 200 | MinSize: 3 201 | 202 | # Offense count: 56 203 | # Cop supports --auto-correct. 204 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 205 | # URISchemes: http, https 206 | Layout/LineLength: 207 | Max: 225 208 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | addressable (2.8.0) 6 | public_suffix (>= 2.0.2, < 5.0) 7 | artifactory (3.0.15) 8 | ast (2.4.2) 9 | atomos (0.1.3) 10 | aws-eventstream (1.1.1) 11 | aws-partitions (1.490.0) 12 | aws-sdk-core (3.119.1) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.239.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1.0) 17 | aws-sdk-kms (1.46.0) 18 | aws-sdk-core (~> 3, >= 3.119.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.99.0) 21 | aws-sdk-core (~> 3, >= 3.119.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.1) 24 | aws-sigv4 (1.2.4) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.0.3) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | diff-lcs (1.4.4) 34 | digest-crc (0.6.4) 35 | rake (>= 12.0.0, < 14.0.0) 36 | domain_name (0.5.20190701) 37 | unf (>= 0.0.5, < 1.0.0) 38 | dotenv (2.7.6) 39 | emoji_regex (3.2.2) 40 | excon (0.85.0) 41 | faraday (1.7.0) 42 | faraday-em_http (~> 1.0) 43 | faraday-em_synchrony (~> 1.0) 44 | faraday-excon (~> 1.1) 45 | faraday-httpclient (~> 1.0.1) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.1) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | multipart-post (>= 1.2, < 3) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-net_http (1.0.1) 60 | faraday-net_http_persistent (1.2.0) 61 | faraday-patron (1.0.0) 62 | faraday-rack (1.0.0) 63 | faraday_middleware (1.1.0) 64 | faraday (~> 1.0) 65 | fastimage (2.2.5) 66 | fastlane (2.192.0) 67 | CFPropertyList (>= 2.3, < 4.0.0) 68 | addressable (>= 2.8, < 3.0.0) 69 | artifactory (~> 3.0) 70 | aws-sdk-s3 (~> 1.0) 71 | babosa (>= 1.0.3, < 2.0.0) 72 | bundler (>= 1.12.0, < 3.0.0) 73 | colored 74 | commander (~> 4.6) 75 | dotenv (>= 2.1.1, < 3.0.0) 76 | emoji_regex (>= 0.1, < 4.0) 77 | excon (>= 0.71.0, < 1.0.0) 78 | faraday (~> 1.0) 79 | faraday-cookie_jar (~> 0.0.6) 80 | faraday_middleware (~> 1.0) 81 | fastimage (>= 2.1.0, < 3.0.0) 82 | gh_inspector (>= 1.1.2, < 2.0.0) 83 | google-apis-androidpublisher_v3 (~> 0.3) 84 | google-apis-playcustomapp_v1 (~> 0.1) 85 | google-cloud-storage (~> 1.31) 86 | highline (~> 2.0) 87 | json (< 3.0.0) 88 | jwt (>= 2.1.0, < 3) 89 | mini_magick (>= 4.9.4, < 5.0.0) 90 | multipart-post (~> 2.0.0) 91 | naturally (~> 2.2) 92 | optparse (~> 0.1.1) 93 | plist (>= 3.1.0, < 4.0.0) 94 | rubyzip (>= 2.0.0, < 3.0.0) 95 | security (= 0.1.3) 96 | simctl (~> 1.6.3) 97 | terminal-notifier (>= 2.0.0, < 3.0.0) 98 | terminal-table (>= 1.4.5, < 2.0.0) 99 | tty-screen (>= 0.6.3, < 1.0.0) 100 | tty-spinner (>= 0.8.0, < 1.0.0) 101 | word_wrap (~> 1.0.0) 102 | xcodeproj (>= 1.13.0, < 2.0.0) 103 | xcpretty (~> 0.3.0) 104 | xcpretty-travis-formatter (>= 0.0.3) 105 | gh_inspector (1.1.3) 106 | google-apis-androidpublisher_v3 (0.10.0) 107 | google-apis-core (>= 0.4, < 2.a) 108 | google-apis-core (0.4.1) 109 | addressable (~> 2.5, >= 2.5.1) 110 | googleauth (>= 0.16.2, < 2.a) 111 | httpclient (>= 2.8.1, < 3.a) 112 | mini_mime (~> 1.0) 113 | representable (~> 3.0) 114 | retriable (>= 2.0, < 4.a) 115 | rexml 116 | webrick 117 | google-apis-iamcredentials_v1 (0.7.0) 118 | google-apis-core (>= 0.4, < 2.a) 119 | google-apis-playcustomapp_v1 (0.5.0) 120 | google-apis-core (>= 0.4, < 2.a) 121 | google-apis-storage_v1 (0.6.0) 122 | google-apis-core (>= 0.4, < 2.a) 123 | google-cloud-core (1.6.0) 124 | google-cloud-env (~> 1.0) 125 | google-cloud-errors (~> 1.0) 126 | google-cloud-env (1.5.0) 127 | faraday (>= 0.17.3, < 2.0) 128 | google-cloud-errors (1.1.0) 129 | google-cloud-storage (1.34.1) 130 | addressable (~> 2.5) 131 | digest-crc (~> 0.4) 132 | google-apis-iamcredentials_v1 (~> 0.1) 133 | google-apis-storage_v1 (~> 0.1) 134 | google-cloud-core (~> 1.6) 135 | googleauth (>= 0.16.2, < 2.a) 136 | mini_mime (~> 1.0) 137 | googleauth (0.17.0) 138 | faraday (>= 0.17.3, < 2.0) 139 | jwt (>= 1.4, < 3.0) 140 | memoist (~> 0.16) 141 | multi_json (~> 1.11) 142 | os (>= 0.9, < 2.0) 143 | signet (~> 0.14) 144 | highline (2.0.3) 145 | http-cookie (1.0.4) 146 | domain_name (~> 0.5) 147 | httpclient (2.8.3) 148 | jmespath (1.4.0) 149 | json (2.5.1) 150 | jwt (2.2.3) 151 | memoist (0.16.2) 152 | mini_magick (4.11.0) 153 | mini_mime (1.1.1) 154 | multi_json (1.15.0) 155 | multipart-post (2.0.0) 156 | nanaimo (0.3.0) 157 | naturally (2.2.1) 158 | openssl (2.2.0) 159 | optparse (0.1.1) 160 | os (1.1.1) 161 | parallel (1.20.1) 162 | parser (3.0.2.0) 163 | ast (~> 2.4.1) 164 | plist (3.6.0) 165 | public_suffix (4.0.6) 166 | rainbow (3.0.0) 167 | rake (13.0.6) 168 | regexp_parser (2.1.1) 169 | representable (3.1.1) 170 | declarative (< 0.1.0) 171 | trailblazer-option (>= 0.1.1, < 0.2.0) 172 | uber (< 0.2.0) 173 | retriable (3.1.2) 174 | rexml (3.2.5) 175 | rouge (2.0.7) 176 | rspec (3.10.0) 177 | rspec-core (~> 3.10.0) 178 | rspec-expectations (~> 3.10.0) 179 | rspec-mocks (~> 3.10.0) 180 | rspec-core (3.10.1) 181 | rspec-support (~> 3.10.0) 182 | rspec-expectations (3.10.1) 183 | diff-lcs (>= 1.2.0, < 2.0) 184 | rspec-support (~> 3.10.0) 185 | rspec-mocks (3.10.2) 186 | diff-lcs (>= 1.2.0, < 2.0) 187 | rspec-support (~> 3.10.0) 188 | rspec-support (3.10.2) 189 | rubocop (1.19.1) 190 | parallel (~> 1.10) 191 | parser (>= 3.0.0.0) 192 | rainbow (>= 2.2.2, < 4.0) 193 | regexp_parser (>= 1.8, < 3.0) 194 | rexml 195 | rubocop-ast (>= 1.9.1, < 2.0) 196 | ruby-progressbar (~> 1.7) 197 | unicode-display_width (>= 1.4.0, < 3.0) 198 | rubocop-ast (1.11.0) 199 | parser (>= 3.0.1.1) 200 | ruby-progressbar (1.11.0) 201 | ruby2_keywords (0.0.5) 202 | rubyzip (2.3.2) 203 | security (0.1.3) 204 | signet (0.15.0) 205 | addressable (~> 2.3) 206 | faraday (>= 0.17.3, < 2.0) 207 | jwt (>= 1.5, < 3.0) 208 | multi_json (~> 1.10) 209 | simctl (1.6.8) 210 | CFPropertyList 211 | naturally 212 | terminal-notifier (2.0.0) 213 | terminal-table (1.6.0) 214 | trailblazer-option (0.1.1) 215 | tty-cursor (0.7.1) 216 | tty-screen (0.8.1) 217 | tty-spinner (0.9.3) 218 | tty-cursor (~> 0.7) 219 | uber (0.1.0) 220 | unf (0.1.4) 221 | unf_ext 222 | unf_ext (0.0.7.7) 223 | unicode-display_width (2.0.0) 224 | webrick (1.7.0) 225 | word_wrap (1.0.0) 226 | xcodeproj (1.21.0) 227 | CFPropertyList (>= 2.3.3, < 4.0) 228 | atomos (~> 0.1.3) 229 | claide (>= 1.0.2, < 2.0) 230 | colored2 (~> 3.1) 231 | nanaimo (~> 0.3.0) 232 | rexml (~> 3.2.4) 233 | xcpretty (0.3.0) 234 | rouge (~> 2.0.7) 235 | xcpretty-travis-formatter (1.0.1) 236 | xcpretty (~> 0.2, >= 0.0.7) 237 | 238 | PLATFORMS 239 | ruby 240 | x86_64-darwin-19 241 | 242 | DEPENDENCIES 243 | fastlane 244 | openssl 245 | plist 246 | rspec 247 | rubocop (~> 1.18) 248 | xcodeproj 249 | 250 | BUNDLED WITH 251 | 2.2.24 252 | -------------------------------------------------------------------------------- /step.rb: -------------------------------------------------------------------------------- 1 | require_relative 'params' 2 | require_relative 'log/log' 3 | require_relative 'lib/autoprovision' 4 | 5 | begin 6 | # Params 7 | params = Params.new 8 | params.print 9 | params.validate 10 | 11 | Log.verbose = (params.verbose_log == 'yes') 12 | ### 13 | 14 | Log.warn("\n") 15 | Log.warn('This Step has been deprecated in favour of the new automatic code signing options on Bitrise.') 16 | Log.warn(" 17 | Option A) 18 | The latest versions of the [Xcode Archive & Export for iOS](https://www.bitrise.io/integrations/steps/xcode-archive), 19 | [Xcode Build for testing for iOS](https://www.bitrise.io/integrations/steps/xcode-build-for-test), 20 | and the [Export iOS and tvOS Xcode archive](https://www.bitrise.io/integrations/steps/xcode-archive) Steps 21 | have built-in automatic code signing. 22 | We recommend removing this Step from your Workflow and using the automatic code signing feature in the Steps mentioned above. 23 | 24 | Option B) 25 | If you are not using any of the mentioned Xcode steps, then you can replace this iOS Auto Provision Step with 26 | the [Manage iOS Code signing](https://www.bitrise.io/integrations/steps/manage-ios-code-signing) Step. 27 | 28 | You can read more about these changes in our blog post: 29 | https://blog.bitrise.io/post/simplifying-automatic-code-signing-on-bitrise 30 | ") 31 | 32 | # Unset SPACESHIP_AVOID_XCODE_API 33 | orig_spaceship_avoid_xcode_api = ENV['SPACESHIP_AVOID_XCODE_API'] 34 | Log.debug("\noriginal SPACESHIP_AVOID_XCODE_API: #{orig_spaceship_avoid_xcode_api}") 35 | ENV['SPACESHIP_AVOID_XCODE_API'] = nil 36 | Log.debug('SPACESHIP_AVOID_XCODE_API cleared') 37 | ### 38 | 39 | # Developer Portal authentication 40 | Log.info('Developer Portal authentication') 41 | 42 | auth = AuthHelper.new 43 | auth.login(params.build_url, params.build_api_token, params.team_id) 44 | 45 | Log.success('authenticated') 46 | ### 47 | 48 | # Download certificates 49 | Log.info('Downloading Certificates') 50 | 51 | certificate_urls = params.certificate_urls.reject(&:empty?) 52 | raise 'no certificates provider' if certificate_urls.to_a.empty? 53 | 54 | cert_helper = CertificateHelper.new 55 | cert_helper.download_and_identify(certificate_urls, params.passphrases) 56 | ### 57 | 58 | # Analyzing project 59 | Log.info('Analyzing project') 60 | 61 | project_helper = ProjectHelper.new(params.project_path, params.scheme, params.configuration) 62 | codesign_identity = project_helper.project_codesign_identity 63 | team_id = project_helper.project_team_id 64 | 65 | Log.print("project codesign identity: #{codesign_identity}") 66 | Log.print("project team id: #{team_id}") 67 | Log.print("uses xcode managed signing: #{project_helper.uses_xcode_auto_codesigning?}") 68 | Log.print("main target's platform: #{project_helper.platform}") 69 | 70 | targets = project_helper.targets.collect(&:name) 71 | targets.each_with_index do |target_name, idx| 72 | bundle_id = project_helper.target_bundle_id(target_name) 73 | entitlements = project_helper.target_entitlements(target_name) || {} 74 | 75 | Log.print("target ##{idx}: #{target_name} (#{bundle_id}) with #{entitlements.length} services") 76 | end 77 | 78 | if !params.team_id.to_s.empty? && params.team_id != team_id 79 | Log.warn("different team id defined: #{params.team_id} than the project's one: #{team_id}") 80 | Log.warn("using defined team id: #{params.team_id}") 81 | Log.warn("dropping project codesign identity: #{codesign_identity}") 82 | 83 | team_id = params.team_id 84 | codesign_identity = nil 85 | end 86 | 87 | raise 'failed to determine project development team' unless team_id 88 | 89 | ### 90 | 91 | # Matching project codesign identity with the uploaded certificates 92 | Log.info('Matching project codesign identity with the uploaded certificates') 93 | 94 | cert_helper.ensure_certificate(codesign_identity, team_id, params.distribution_type) 95 | 96 | # If development certificate is uploaded, do development auto code signing next to the specified distribution type. 97 | distribution_types = [params.distribution_type] 98 | distribution_types = ['development'].concat(distribution_types) if params.distribution_type != 'development' && cert_helper.certificate_info('development') 99 | 100 | Log.debug("distribution_types: #{distribution_types}") 101 | ## 102 | 103 | # Ensure test devices 104 | dev_portal_devices = [] 105 | distribution_type_requires_device_list = !(%w[development ad-hoc] & distribution_types).empty? 106 | if distribution_type_requires_device_list 107 | Log.info('Ensure test devices on Developer Portal') 108 | test_devices = params.register_test_devices == 'yes' ? auth.test_devices : [] 109 | dev_portal_devices = Portal::DeviceClient.ensure_test_devices(test_devices, project_helper.platform) 110 | end 111 | ### 112 | 113 | # Ensure Provisioning Profiles on Developer Portal 114 | Log.info('Ensure Provisioning Profiles on Developer Portal') 115 | 116 | profile_helper = ProfileHelper.new(project_helper, cert_helper) 117 | xcode_managed_signing = profile_helper.ensure_profiles(distribution_types, dev_portal_devices, params.generate_profiles == 'yes', params.min_profile_days_valid) 118 | ### 119 | 120 | unless xcode_managed_signing 121 | # Apply code sign setting in project 122 | Log.info('Apply code sign setting in project') 123 | 124 | targets.each do |target_name| 125 | bundle_id = project_helper.target_bundle_id(target_name) 126 | 127 | puts 128 | Log.success("configure target: #{target_name} (#{bundle_id})") 129 | 130 | code_sign_identity = nil 131 | provisioning_profile = nil 132 | 133 | if cert_helper.development_certificate_info 134 | certificate = cert_helper.development_certificate_info.certificate 135 | code_sign_identity = certificate_common_name(certificate) 136 | 137 | portal_profile = profile_helper.profiles_by_bundle_id('development')[bundle_id].portal_profile 138 | provisioning_profile = portal_profile.uuid 139 | elsif cert_helper.production_certificate_info 140 | certificate = cert_helper.production_certificate_info.certificate 141 | code_sign_identity = certificate_common_name(certificate) 142 | 143 | portal_profile = profile_helper.profiles_by_bundle_id(params.distribution_type)[bundle_id].portal_profile 144 | provisioning_profile = portal_profile.uuid 145 | else 146 | raise "no codesign settings generated for target: #{target_name} (#{bundle_id})" 147 | end 148 | 149 | project_helper.force_code_sign_properties(target_name, team_id, code_sign_identity, provisioning_profile) 150 | end 151 | ### 152 | end 153 | 154 | # Install certificates 155 | Log.info('Install certificates') 156 | 157 | certificate_infos = [] 158 | certificate_infos.push(cert_helper.development_certificate_info) if cert_helper.development_certificate_info 159 | certificate_infos.push(cert_helper.production_certificate_info) if cert_helper.production_certificate_info 160 | certificate_path_passphrase_map = Hash[certificate_infos.map { |info| [info.path, info.passphrase] }] 161 | 162 | keychain_helper = KeychainHelper.new(params.keychain_path, params.keychain_password) 163 | keychain_helper.install_certificates(certificate_path_passphrase_map) 164 | 165 | Log.success("#{certificate_path_passphrase_map.length} certificates installed") 166 | ### 167 | 168 | # Export outputs 169 | Log.info('Export outputs') 170 | 171 | bundle_id = project_helper.target_bundle_id(project_helper.main_target) 172 | 173 | outputs = { 174 | 'BITRISE_EXPORT_METHOD' => params.distribution_type, 175 | 'BITRISE_DEVELOPER_TEAM' => team_id 176 | } 177 | 178 | if cert_helper.development_certificate_info 179 | certificate = cert_helper.development_certificate_info.certificate 180 | code_sign_identity = certificate_common_name(certificate) 181 | 182 | portal_profile = profile_helper.profiles_by_bundle_id('development')[bundle_id].portal_profile 183 | provisioning_profile = portal_profile.uuid 184 | 185 | outputs['BITRISE_DEVELOPMENT_CODESIGN_IDENTITY'] = code_sign_identity 186 | outputs['BITRISE_DEVELOPMENT_PROFILE'] = provisioning_profile 187 | end 188 | 189 | if params.distribution_type != 'development' && cert_helper.production_certificate_info 190 | certificate = cert_helper.production_certificate_info.certificate 191 | code_sign_identity = certificate_common_name(certificate) 192 | 193 | portal_profile = profile_helper.profiles_by_bundle_id(params.distribution_type)[bundle_id].portal_profile 194 | provisioning_profile = portal_profile.uuid 195 | 196 | outputs['BITRISE_PRODUCTION_CODESIGN_IDENTITY'] = code_sign_identity 197 | outputs['BITRISE_PRODUCTION_PROFILE'] = provisioning_profile 198 | end 199 | 200 | outputs.each do |key, value| 201 | `envman add --key "#{key}" --value "#{value}"` 202 | Log.success("#{key}=#{value}") 203 | end 204 | ### 205 | 206 | # restore SPACESHIP_AVOID_XCODE_API 207 | ENV['SPACESHIP_AVOID_XCODE_API'] = orig_spaceship_avoid_xcode_api 208 | rescue => ex 209 | # restore SPACESHIP_AVOID_XCODE_API 210 | ENV['SPACESHIP_AVOID_XCODE_API'] = orig_spaceship_avoid_xcode_api 211 | 212 | puts 213 | Log.error('Error:') 214 | Log.error(ex.to_s) 215 | puts 216 | Log.error('Stacktrace (for debugging):') 217 | Log.error(ex.backtrace.join("\n").to_s) 218 | exit 1 219 | end 220 | -------------------------------------------------------------------------------- /e2e/bitrise.yml: -------------------------------------------------------------------------------- 1 | format_version: 11 2 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 3 | 4 | app: 5 | envs: 6 | # Shared test configs 7 | - BITRISE_KEYCHAIN_PATH: $HOME/Library/Keychains/login.keychain 8 | # Shared test secrets 9 | - BITRISE_KEYCHAIN_PASSWORD: $BITRISE_KEYCHAIN_PASSWORD 10 | - BITFALL_APPLE_IOS_CERTIFICATE_URL_LIST: $BITFALL_APPLE_IOS_CERTIFICATE_URL_LIST 11 | - BITFALL_APPLE_IOS_CERTIFICATE_PASSPHRASE_LIST: $BITFALL_APPLE_IOS_CERTIFICATE_PASSPHRASE_LIST 12 | - BITRISE_APPLE_TEAM_ID: $BITRISE_APPLE_TEAM_ID 13 | - REGISTER_TEST_DEVICES: "yes" 14 | 15 | workflows: 16 | test_new_certificates: 17 | steps: 18 | - bitrise-run: 19 | run_if: '{{enveq "BITRISEIO_STACK_ID" "osx-xcode-12.5.x"}}' 20 | inputs: 21 | - workflow_id: utility_test_new_certificates 22 | - bitrise_config_path: ./e2e/bitrise.yml 23 | 24 | utility_test_new_certificates: 25 | title: Test new Apple Development and Distribution certificates 26 | description: |- 27 | This workflow requires Xcode 11 stack or above to run. 28 | envs: 29 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-simple-objc.git 30 | - TEST_APP_BRANCH: new-certificates 31 | - BITRISE_PROJECT_PATH: ios-simple-objc/ios-simple-objc.xcodeproj 32 | - BITRISE_SCHEME: ios-simple-objc 33 | - BITRISE_CONFIGURATION: Release 34 | - DISTRIBUTION_TYPE: app-store 35 | - GENERATE_PROFILES: "yes" 36 | - EXPORT_OPTIONS: 37 | after_run: 38 | - _run 39 | - _check_outputs 40 | - _check_xcode_archive 41 | 42 | test_bundle_id: 43 | envs: 44 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-simple-objc.git 45 | - TEST_APP_BRANCH: bundle_id 46 | - BITRISE_PROJECT_PATH: ios-simple-objc/ios-simple-objc.xcodeproj 47 | - BITRISE_SCHEME: ios-simple-objc 48 | - BITRISE_CONFIGURATION: Release 49 | - DISTRIBUTION_TYPE: ad-hoc 50 | - GENERATE_PROFILES: "yes" 51 | - EXPORT_OPTIONS: 52 | - REGISTER_TEST_DEVICES: "no" 53 | after_run: 54 | - _run 55 | - _check_outputs 56 | - _check_xcode_archive 57 | 58 | test_xcode_managed: 59 | envs: 60 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-multi-target.git 61 | - TEST_APP_BRANCH: automatic 62 | - BITRISE_PROJECT_PATH: code-sign-test.xcodeproj 63 | - BITRISE_SCHEME: code-sign-test 64 | - BITRISE_CONFIGURATION: 65 | - DISTRIBUTION_TYPE: app-store 66 | - GENERATE_PROFILES: "no" 67 | - EXPORT_OPTIONS: |- 68 | 69 | 70 | 71 | 72 | method 73 | app-store 74 | provisioningProfiles 75 | 76 | com.bitrise.code-sign-test 77 | iOS Team Store Provisioning Profile: com.bitrise.code-sign-test 78 | com.bitrise.code-sign-test.share-extension 79 | iOS Team Store Provisioning Profile: com.bitrise.code-sign-test.share-extension 80 | com.bitrise.code-sign-test.watchkitapp 81 | iOS Team Store Provisioning Profile: com.bitrise.code-sign-test.watchkitapp 82 | com.bitrise.code-sign-test.watchkitapp.watchkitextension 83 | iOS Team Store Provisioning Profile: com.bitrise.code-sign-test.watchkitapp.watchkitextension 84 | 85 | signingCertificate 86 | iPhone Distribution: BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG (72SA8V3WYL) 87 | teamID 88 | 72SA8V3WYL 89 | 90 | 91 | after_run: 92 | - _run 93 | - _check_outputs 94 | - _check_xcode_archive 95 | 96 | test_xcode_managed_generate_enabled: 97 | envs: 98 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-multi-target.git 99 | - TEST_APP_BRANCH: automatic 100 | - BITRISE_PROJECT_PATH: code-sign-test.xcodeproj 101 | - BITRISE_SCHEME: code-sign-test 102 | - BITRISE_CONFIGURATION: 103 | - DISTRIBUTION_TYPE: app-store 104 | - GENERATE_PROFILES: "yes" 105 | - EXPORT_OPTIONS: 106 | after_run: 107 | - _run 108 | - _check_outputs 109 | - _check_xcode_archive 110 | 111 | test_entitlements: 112 | envs: 113 | - TEST_APP_URL: https://github.com/bitrise-io/sample-apps-ios-multi-target.git 114 | - TEST_APP_BRANCH: entitlements 115 | - BITRISE_PROJECT_PATH: code-sign-test.xcodeproj 116 | - BITRISE_SCHEME: code-sign-test 117 | - BITRISE_CONFIGURATION: 118 | - DISTRIBUTION_TYPE: app-store 119 | - GENERATE_PROFILES: "yes" 120 | - EXPORT_OPTIONS: 121 | after_run: 122 | - _run 123 | - _check_outputs 124 | - _check_xcode_archive 125 | 126 | test_workspace: 127 | steps: 128 | - bitrise-run: 129 | run_if: '{{getenv "BITRISEIO_STACK_ID" | ne "osx-xcode-10.3.x"}}' 130 | inputs: 131 | - workflow_id: utility_test_workspace 132 | - bitrise_config_path: ./e2e/bitrise.yml 133 | 134 | utility_test_workspace: 135 | envs: 136 | - TEST_APP_URL: https://github.com/bitrise-io/ios-cocoapods-minimal-sample.git 137 | - TEST_APP_BRANCH: master 138 | - BITRISE_PROJECT_PATH: iOSMinimalCocoaPodsSample/iOSMinimalCocoaPodsSample.xcworkspace 139 | - BITRISE_SCHEME: iOSMinimalCocoaPodsSample 140 | - BITRISE_CONFIGURATION: 141 | - INSTALL_PODS: "true" 142 | - DISTRIBUTION_TYPE: app-store 143 | - GENERATE_PROFILES: "yes" 144 | - EXPORT_OPTIONS: 145 | after_run: 146 | - _run 147 | - _check_outputs 148 | - _check_xcode_archive 149 | 150 | _run: 151 | steps: 152 | - script: 153 | inputs: 154 | - content: |- 155 | #!/bin/env bash 156 | set -ex 157 | rm -rf ./_tmp 158 | - git::https://github.com/bitrise-steplib/bitrise-step-simple-git-clone.git: 159 | inputs: 160 | - repository_url: $TEST_APP_URL 161 | - branch: $TEST_APP_BRANCH 162 | - clone_into_dir: ./_tmp 163 | - cocoapods-install: 164 | run_if: '{{enveq "INSTALL_PODS" "true"}}' 165 | title: CocoaPods install 166 | inputs: 167 | - verbose: "true" 168 | - path::./: 169 | title: Step Test 170 | run_if: true 171 | inputs: 172 | - build_url: $BITRISE_BUILD_URL 173 | - build_api_token: $BITRISE_BUILD_API_TOKEN 174 | - certificate_urls: $BITFALL_APPLE_IOS_CERTIFICATE_URL_LIST 175 | - passphrases: $BITFALL_APPLE_IOS_CERTIFICATE_PASSPHRASE_LIST 176 | - team_id: $BITRISE_APPLE_TEAM_ID 177 | - distribution_type: $DISTRIBUTION_TYPE 178 | - project_path: ./_tmp/$BITRISE_PROJECT_PATH 179 | - scheme: $BITRISE_SCHEME 180 | - configuration: $BITRISE_CONFIGURATION 181 | - generate_profiles: $GENERATE_PROFILES 182 | - verbose_log: "yes" 183 | - register_test_devices: $REGISTER_TEST_DEVICES 184 | 185 | _check_outputs: 186 | steps: 187 | - script: 188 | title: Output test 189 | inputs: 190 | - content: |- 191 | #!/bin/bash 192 | set -e 193 | echo "BITRISE_EXPORT_METHOD: $BITRISE_EXPORT_METHOD" 194 | echo "BITRISE_DEVELOPER_TEAM: $BITRISE_DEVELOPER_TEAM" 195 | echo "BITRISE_DEVELOPMENT_CODESIGN_IDENTITY: $BITRISE_DEVELOPMENT_CODESIGN_IDENTITY" 196 | echo "BITRISE_DEVELOPMENT_PROFILE: $BITRISE_DEVELOPMENT_PROFILE" 197 | echo "BITRISE_PRODUCTION_CODESIGN_IDENTITY: $BITRISE_PRODUCTION_CODESIGN_IDENTITY" 198 | echo "BITRISE_PRODUCTION_PROFILE: $BITRISE_PRODUCTION_PROFILE" 199 | if [ "$BITRISE_EXPORT_METHOD" != "$DISTRIBUTION_TYPE" ]; then exit 1; fi 200 | 201 | _check_xcode_archive: 202 | steps: 203 | - xcode-archive: 204 | title: Xcode archive 205 | inputs: 206 | - export_method: $DISTRIBUTION_TYPE 207 | - project_path: ./_tmp/$BITRISE_PROJECT_PATH 208 | - scheme: $BITRISE_SCHEME 209 | - configuration: $BITRISE_CONFIGURATION 210 | - output_tool: xcodebuild 211 | - custom_export_options_plist_content: $EXPORT_OPTIONS 212 | - team_id: $BITRISE_APPLE_TEAM_ID 213 | - force_team_id: $BITRISE_APPLE_TEAM_ID 214 | - deploy-to-bitrise-io: 215 | inputs: 216 | - notify_user_groups: none 217 | 218 | _expose_xcode_version: 219 | steps: 220 | - script: 221 | title: Expose Xcode major version 222 | inputs: 223 | - content: |- 224 | #!/bin/env bash 225 | set -e 226 | if [[ ! -z "$XCODE_MAJOR_VERSION" ]]; then 227 | echo "Xcode major version already exposed: $XCODE_MAJOR_VERSION" 228 | exit 0 229 | fi 230 | version=`xcodebuild -version` 231 | regex="Xcode ([0-9]*)." 232 | if [[ ! $version =~ $regex ]]; then 233 | echo "Failed to determine Xcode major version" 234 | exit 1 235 | fi 236 | xcode_major_version=${BASH_REMATCH[1]} 237 | echo "Xcode major version: $xcode_major_version" 238 | envman add --key XCODE_MAJOR_VERSION --value $xcode_major_version 239 | -------------------------------------------------------------------------------- /lib/autoprovision/portal/app_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | require_relative 'common' 4 | 5 | module Portal 6 | # AppClient ... 7 | class AppClient 8 | ON_OFF_SERVICES_BY_KEY = { 9 | 'com.apple.security.application-groups' => Spaceship::Portal.app_service.app_group, 10 | 'com.apple.developer.in-app-payments' => Spaceship::Portal.app_service.apple_pay, 11 | 'com.apple.developer.associated-domains' => Spaceship::Portal.app_service.associated_domains, 12 | 'com.apple.developer.healthkit' => Spaceship::Portal.app_service.health_kit, 13 | 'com.apple.developer.homekit' => Spaceship::Portal.app_service.home_kit, 14 | 'com.apple.developer.networking.HotspotConfiguration' => Spaceship::Portal.app_service.hotspot, 15 | 'com.apple.InAppPurchase' => Spaceship::Portal.app_service.in_app_purchase, 16 | 'inter-app-audio' => Spaceship::Portal.app_service.inter_app_audio, 17 | 'com.apple.developer.networking.multipath' => Spaceship::Portal.app_service.multipath, 18 | 'com.apple.developer.networking.networkextension' => Spaceship::Portal.app_service.network_extension, 19 | 'com.apple.developer.nfc.readersession.formats' => Spaceship::Portal.app_service.nfc_tag_reading, 20 | 'com.apple.developer.networking.vpn.api' => Spaceship::Portal.app_service.vpn_configuration, 21 | 'aps-environment' => Spaceship::Portal.app_service.push_notification, 22 | 'com.apple.developer.siri' => Spaceship::Portal.app_service.siri_kit, 23 | 'com.apple.developer.pass-type-identifiers' => Spaceship::Portal.app_service.passbook, 24 | 'com.apple.external-accessory.wireless-configuration' => Spaceship::Portal.app_service.wireless_accessory 25 | }.freeze 26 | 27 | ON_OFF_SERVICE_NAME_BY_KEY = { 28 | 'com.apple.security.application-groups' => 'App Groups', 29 | 'com.apple.developer.in-app-payments' => 'Apple Pay', 30 | 'com.apple.developer.associated-domains' => 'Associated Domains', 31 | 'com.apple.developer.healthkit' => 'HealthKit', 32 | 'com.apple.developer.homekit' => 'HomeKit', 33 | 'com.apple.developer.networking.HotspotConfiguration' => 'Hotspot', 34 | 'com.apple.InAppPurchase' => 'In-App Purchase', 35 | 'inter-app-audio' => 'Inter-App Audio', 36 | 'com.apple.developer.networking.multipath' => 'Multipath', 37 | 'com.apple.developer.networking.networkextension' => 'Network Extensions', 38 | 'com.apple.developer.nfc.readersession.formats' => 'NFC Tag Reading', 39 | 'com.apple.developer.networking.vpn.api' => 'Personal VPN', 40 | 'aps-environment' => 'Push Notifications', 41 | 'com.apple.developer.siri' => 'SiriKit', 42 | 'com.apple.developer.pass-type-identifiers' => 'Wallet', 43 | 'com.apple.external-accessory.wireless-configuration' => 'Wireless Accessory Configuration' 44 | }.freeze 45 | 46 | ON_OFF_FEATURE_NAME_BY_KEY = { 47 | 'com.apple.security.application-groups' => 'APG3427HIY', 48 | 'com.apple.developer.in-app-payments' => 'OM633U5T5G', 49 | 'com.apple.developer.associated-domains' => 'SKC3T5S89Y', 50 | 'com.apple.developer.healthkit' => 'HK421J6T7P', 51 | 'com.apple.developer.homekit' => 'homeKit', 52 | 'com.apple.developer.networking.HotspotConfiguration' => 'HSC639VEI8', 53 | 'com.apple.InAppPurchase' => 'inAppPurchase', 54 | 'inter-app-audio' => 'IAD53UNK2F', 55 | 'com.apple.developer.networking.multipath' => 'MP49FN762P', 56 | 'com.apple.developer.networking.networkextension' => 'NWEXT04537', 57 | 'com.apple.developer.nfc.readersession.formats' => 'NFCTRMAY17', 58 | 'com.apple.developer.networking.vpn.api' => 'V66P55NK2I', 59 | 'aps-environment' => 'push', 60 | 'com.apple.developer.siri' => 'SI015DKUHP', 61 | 'com.apple.developer.pass-type-identifiers' => 'passbook', 62 | 'com.apple.external-accessory.wireless-configuration' => 'WC421J6T7P' 63 | }.freeze 64 | 65 | def self.ensure_app(bundle_id) 66 | app = Spaceship::Portal.app.find(bundle_id) 67 | return app if app 68 | 69 | name = "Bitrise - (#{bundle_id.tr('.', ' ')})" 70 | Log.debug("registering app: #{name} with bundle id: (#{bundle_id})") 71 | 72 | app = nil 73 | run_or_raise_preferred_error_message { app = Spaceship::Portal.app.create!(bundle_id: bundle_id, name: name) } 74 | 75 | raise "failed to create app with bundle id: #{bundle_id}" unless app 76 | app 77 | end 78 | 79 | def self.all_services_enabled?(app, entitlements) 80 | entitlements ||= {} 81 | app_features = app.details.features 82 | 83 | # on-off services 84 | entitlements.each_key do |key| 85 | on_off_app_service = ON_OFF_SERVICES_BY_KEY[key] 86 | next unless on_off_app_service 87 | return false unless AppClient.feature_enabled?(key, app_features) 88 | end 89 | 90 | # Data Protection 91 | feature_value = app_features['dataProtection'] 92 | 93 | data_protection_value = entitlements['com.apple.developer.default-data-protection'] 94 | if data_protection_value == 'NSFileProtectionComplete' 95 | return false unless feature_value == 'complete' 96 | elsif data_protection_value == 'NSFileProtectionCompleteUnlessOpen' 97 | return false unless feature_value == 'unless_open' 98 | elsif data_protection_value == 'NSFileProtectionCompleteUntilFirstUserAuthentication' 99 | return false unless feature_value == 'until_first_auth' 100 | end 101 | 102 | # iCloud 103 | uses_key_value_storage = !entitlements['com.apple.developer.ubiquity-kvstore-identifier'].nil? 104 | uses_cloud_documents = false 105 | uses_cloudkit = false 106 | 107 | icloud_services = entitlements['com.apple.developer.icloud-services'] 108 | unless icloud_services.to_a.empty? 109 | uses_cloud_documents = icloud_services.include?('CloudDocuments') 110 | uses_cloudkit = icloud_services.include?('CloudKit') 111 | end 112 | 113 | if uses_key_value_storage || uses_cloud_documents || uses_cloudkit 114 | return false unless app_features['cloudKitVersion'].to_i == 2 115 | return false unless app_features['iCloud'] 116 | end 117 | 118 | true 119 | end 120 | 121 | def self.sync_app_services(app, entitlements) 122 | entitlements ||= {} 123 | 124 | details = app.details 125 | app_features = details.features 126 | 127 | # on-off services 128 | entitlements.each_key do |key| 129 | on_off_app_service = ON_OFF_SERVICES_BY_KEY[key] 130 | next unless on_off_app_service 131 | 132 | service_name = ON_OFF_SERVICE_NAME_BY_KEY[key] 133 | 134 | if AppClient.feature_enabled?(key, app_features) 135 | Log.print("#{service_name} already enabled") 136 | else 137 | Log.success("set #{service_name}: on") 138 | app = app.update_service(on_off_app_service.on) 139 | end 140 | end 141 | 142 | # Data Protection 143 | feature_value = app_features['dataProtection'] 144 | 145 | data_protection_value = entitlements['com.apple.developer.default-data-protection'] 146 | if data_protection_value == 'NSFileProtectionComplete' 147 | if feature_value == 'complete' 148 | Log.print('Data Protection: complete already set') 149 | else 150 | Log.success('set Data Protection: complete') 151 | app = app.update_service(Spaceship::Portal.app_service.data_protection.complete) 152 | end 153 | elsif data_protection_value == 'NSFileProtectionCompleteUnlessOpen' 154 | if feature_value == 'unless_open' 155 | Log.print('Data Protection: unless_open already set') 156 | else 157 | Log.success('set Data Protection: unless_open') 158 | app = app.update_service(Spaceship::Portal.app_service.data_protection.unless_open) 159 | end 160 | elsif data_protection_value == 'NSFileProtectionCompleteUntilFirstUserAuthentication' 161 | if feature_value == 'until_first_auth' 162 | Log.print('Data Protection: until_first_auth already set') 163 | else 164 | Log.success('set Data Protection: until_first_auth') 165 | app = app.update_service(Spaceship::Portal.app_service.data_protection.until_first_auth) 166 | end 167 | end 168 | 169 | # iCloud 170 | uses_key_value_storage = !entitlements['com.apple.developer.ubiquity-kvstore-identifier'].nil? 171 | uses_cloud_documents = false 172 | uses_cloudkit = false 173 | 174 | icloud_services = entitlements['com.apple.developer.icloud-services'] 175 | unless icloud_services.to_a.empty? 176 | uses_cloud_documents = icloud_services.include?('CloudDocuments') 177 | uses_cloudkit = icloud_services.include?('CloudKit') 178 | end 179 | 180 | if uses_key_value_storage || uses_cloud_documents || uses_cloudkit 181 | if app_features['cloudKitVersion'].to_i == 2 182 | Log.print('CloudKit: already set') 183 | else 184 | Log.success('set CloudKit: on') 185 | app = app.update_service(Spaceship::Portal.app_service.cloud_kit.cloud_kit) 186 | end 187 | 188 | if app_features['iCloud'] 189 | Log.print('iCloud: already set') 190 | else 191 | Log.success('set iCloud: on') 192 | app = app.update_service(Spaceship::Portal.app_service.icloud.on) 193 | end 194 | end 195 | 196 | app 197 | end 198 | 199 | def self.feature_enabled?(entitlement_key, app_features) 200 | feature_key = ON_OFF_FEATURE_NAME_BY_KEY[entitlement_key] 201 | raise 'not on-off app service key provided' unless feature_key 202 | 203 | feature_value = app_features[feature_key] 204 | feature_value || false 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Auto Provision with Apple ID (Deprecated) 2 | 3 | [![Step changelog](https://shields.io/github/v/release/bitrise-steplib/steps-ios-auto-provision?include_prereleases&label=changelog&color=blueviolet)](https://github.com/bitrise-steplib/steps-ios-auto-provision/releases) 4 | 5 | Automatically manages your iOS Provisioning Profiles for your Xcode project. 6 | 7 |
8 | Description 9 | 10 | ### This Step has been deprecated in favour of the new automatic code signing options on Bitrise. 11 | You can read more about these changes in our blog post: [https://blog.bitrise.io/post/simplifying-automatic-code-signing-on-bitrise](https://blog.bitrise.io/post/simplifying-automatic-code-signing-on-bitrise). 12 | 13 | #### Option A) 14 | The latest versions of the [Xcode Archive & Export for iOS](https://www.bitrise.io/integrations/steps/xcode-archive), [Xcode Build for testing for iOS](https://www.bitrise.io/integrations/steps/xcode-build-for-test), and the [Export iOS and tvOS Xcode archive](https://www.bitrise.io/integrations/steps/xcode-archive) Steps have built-in automatic code signing. 15 | We recommend removing this Step from your Workflow and using the automatic code signing feature in the Steps mentioned above. 16 | 17 | #### Option B) 18 | If you are not using any of the mentioned Xcode Steps, then you can replace 19 | this iOS Auto Provision Step with the [Manage iOS Code signing](https://www.bitrise.io/integrations/steps/manage-ios-code-signing) Step. 20 | 21 | ### Description 22 | The [Step](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#ios-auto-provision-with-apple-id-step) uses session-based authentication to connect to an Apple Developer account. In addition to an Apple ID and password, it also stores the 2-factor authentication (2FA) code you provide. 23 | 24 | Please note that the [iOS Auto Provision with App Store Connect API](https://app.bitrise.io/integrations/steps/ios-auto-provision-appstoreconnect) Step uses the official [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests) instead of the old session-based method. 25 | 26 | The **iOS Auto Provision with Apple ID** Step supports in Xcode managed and manual code signing in the following ways: 27 | 28 | In the case of Xcode managed code signing projects, the Step: 29 | - Downloads the Xcode managed Provisioning Profiles and installs them for the build. 30 | - Installs the provided code signing certificates into the Keychain. 31 | In the case of manual code signing projects, the Step: 32 | - Ensures that the Application Identifier exists on the Apple Developer Portal. 33 | - Ensures that the project's Capabilities are set correctly in the Application Identifier. 34 | - Ensures that the Provisioning Profiles exist on the Apple Developer Portal and are installed for the build. 35 | - Ensures that all the available Test Devices exist on the Apple Developer Portal and are included in the Provisioning Profiles. 36 | - Installs the provided code signing certificates into the Keychain. 37 | 38 | ### Configuring the Step 39 | 40 | Before you start configuring the Step, make sure you've completed the following requirements: 41 | - You've [defining your Apple Developer Account to Bitrise](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#defining-your-apple-developer-account-to-bitrise-1). 42 | - You've [assigned an Apple Developer Account for your app](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#assigning-an-apple-developer-account-for-your-app-1). 43 | 44 | To configure the Step: 45 | Once you've completed the above requirements, there is very little configuration needed to this Step. 46 | 1. Add the **iOS Auto Provision with Apple ID** Step after any dependency installer Step in your Workflow, such as **Run CocoaPods install** or **Carthage**. 47 | 2. Click the Step to edit its input fields. You can see that the **Distribution type**, **Xcode Project (or Workspace) path**, and the **Scheme name** inputs are automatically filled out for you. 48 | 3. If your Developer Portal Account belongs to multiple development teams, add the **Developer Portal team ID** to manage the project's code signing files, for example '1MZX23ABCD4'. If that's not the case, you can still add it to manage the Provisioning Profiles with a different team than the one set in your project. If you leave it empty, the team defined by the project will be used. 49 | 4. If you wish to overwrite the configuration defined in your Scheme (for example, Debug, Release), you can do so in the **Configuration name** input. 50 | 5. If Xcode managed signing is enabled in the iOS app, check the value of the **Should the step try to generate Provisioning Profiles even if Xcode managed signing is enabled in the Xcode project?** input. 51 | - If it’s set to 'no', the Step will look for an Xcode Managed Provisioning Profile on the Apple Developer Portal. 52 | - If it’s set to 'yes', the Step will generate a new manual provisioning profile on the Apple Developer portal for the project. 53 | This input has no effect in the case of Manual code signing projects. 54 | 6. **The minimum days the Provisioning Profile should be valid** lets you specify how long a Provisioning Profile should be valid to sign an iOS app. By default it will only renew the Provisioning Profile when it expires. 55 | 56 | ### Troubleshooting 57 | Please note that the 2FA code is only valid for 30 days. 58 | When the 2FA code expires, you will need to re-authenticate to provide a new code. 59 | Go to the Apple Developer Account of the **Account settings** page, it will automatically ask for the 2FA code to authenticate again. 60 | There will be a list of the Apple Developer accounts that you have defined. To the far right of each, there are 3 dots. 61 | Click the dots and select **Re-authenticate (2SA/2FA)**. 62 | 63 | ### Useful links 64 | - [Managing code signing files - automatic provisioning](https://devcenter.bitrise.io/code-signing/ios-code-signing/ios-auto-provisioning/#configuring-ios-auto-provisioning) 65 | - [iOS code signing troubleshooting](https://devcenter.bitrise.io/code-signing/ios-code-signing/ios-code-signing-troubleshooting/) 66 | 67 | ### Related Steps 68 | - [iOS Auto Provision with App Store Connect API](https://app.bitrise.io/integrations/steps/ios-auto-provision-appstoreconnect) 69 | - [Xcode Archive & Export](https://www.bitrise.io/integrations/steps/xcode-archive) 70 |
71 | 72 | ## 🧩 Get started 73 | 74 | Add this step directly to your workflow in the [Bitrise Workflow Editor](https://devcenter.bitrise.io/steps-and-workflows/steps-and-workflows-index/). 75 | 76 | You can also run this step directly with [Bitrise CLI](https://github.com/bitrise-io/bitrise). 77 | 78 | ## ⚙️ Configuration 79 | 80 |
81 | Inputs 82 | 83 | | Key | Description | Flags | Default | 84 | | --- | --- | --- | --- | 85 | | `distribution_type` | Describes how Xcode should sign your project. | required | `development` | 86 | | `team_id` | The Developer Portal team to manage the project's code signing files. __If your Developer Portal Account belongs to multiple development team, this input is required!__ Otherwise specify this input if you want to manage the Provisioning Profiles with a different team than the one set in your project. If you leave it empty the team defined by the project will be used. __Example:__ `1MZX23ABCD4` | | | 87 | | `project_path` | A `.xcodeproj` or `.xcworkspace` path. | required | `$BITRISE_PROJECT_PATH` | 88 | | `scheme` | The Xcode Scheme to use. | required | `$BITRISE_SCHEME` | 89 | | `configuration` | The Xcode Configuration to use. By default your Scheme defines which Configuration (Debug, Release, ...) should be used, but you can overwrite it with this option. | | | 90 | | `generate_profiles` | In the case of __Xcode managed code signing__ projects, by default the step downloads and installs the Xcode managed Provisioning Profiles. If this input is set to: `yes`, the step will try to manage the Provisioning Profiles by itself (__like in the case of Manual code signing projects__), the step will fall back to use the Xcode managed Provisioning Profiles if there is an issue. __This input has no effect in the case of Manual codesigning projects.__ | | `no` | 91 | | `register_test_devices` | If set the step will register known test devices on Bitrise from team members with the Apple Developer Portal. Note that setting this to "yes" may cause devices to be registered against your limited quantity of test devices in the Apple Developer Portal, which can only be removed once annually during your renewal window. | | `no` | 92 | | `min_profile_days_valid` | Sometimes you want to sign an app with a Provisioning Profile that is valid for at least 'x' days. For example, an enterprise app won't open if your Provisioning Profile is expired. With this parameter, you can have a Provisioning Profile that's at least valid for 'x' days. By default (0) it just renews the Provisioning Profile when expired. | | `0` | 93 | | `verbose_log` | Enable verbose logging? | required | `no` | 94 | | `certificate_urls` | URLs of the certificates to download. Multiple URLs can be specified, separated by a pipe (`\|`) character, you can specify a local path as well, using the `file://` scheme. __Provide a development certificate__ url, to ensure development code signing files for the project and __also provide a distribution certificate__ url, to ensure distribution code signing files for your project. __Example:__ `file://./development/certificate/path\|https://distribution/certificate/url` | required, sensitive | `$BITRISE_CERTIFICATE_URL` | 95 | | `passphrases` | Certificate passphrases. Multiple passphrases can be specified, separated by a pipe (`\|`) character. __Specified certificate passphrase count should match the count of the certificate URLs.__ For example, (1 certificate with empty passphrase, 1 certificate with non-empty passphrase) `\|distribution-passphrase`. | required, sensitive | `$BITRISE_CERTIFICATE_PASSPHRASE` | 96 | | `keychain_path` | The Keychain path. | required | `$HOME/Library/Keychains/login.keychain` | 97 | | `keychain_password` | The Keychain's password. | required, sensitive | `$BITRISE_KEYCHAIN_PASSWORD` | 98 | | `build_url` | Bitrise build URL. | required | `$BITRISE_BUILD_URL` | 99 | | `build_api_token` | Bitrise build API token. | required, sensitive | `$BITRISE_BUILD_API_TOKEN` | 100 |
101 | 102 |
103 | Outputs 104 | 105 | | Environment Variable | Description | 106 | | --- | --- | 107 | | `BITRISE_EXPORT_METHOD` | The selected distribution type. One of these: `development`, `app-store`, `ad-hoc` or `enterprise`. | 108 | | `BITRISE_DEVELOPER_TEAM` | The development team's ID. Example: `1MZX23ABCD4` | 109 | | `BITRISE_DEVELOPMENT_CODESIGN_IDENTITY` | The development code signing identity's name. For example, `iPhone Developer: Bitrise Bot (VV2J4SV8V4)`. | 110 | | `BITRISE_PRODUCTION_CODESIGN_IDENTITY` | The production code signing identity's name. Example: `iPhone Distribution: Bitrise Bot (VV2J4SV8V4)` | 111 | | `BITRISE_DEVELOPMENT_PROFILE` | The main target's development provisioning profile's UUID. Example: `c5be4123-1234-4f9d-9843-0d9be985a068` | 112 | | `BITRISE_PRODUCTION_PROFILE` | The main target's production provisioning profile UUID. Example: `c5be4123-1234-4f9d-9843-0d9be985a068` | 113 |
114 | 115 | ## 🙋 Contributing 116 | 117 | We welcome [pull requests](https://github.com/bitrise-steplib/steps-ios-auto-provision/pulls) and [issues](https://github.com/bitrise-steplib/steps-ios-auto-provision/issues) against this repository. 118 | 119 | For pull requests, work on your changes in a forked repository and use the Bitrise CLI to [run step tests locally](https://devcenter.bitrise.io/bitrise-cli/run-your-first-build/). 120 | 121 | **Note:** this step's end-to-end tests (defined in `e2e/bitrise.yml`) are working with secrets which are intentionally not stored in this repo. External contributors won't be able to run those tests. Don't worry, if you open a PR with your contribution, we will help with running tests and make sure that they pass. 122 | 123 | Learn more about developing steps: 124 | 125 | - [Create your own step](https://devcenter.bitrise.io/contributors/create-your-own-step/) 126 | - [Testing your Step](https://devcenter.bitrise.io/contributors/testing-and-versioning-your-steps/) 127 | -------------------------------------------------------------------------------- /lib/autoprovision/certificate_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative 'certificate_info' 2 | require_relative 'utils' 3 | require_relative 'portal/certificate_client' 4 | 5 | # CertificateHelper ... 6 | class CertificateHelper 7 | attr_reader :development_certificate_info 8 | attr_reader :production_certificate_info 9 | 10 | def initialize 11 | @all_development_certificate_infos = [] 12 | @all_production_certificate_infos = [] 13 | 14 | @portal_certificate_by_id = {} 15 | end 16 | 17 | def download_and_identify(urls, passes) 18 | raise "certificates count (#{urls.length}) and passphrases count (#{passes.length}) should match" unless urls.length == passes.length 19 | 20 | certificate_infos = [] 21 | urls.each_with_index do |url, idx| 22 | Log.debug("downloading certificate ##{idx + 1}") 23 | 24 | path = download_or_create_local_path(url, "Certrificate_#{idx}.p12") 25 | Log.debug("certificate source: #{path}") 26 | 27 | certificates = read_certificates(path, passes[idx]) 28 | Log.debug("#{certificates.length} codesign identities included:") 29 | 30 | certificates.each do |certificate| 31 | Log.debug("- #{certificate_name_and_serial(certificate)}") 32 | 33 | if certificate.not_after < Time.now.utc 34 | Log.error("[X] Certificate is not valid anymore - validity ended at: #{certificate.not_after}\n") 35 | else 36 | certificate_info = CertificateInfo.new(path, passes[idx], certificate) 37 | certificate_infos = append_if_latest_certificate(certificate_info, certificate_infos) 38 | end 39 | end 40 | end 41 | Log.success("#{urls.length} certificate files downloaded, #{certificate_infos.length} distinct codesign identities included") 42 | 43 | @all_development_certificate_infos, @all_production_certificate_infos = identify_certificate_infos(certificate_infos) 44 | end 45 | 46 | def ensure_certificate(name, team_id, distribution_type) 47 | team_development_certificate_infos = map_certificates_infos_by_team_id(@all_development_certificate_infos)[team_id] || [] 48 | team_production_certificate_infos = map_certificates_infos_by_team_id(@all_production_certificate_infos)[team_id] || [] 49 | 50 | if team_development_certificate_infos.empty? && team_production_certificate_infos.empty? 51 | raise "no certificate uploaded for the desired team: #{team_id}" 52 | end 53 | 54 | if name 55 | filtered_team_development_certificate_infos = team_development_certificate_infos.select do |certificate_info| 56 | common_name = certificate_common_name(certificate_info.certificate) 57 | common_name.downcase.include?(name.downcase) 58 | end 59 | team_development_certificate_infos = filtered_team_development_certificate_infos unless filtered_team_development_certificate_infos.empty? 60 | 61 | filtered_team_production_certificate_infos = team_production_certificate_infos.select do |certificate_info| 62 | common_name = certificate_common_name(certificate_info.certificate) 63 | common_name.downcase.include?(name.downcase) 64 | end 65 | team_production_certificate_infos = filtered_team_production_certificate_infos unless filtered_team_production_certificate_infos.empty? 66 | end 67 | 68 | if team_development_certificate_infos.length > 1 69 | msg = "Multiple Development certificates mathes to development team: #{team_id}" 70 | msg += " and name: #{name}" if name 71 | Log.warn(msg) 72 | team_development_certificate_infos.each { |info| Log.warn(" - #{certificate_name_and_serial(info.certificate)}") } 73 | end 74 | 75 | unless team_development_certificate_infos.empty? 76 | certificate_info = team_development_certificate_infos[0] 77 | Log.success("using: #{certificate_name_and_serial(certificate_info.certificate)}") 78 | @development_certificate_info = certificate_info 79 | end 80 | 81 | if team_production_certificate_infos.length > 1 82 | msg = "Multiple Distribution certificates mathes to development team: #{team_id}" 83 | msg += " and name: #{name}" if name 84 | Log.warn(msg) 85 | team_production_certificate_infos.each { |info| Log.warn(" - #{certificate_name_and_serial(info.certificate)}") } 86 | end 87 | 88 | unless team_production_certificate_infos.empty? 89 | certificate_info = team_production_certificate_infos[0] 90 | Log.success("using: #{certificate_name_and_serial(certificate_info.certificate)}") 91 | @production_certificate_info = certificate_info 92 | end 93 | 94 | if distribution_type == 'development' && @development_certificate_info.nil? 95 | raise [ 96 | 'Selected distribution type: development, but forgot to provide a Development type certificate.', 97 | "Don't worry, it's really simple to fix! :)", 98 | "Simply upload a Development type certificate (.p12) on the workflow editor's CodeSign tab and we'll be building in no time!" 99 | ].join("\n") 100 | end 101 | 102 | if distribution_type != 'development' && @production_certificate_info.nil? 103 | raise [ 104 | "Selected distribution type: #{distribution_type}, but forgot to provide a Distribution type certificate.", 105 | "Don't worry, it's really simple to fix! :)", 106 | "Simply upload a Distribution type certificate (.p12) on the workflow editor's CodeSign tab and we'll be building in no time!" 107 | ].join("\n") 108 | end 109 | end 110 | 111 | def certificate_info(distribution_type) 112 | if distribution_type == 'development' 113 | @development_certificate_info 114 | else 115 | @production_certificate_info 116 | end 117 | end 118 | 119 | def identify_certificate_infos(certificate_infos) 120 | Log.info('Identify Certificates on Developer Portal') 121 | 122 | portal_development_certificates = Portal::CertificateClient.download_development_certificates 123 | Log.debug('Development certificates on Apple Developer Portal:') 124 | portal_development_certificates.each do |cert| 125 | downloaded_portal_cert = download(cert) 126 | Log.debug("- #{cert.name}: #{certificate_name_and_serial(downloaded_portal_cert)} expire: #{downloaded_portal_cert.not_after}") 127 | end 128 | 129 | portal_production_certificates = Portal::CertificateClient.download_production_certificates 130 | Log.debug('Production certificates on Apple Developer Portal:') 131 | portal_production_certificates.each do |cert| 132 | downloaded_portal_cert = download(cert) 133 | Log.debug("- #{cert.name}: #{certificate_name_and_serial(downloaded_portal_cert)} expire: #{downloaded_portal_cert.not_after}") 134 | end 135 | 136 | development_certificate_infos = [] 137 | production_certificate_infos = [] 138 | certificate_infos.each do |certificate_info| 139 | Log.debug("searching for Certificate: #{certificate_name_and_serial(certificate_info.certificate)}") 140 | found = false 141 | 142 | portal_development_certificates.each do |portal_cert| 143 | downloaded_portal_cert = download(portal_cert) 144 | next unless certificate_matches(certificate_info.certificate, downloaded_portal_cert) 145 | 146 | Log.success("#{portal_cert.name} certificate found: #{certificate_name_and_serial(certificate_info.certificate)}") 147 | certificate_info.portal_certificate = portal_cert 148 | development_certificate_infos.push(certificate_info) 149 | found = true 150 | break 151 | end 152 | 153 | next if found 154 | 155 | portal_production_certificates.each do |portal_cert| 156 | downloaded_portal_cert = download(portal_cert) 157 | next unless certificate_matches(certificate_info.certificate, downloaded_portal_cert) 158 | 159 | Log.success("#{portal_cert.name} certificate found: #{certificate_name_and_serial(certificate_info.certificate)}") 160 | certificate_info.portal_certificate = portal_cert 161 | production_certificate_infos.push(certificate_info) 162 | end 163 | end 164 | 165 | if development_certificate_infos.empty? && production_certificate_infos.empty? 166 | raise 'no development nor production certificate identified on development portal' 167 | end 168 | 169 | [development_certificate_infos, production_certificate_infos] 170 | end 171 | 172 | def download(portal_certificate) 173 | downloaded_cert = @portal_certificate_by_id[portal_certificate.id] 174 | unless downloaded_cert 175 | downloaded_cert = portal_certificate.download 176 | @portal_certificate_by_id[portal_certificate.id] = downloaded_cert 177 | end 178 | downloaded_cert 179 | end 180 | 181 | def certificate_matches(certificate1, certificate2) 182 | return true if certificate1.serial == certificate2.serial 183 | 184 | if certificate_common_name(certificate1) == certificate_common_name(certificate2) && certificate1.not_after < certificate2.not_after 185 | Log.warn([ 186 | "Provided an older version of #{certificate_common_name(certificate1)} certificate (serial: #{certificate1.serial} expire: #{certificate1.not_after}),", 187 | "please download the most recent version from the Apple Developer Portal (serial: #{certificate2.serial} expire: #{certificate2.not_after}) and use it on Bitrise!" 188 | ].join("\n")) 189 | end 190 | 191 | false 192 | end 193 | 194 | def certificate_team_id(certificate) 195 | certificate.subject.to_a.find { |name, _, _| name == 'OU' }[1] 196 | end 197 | 198 | def find_certificate_info_by_identity(identity, certificate_infos) 199 | certificate_infos.each do |certificate_info| 200 | common_name = certificate_common_name(certificate_info.certificate) 201 | return certificate_info if common_name.downcase.include?(identity.downcase) 202 | end 203 | nil 204 | end 205 | 206 | def find_certificate_infos_by_team_id(team_id, certificate_infos) 207 | matching_certificate_infos = [] 208 | certificate_infos.each do |certificate_info| 209 | org_unit = certificate_team_id(certificate_info.certificate) 210 | matching_certificate_infos.push(certificate_info) if org_unit.downcase.include?(team_id.downcase) 211 | end 212 | matching_certificate_infos 213 | end 214 | 215 | def find_matching_codesign_identity_info(identity_name, team_id, certificate_infos) 216 | if identity_name 217 | certificate_info = find_certificate_info_by_identity(identity_name, certificate_infos) 218 | return certificate_info if certificate_info 219 | end 220 | 221 | team_certificate_infos = find_certificate_infos_by_team_id(team_id, certificate_infos) 222 | return team_certificate_infos[0] if team_certificate_infos.to_a.length == 1 223 | Log.print('no development certificate found') if team_certificate_infos.to_a.empty? 224 | Log.warn("#{team_certificate_infos.length} development certificate found") if team_certificate_infos.to_a.length > 1 225 | end 226 | 227 | def read_certificates(path, passphrase) 228 | content = File.read(path) 229 | p12 = OpenSSL::PKCS12.new(content, passphrase) 230 | 231 | certificates = [p12.certificate] 232 | certificates.concat(p12.ca_certs) if p12.ca_certs 233 | certificates 234 | end 235 | 236 | def append_if_latest_certificate(new_certificate_info, certificate_infos) 237 | new_certificate_common_name = certificate_common_name(new_certificate_info.certificate) 238 | index = certificate_infos.index { |info| certificate_common_name(info.certificate) == new_certificate_common_name } 239 | return certificate_infos.push(new_certificate_info) unless index 240 | 241 | Log.warn("multiple codesign identity uploaded with common name: #{new_certificate_common_name}") 242 | 243 | cert_info = certificate_infos[index] 244 | certificate_infos[index] = new_certificate_info if new_certificate_info.certificate.not_after > cert_info.certificate.not_after 245 | 246 | certificate_infos 247 | end 248 | 249 | def map_certificates_infos_by_team_id(certificate_infos) 250 | map = {} 251 | certificate_infos.each do |certificate_info| 252 | team_id = certificate_team_id(certificate_info.certificate) 253 | infos = map[team_id] || [] 254 | infos.push(certificate_info) 255 | map[team_id] = infos 256 | end 257 | map 258 | end 259 | 260 | def download_or_create_local_path(url, filename) 261 | pth = nil 262 | if url.start_with?('file://') 263 | pth = url.sub('file://', '') 264 | raise "Certificate not exist at: #{pth}" unless File.exist?(pth) 265 | else 266 | pth = File.join(Dir.tmpdir, filename) 267 | download_to_path(url, pth) 268 | end 269 | pth 270 | end 271 | 272 | def download_to_path(url, path) 273 | uri = URI.parse(url) 274 | request = Net::HTTP::Get.new(uri.request_uri) 275 | http_object = Net::HTTP.new(uri.host, uri.port) 276 | http_object.use_ssl = true 277 | response = http_object.start do |http| 278 | http.request(request) 279 | end 280 | 281 | raise printable_response(response) unless response.code == '200' 282 | 283 | File.open(path, 'wb') do |file| 284 | file.write(response.body) 285 | end 286 | 287 | content = File.read(path) 288 | raise 'empty file' if content.to_s.empty? 289 | 290 | path 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /lib/autoprovision/portal/profile_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | require_relative 'app_client' 4 | 5 | module Portal 6 | # ProfileClient ... 7 | class ProfileClient 8 | @profiles = {} 9 | 10 | # Xcode Managed profile examples: 11 | # XC Ad Hoc: * 12 | # XC: * 13 | # XC Ad Hoc: { bundle id } 14 | # XC: { bundle id } 15 | # iOS Team Provisioning Profile: * 16 | # iOS Team Ad Hoc Provisioning Profile: * 17 | # iOS Team Ad Hoc Provisioning Profile: {bundle id} 18 | # iOS Team Provisioning Profile: {bundle id} 19 | # tvOS Team Provisioning Profile: * 20 | # tvOS Team Ad Hoc Provisioning Profile: * 21 | # tvOS Team Ad Hoc Provisioning Profile: {bundle id} 22 | # tvOS Team Provisioning Profile: {bundle id} 23 | # Mac Team Provisioning Profile: * 24 | # Mac Team Ad Hoc Provisioning Profile: * 25 | # Mac Team Ad Hoc Provisioning Profile: {bundle id} 26 | # Mac Team Provisioning Profile: {bundle id} 27 | def self.xcode_managed?(profile) 28 | return true if profile.name.start_with?('XC') 29 | 30 | return true if profile.name.start_with?('iOS Team') && profile.name.include?('Provisioning Profile') 31 | 32 | return true if profile.name.start_with?('tvOS Team') && profile.name.include?('Provisioning Profile') 33 | 34 | return true if profile.name.start_with?('Mac Team') && profile.name.include?('Provisioning Profile') 35 | 36 | false 37 | end 38 | 39 | def self.ensure_xcode_managed_profile(bundle_id, entitlements, distribution_type, certificate, platform, test_devices, min_profile_days_valid, allow_retry = true) 40 | profiles = ProfileClient.fetch_profiles(true, platform) 41 | 42 | # Separate matching profiles 43 | # full_matching_profiles contains profiles which bundle id equals to the provided bundle_id, these are the prefered profiles 44 | # matching_profiles contains profiles which bundle id glob matches to the provided bundle_id 45 | full_matching_profiles = [] 46 | matching_profiles = [] 47 | profiles.each do |profile| 48 | if bundle_id_matches?(profile, bundle_id) 49 | full_matching_profiles.push(profile) 50 | next 51 | end 52 | 53 | matching_profiles.push(profile) if File.fnmatch(profile.app.bundle_id, bundle_id) 54 | end 55 | 56 | begin 57 | profiles = full_matching_profiles.select do |profile| 58 | distribution_type_matches?(profile, distribution_type, platform) && 59 | !expired?(profile, min_profile_days_valid) && 60 | all_services_enabled?(profile, entitlements) && 61 | include_certificate?(profile, certificate) && 62 | device_list_up_to_date?(profile, distribution_type, test_devices) 63 | end 64 | 65 | return profiles.first unless profiles.empty? 66 | 67 | profiles = matching_profiles.select do |profile| 68 | distribution_type_matches?(profile, distribution_type, platform) && 69 | !expired?(profile, min_profile_days_valid) && 70 | all_services_enabled?(profile, entitlements) && 71 | include_certificate?(profile, certificate) && 72 | device_list_up_to_date?(profile, distribution_type, test_devices) 73 | end 74 | rescue => ex 75 | raise ex unless allow_retry 76 | 77 | Log.debug_exception(ex) 78 | Log.debug('failed to validate profiles, retrying in 2 sec ...') 79 | sleep(2) 80 | ProfileClient.clear_cache(true, platform) 81 | return ProfileClient.ensure_xcode_managed_profile(bundle_id, entitlements, distribution_type, certificate, platform, test_devices, min_profile_days_valid, false) 82 | end 83 | 84 | return profiles.first unless profiles.empty? 85 | 86 | raise [ 87 | "Failed to find #{distribution_type} Xcode managed provisioning profile for bundle id: #{bundle_id}.", 88 | 'Please open your project in your local Xcode and generate an ipa file', 89 | 'with the desired distribution type and by using Xcode managed codesigning.', 90 | 'This will create / refresh the desired managed profiles.' 91 | ].join("\n") 92 | end 93 | 94 | def self.ensure_manual_profile(certificate, app, entitlements, distribution_type, platform, min_profile_days_valid, allow_retry = true, test_devices) 95 | all_profiles = ProfileClient.fetch_profiles(false, platform) 96 | 97 | # search for the Bitrise managed profile 98 | profile_name = "Bitrise #{platform} #{distribution_type} - (#{app.bundle_id})" 99 | profile = all_profiles.select { |prof| prof.name == profile_name }.first 100 | 101 | unless profile.nil? 102 | begin 103 | return profile if bundle_id_matches?(profile, app.bundle_id) && 104 | distribution_type_matches?(profile, distribution_type, platform) && 105 | !expired?(profile, min_profile_days_valid) && 106 | all_services_enabled?(profile, entitlements) && 107 | include_certificate?(profile, certificate) && 108 | device_list_up_to_date?(profile, distribution_type, test_devices) 109 | rescue => ex 110 | raise ex unless allow_retry 111 | 112 | Log.debug_exception(ex) 113 | Log.debug('failed to validate profile, retrying in 2 sec ...') 114 | sleep(2) 115 | ProfileClient.clear_cache(false, platform) 116 | return ProfileClient.ensure_manual_profile(certificate, app, entitlements, distribution_type, platform, min_profile_days_valid, false, test_devices) 117 | end 118 | end 119 | 120 | # profile name needs to be unique 121 | unless profile.nil? 122 | profile.delete! 123 | ProfileClient.clear_cache(false, platform) 124 | end 125 | 126 | begin 127 | Log.debug("generating profile: #{profile_name}") 128 | profile_class = portal_profile_class(distribution_type) 129 | run_or_raise_preferred_error_message { profile = profile_class.create!(bundle_id: app.bundle_id, certificate: certificate, name: profile_name, sub_platform: platform == :tvos ? 'tvOS' : nil) } 130 | rescue => ex 131 | raise ex unless allow_retry 132 | raise ex unless ex.to_s =~ /Multiple profiles found with the name/i 133 | 134 | # The profile already exist, paralell step run can produce this issue 135 | Log.debug_exception(ex) 136 | Log.debug('failed to generate the profile, retrying in 2 sec ...') 137 | sleep(2) 138 | ProfileClient.clear_cache(false, platform) 139 | return ProfileClient.ensure_manual_profile(certificate, app, entitlements, distribution_type, platform, min_profile_days_valid, false, test_devices) 140 | end 141 | 142 | raise "failed to find or create provisioning profile for bundle id: #{app.bundle_id}" unless profile 143 | 144 | profile 145 | end 146 | 147 | def self.bundle_id_matches?(profile, bundle_id) 148 | unless profile.app.bundle_id == bundle_id 149 | Log.debug("Profile (#{profile.name}) bundle id: #{profile.app.bundle_id}, should be: #{bundle_id}") 150 | return false 151 | end 152 | true 153 | end 154 | 155 | def self.distribution_type_matches?(profile, distribution_type, platform) 156 | distribution_methods = { 157 | 'development' => 'limited', 158 | 'app-store' => 'store', 159 | 'ad-hoc' => 'adhoc', 160 | 'enterprise' => 'inhouse' 161 | } 162 | desired_distribution_method = distribution_methods[distribution_type] 163 | 164 | # Both app_store.all and ad_hoc.all return the same 165 | # This is the case since September 2016, since the API has changed 166 | # and there is no fast way to get the type when fetching the profiles 167 | # Distinguish between App Store and Ad Hoc profiles 168 | 169 | # Profile name examples: 170 | # XC Ad Hoc: { bundle id } 171 | # iOS Team Ad Hoc Provisioning Profile: * 172 | # iOS Team Ad Hoc Provisioning Profile: {bundle id} 173 | # tvOS Team Ad Hoc Provisioning Profile: * 174 | # tvOS Team Ad Hoc Provisioning Profile: {bundle id} 175 | if ProfileClient.xcode_managed?(profile) 176 | if distribution_type == 'app-store' && platform.casecmp('tvos') 177 | return false if profile.name.downcase.start_with?('tvos team ad hoc', 'xc ad hoc', 'xc tvos ad hoc') 178 | elsif distribution_type == 'app-store' 179 | return false if profile.name.downcase.start_with?('ios team ad hoc', 'xc ad hoc', 'xc ios ad hoc') 180 | end 181 | end 182 | 183 | unless profile.distribution_method == desired_distribution_method 184 | Log.debug("Profile (#{profile.name}) distribution type: #{profile.distribution_method}, should be: #{desired_distribution_method}") 185 | return false 186 | end 187 | true 188 | end 189 | 190 | def self.expired?(profile, min_profile_days_valid) 191 | # Increment the current time with days in seconds (1 day = 86400 secs) the profile has to be valid for 192 | expire = Time.now + (min_profile_days_valid * 86_400) 193 | 194 | if Time.parse(profile.expires.to_s) < expire 195 | if min_profile_days_valid > 0 196 | Log.debug("Profile (#{profile.name}) is not valid for #{min_profile_days_valid} days") 197 | else 198 | Log.debug("Profile (#{profile.name}) expired at: #{profile.expires}") 199 | end 200 | 201 | return true 202 | end 203 | false 204 | end 205 | 206 | def self.all_services_enabled?(profile, entitlements) 207 | unless AppClient.all_services_enabled?(profile.app, entitlements) 208 | Log.debug("Profile (#{profile.name}) does not contain every required services") 209 | return false 210 | end 211 | true 212 | end 213 | 214 | def self.include_certificate?(profile, certificate) 215 | profile.certificates.each do |portal_certificate| 216 | return true if portal_certificate.id == certificate.id 217 | end 218 | Log.debug("Profile (#{profile.name}) does not contain certificate (#{certificate.name}) with details: #{certificate}") 219 | Log.debug("Profile (#{profile.name}) includes certificates:") 220 | profile.certificates.each do |portal_certificate| 221 | Log.debug(portal_certificate.to_s) 222 | end 223 | false 224 | end 225 | 226 | def self.device_list_up_to_date?(profile, distribution_type, test_devices) 227 | # check if the development and ad-hoc profile's device list is up to date 228 | if ['development', 'ad-hoc'].include?(distribution_type) && !test_devices.to_a.nil? 229 | profile_device_udids = profile.devices.map(&:udid) 230 | test_device_udids = test_devices.map(&:udid) 231 | 232 | unless (test_device_udids - profile_device_udids).empty? 233 | Log.debug("Profile (#{profile.name}) does not contain all the test devices") 234 | Log.debug("Missing devices:\n#{(test_device_udids - profile_device_udids).join("\n")}") 235 | 236 | return false 237 | end 238 | end 239 | 240 | true 241 | end 242 | 243 | def self.clear_cache(xcode_managed, platform) 244 | @profiles[platform].to_h[xcode_managed] = nil 245 | end 246 | 247 | def self.fetch_profiles(xcode_managed, platform) 248 | cached = @profiles[platform].to_h[xcode_managed] 249 | return cached unless cached.to_a.empty? 250 | 251 | profiles = [] 252 | run_or_raise_preferred_error_message { profiles = Spaceship::Portal.provisioning_profile.all(mac: false, xcode: xcode_managed) } 253 | # Log.debug("all profiles (#{profiles.length}):") 254 | # profiles.each do |profile| 255 | # Log.debug("#{profile.name}") 256 | # end 257 | 258 | # filter for sub_platform 259 | profiles = profiles.reject do |profile| 260 | if platform == :tvos 261 | profile.sub_platform.to_s.casecmp('tvos') == -1 262 | else 263 | profile.sub_platform.to_s.casecmp('tvos').zero? 264 | end 265 | end 266 | 267 | # filter non Xcode Managed profiles 268 | profiles = profiles.select { |profile| ProfileClient.xcode_managed?(profile) } if xcode_managed 269 | 270 | # Log.debug("subplatform #{platform} profiles (#{profiles.length}):") 271 | # profiles.each do |profile| 272 | # Log.debug("#{profile.name}") 273 | # end 274 | 275 | # update the cache 276 | platform_profiles = @profiles[platform].to_h 277 | platform_profiles[xcode_managed] = profiles 278 | @profiles[platform] = platform_profiles 279 | profiles 280 | end 281 | 282 | def self.portal_profile_class(distribution_type) 283 | case distribution_type 284 | when 'development' 285 | Spaceship::Portal.provisioning_profile.development 286 | when 'app-store' 287 | Spaceship::Portal.provisioning_profile.app_store 288 | when 'ad-hoc' 289 | Spaceship::Portal.provisioning_profile.ad_hoc 290 | when 'enterprise' 291 | Spaceship::Portal.provisioning_profile.in_house 292 | else 293 | raise "invalid distribution type provided: #{distribution_type}, available: [development, app-store, ad-hoc, enterprise]" 294 | end 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /step.yml: -------------------------------------------------------------------------------- 1 | title: iOS Auto Provision with Apple ID (Deprecated) 2 | summary: Automatically manages your iOS Provisioning Profiles for your Xcode project. 3 | description: |- 4 | ### This Step has been deprecated in favour of the new automatic code signing options on Bitrise. 5 | You can read more about these changes in our blog post: [https://blog.bitrise.io/post/simplifying-automatic-code-signing-on-bitrise](https://blog.bitrise.io/post/simplifying-automatic-code-signing-on-bitrise). 6 | 7 | #### Option A) 8 | The latest versions of the [Xcode Archive & Export for iOS](https://www.bitrise.io/integrations/steps/xcode-archive), [Xcode Build for testing for iOS](https://www.bitrise.io/integrations/steps/xcode-build-for-test), and the [Export iOS and tvOS Xcode archive](https://www.bitrise.io/integrations/steps/xcode-archive) Steps have built-in automatic code signing. 9 | We recommend removing this Step from your Workflow and using the automatic code signing feature in the Steps mentioned above. 10 | 11 | #### Option B) 12 | If you are not using any of the mentioned Xcode Steps, then you can replace 13 | this iOS Auto Provision Step with the [Manage iOS Code signing](https://www.bitrise.io/integrations/steps/manage-ios-code-signing) Step. 14 | 15 | ### Description 16 | The [Step](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#ios-auto-provision-with-apple-id-step) uses session-based authentication to connect to an Apple Developer account. In addition to an Apple ID and password, it also stores the 2-factor authentication (2FA) code you provide. 17 | 18 | Please note that the [iOS Auto Provision with App Store Connect API](https://app.bitrise.io/integrations/steps/ios-auto-provision-appstoreconnect) Step uses the official [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests) instead of the old session-based method. 19 | 20 | The **iOS Auto Provision with Apple ID** Step supports in Xcode managed and manual code signing in the following ways: 21 | 22 | In the case of Xcode managed code signing projects, the Step: 23 | - Downloads the Xcode managed Provisioning Profiles and installs them for the build. 24 | - Installs the provided code signing certificates into the Keychain. 25 | In the case of manual code signing projects, the Step: 26 | - Ensures that the Application Identifier exists on the Apple Developer Portal. 27 | - Ensures that the project's Capabilities are set correctly in the Application Identifier. 28 | - Ensures that the Provisioning Profiles exist on the Apple Developer Portal and are installed for the build. 29 | - Ensures that all the available Test Devices exist on the Apple Developer Portal and are included in the Provisioning Profiles. 30 | - Installs the provided code signing certificates into the Keychain. 31 | 32 | ### Configuring the Step 33 | 34 | Before you start configuring the Step, make sure you've completed the following requirements: 35 | - You've [defining your Apple Developer Account to Bitrise](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#defining-your-apple-developer-account-to-bitrise-1). 36 | - You've [assigned an Apple Developer Account for your app](https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/#assigning-an-apple-developer-account-for-your-app-1). 37 | 38 | To configure the Step: 39 | Once you've completed the above requirements, there is very little configuration needed to this Step. 40 | 1. Add the **iOS Auto Provision with Apple ID** Step after any dependency installer Step in your Workflow, such as **Run CocoaPods install** or **Carthage**. 41 | 2. Click the Step to edit its input fields. You can see that the **Distribution type**, **Xcode Project (or Workspace) path**, and the **Scheme name** inputs are automatically filled out for you. 42 | 3. If your Developer Portal Account belongs to multiple development teams, add the **Developer Portal team ID** to manage the project's code signing files, for example '1MZX23ABCD4'. If that's not the case, you can still add it to manage the Provisioning Profiles with a different team than the one set in your project. If you leave it empty, the team defined by the project will be used. 43 | 4. If you wish to overwrite the configuration defined in your Scheme (for example, Debug, Release), you can do so in the **Configuration name** input. 44 | 5. If Xcode managed signing is enabled in the iOS app, check the value of the **Should the step try to generate Provisioning Profiles even if Xcode managed signing is enabled in the Xcode project?** input. 45 | - If it’s set to 'no', the Step will look for an Xcode Managed Provisioning Profile on the Apple Developer Portal. 46 | - If it’s set to 'yes', the Step will generate a new manual provisioning profile on the Apple Developer portal for the project. 47 | This input has no effect in the case of Manual code signing projects. 48 | 6. **The minimum days the Provisioning Profile should be valid** lets you specify how long a Provisioning Profile should be valid to sign an iOS app. By default it will only renew the Provisioning Profile when it expires. 49 | 50 | ### Troubleshooting 51 | Please note that the 2FA code is only valid for 30 days. 52 | When the 2FA code expires, you will need to re-authenticate to provide a new code. 53 | Go to the Apple Developer Account of the **Account settings** page, it will automatically ask for the 2FA code to authenticate again. 54 | There will be a list of the Apple Developer accounts that you have defined. To the far right of each, there are 3 dots. 55 | Click the dots and select **Re-authenticate (2SA/2FA)**. 56 | 57 | ### Useful links 58 | - [Managing code signing files - automatic provisioning](https://devcenter.bitrise.io/code-signing/ios-code-signing/ios-auto-provisioning/#configuring-ios-auto-provisioning) 59 | - [iOS code signing troubleshooting](https://devcenter.bitrise.io/code-signing/ios-code-signing/ios-code-signing-troubleshooting/) 60 | 61 | ### Related Steps 62 | - [iOS Auto Provision with App Store Connect API](https://app.bitrise.io/integrations/steps/ios-auto-provision-appstoreconnect) 63 | - [Xcode Archive & Export](https://www.bitrise.io/integrations/steps/xcode-archive) 64 | 65 | website: https://github.com/bitrise-steplib/steps-ios-auto-provision 66 | source_code_url: https://github.com/bitrise-steplib/steps-ios-auto-provision 67 | support_url: https://github.com/bitrise-steplib/steps-ios-auto-provision/issues 68 | 69 | project_type_tags: 70 | - ios 71 | - cordova 72 | - ionic 73 | - react-native 74 | - flutter 75 | 76 | type_tags: 77 | - code-sign 78 | 79 | is_requires_admin_user: true 80 | is_always_run: false 81 | is_skippable: false 82 | run_if: ".IsCI" 83 | 84 | inputs: 85 | - distribution_type: development 86 | opts: 87 | title: Distribution type 88 | description: Describes how Xcode should sign your project. 89 | value_options: 90 | - "development" 91 | - "app-store" 92 | - "ad-hoc" 93 | - "enterprise" 94 | is_required: true 95 | - team_id: 96 | opts: 97 | title: The Developer Portal team ID 98 | description: |- 99 | The Developer Portal team to manage the project's code signing files. 100 | __If your Developer Portal Account belongs to multiple development team, this input is required!__ 101 | Otherwise specify this input if you want to manage the Provisioning Profiles with a different team than the one set in your project. 102 | If you leave it empty the team defined by the project will be used. 103 | __Example:__ `1MZX23ABCD4` 104 | - project_path: $BITRISE_PROJECT_PATH 105 | opts: 106 | title: Xcode Project (or Workspace) path 107 | description: A `.xcodeproj` or `.xcworkspace` path. 108 | is_required: true 109 | - scheme: $BITRISE_SCHEME 110 | opts: 111 | title: Scheme name 112 | description: The Xcode Scheme to use. 113 | is_required: true 114 | - configuration: 115 | opts: 116 | title: Configuration name 117 | description: |- 118 | The Xcode Configuration to use. 119 | By default your Scheme defines which Configuration (Debug, Release, ...) should be used, 120 | but you can overwrite it with this option. 121 | - generate_profiles: "no" 122 | opts: 123 | title: Should the step try to generate Provisioning Profiles even if Xcode managed signing is enabled in the Xcode project? 124 | description: |- 125 | In the case of __Xcode managed code signing__ projects, by default the step downloads and installs the Xcode managed Provisioning Profiles. 126 | If this input is set to: `yes`, the step will try to manage the Provisioning Profiles by itself (__like in the case of Manual code signing projects__), 127 | the step will fall back to use the Xcode managed Provisioning Profiles if there is an issue. 128 | __This input has no effect in the case of Manual codesigning projects.__ 129 | value_options: 130 | - "yes" 131 | - "no" 132 | - register_test_devices: "no" 133 | opts: 134 | title: Should the step register test devices with the Apple Developer Portal? 135 | description: |- 136 | If set the step will register known test devices on Bitrise from team members with the Apple Developer Portal. 137 | Note that setting this to "yes" may cause devices to be registered against your limited quantity of test devices in the Apple Developer Portal, which can only be removed once annually during your renewal window. 138 | value_options: 139 | - "yes" 140 | - "no" 141 | - min_profile_days_valid: 0 142 | opts: 143 | title: The minimum days the Provisioning Profile should be valid 144 | description: |- 145 | Sometimes you want to sign an app with a Provisioning Profile that is valid for at least 'x' days. 146 | For example, an enterprise app won't open if your Provisioning Profile is expired. With this parameter, you can have a Provisioning Profile that's at least valid for 'x' days. 147 | 148 | By default (0) it just renews the Provisioning Profile when expired. 149 | is_required: false 150 | - verbose_log: "no" 151 | opts: 152 | category: Debug 153 | title: Enable verbose logging? 154 | description: Enable verbose logging? 155 | is_required: true 156 | value_options: 157 | - "yes" 158 | - "no" 159 | - certificate_urls: $BITRISE_CERTIFICATE_URL 160 | opts: 161 | category: Debug 162 | title: Certificate URL 163 | description: | 164 | URLs of the certificates to download. 165 | Multiple URLs can be specified, separated by a pipe (`|`) character, 166 | you can specify a local path as well, using the `file://` scheme. 167 | __Provide a development certificate__ url, to ensure development code signing files for the project and __also provide a distribution certificate__ url, to ensure distribution code signing files for your project. 168 | __Example:__ `file://./development/certificate/path|https://distribution/certificate/url` 169 | is_required: true 170 | is_sensitive: true 171 | - passphrases: $BITRISE_CERTIFICATE_PASSPHRASE 172 | opts: 173 | category: Debug 174 | title: Certificate passphrase 175 | description: | 176 | Certificate passphrases. 177 | Multiple passphrases can be specified, separated by a pipe (`|`) character. 178 | __Specified certificate passphrase count should match the count of the certificate URLs.__ 179 | For example, (1 certificate with empty passphrase, 1 certificate with non-empty passphrase) `|distribution-passphrase`. 180 | is_required: true 181 | is_sensitive: true 182 | - keychain_path: $HOME/Library/Keychains/login.keychain 183 | opts: 184 | category: Debug 185 | title: Keychain path 186 | description: The Keychain path. 187 | is_required: true 188 | - keychain_password: $BITRISE_KEYCHAIN_PASSWORD 189 | opts: 190 | category: Debug 191 | title: Keychain's password 192 | description: The Keychain's password. 193 | is_required: true 194 | is_sensitive: true 195 | - build_url: $BITRISE_BUILD_URL 196 | opts: 197 | category: Debug 198 | title: Bitrise build URL 199 | description: Bitrise build URL. 200 | is_required: true 201 | - build_api_token: $BITRISE_BUILD_API_TOKEN 202 | opts: 203 | category: Debug 204 | title: Bitrise build API token 205 | description: Bitrise build API token. 206 | is_required: true 207 | is_sensitive: true 208 | outputs: 209 | - BITRISE_EXPORT_METHOD: 210 | opts: 211 | title: "The selected distribution type" 212 | description: |- 213 | The selected distribution type. 214 | One of these: `development`, `app-store`, `ad-hoc` or `enterprise`. 215 | - BITRISE_DEVELOPER_TEAM: 216 | opts: 217 | title: "The development team's ID" 218 | description: |- 219 | The development team's ID. 220 | Example: `1MZX23ABCD4` 221 | - BITRISE_DEVELOPMENT_CODESIGN_IDENTITY: 222 | opts: 223 | title: "The development code signing identity's name" 224 | description: |- 225 | The development code signing identity's name. 226 | For example, `iPhone Developer: Bitrise Bot (VV2J4SV8V4)`. 227 | - BITRISE_PRODUCTION_CODESIGN_IDENTITY: 228 | opts: 229 | title: "The production code signing identity's name" 230 | description: |- 231 | The production code signing identity's name. 232 | Example: `iPhone Distribution: Bitrise Bot (VV2J4SV8V4)` 233 | - BITRISE_DEVELOPMENT_PROFILE: 234 | opts: 235 | title: "The main target's development provisioning profile's UUID" 236 | description: |- 237 | The main target's development provisioning profile's UUID. 238 | Example: `c5be4123-1234-4f9d-9843-0d9be985a068` 239 | - BITRISE_PRODUCTION_PROFILE: 240 | opts: 241 | title: "The main target's production provisioning profile UUID" 242 | description: |- 243 | The main target's production provisioning profile UUID. 244 | Example: `c5be4123-1234-4f9d-9843-0d9be985a068` 245 | -------------------------------------------------------------------------------- /lib/autoprovision/project_helper.rb: -------------------------------------------------------------------------------- 1 | require 'xcodeproj' 2 | require 'json' 3 | require 'plist' 4 | require 'English' 5 | 6 | # ProjectHelper ... 7 | class ProjectHelper 8 | attr_reader :main_target 9 | attr_reader :targets 10 | attr_reader :platform 11 | 12 | def initialize(project_or_workspace_path, scheme_name, configuration_name) 13 | raise "project not exist at: #{project_or_workspace_path}" unless File.exist?(project_or_workspace_path) 14 | 15 | extname = File.extname(project_or_workspace_path) 16 | raise "unkown project extension: #{extname}, should be: .xcodeproj or .xcworkspace" unless ['.xcodeproj', '.xcworkspace'].include?(extname) 17 | 18 | @project_path = project_or_workspace_path 19 | 20 | # ensure scheme exist 21 | scheme, scheme_container_project_path = read_scheme_and_container_project(scheme_name) 22 | 23 | # read scheme application targets 24 | @main_target, @targets_container_project_path = read_scheme_archivable_target_and_container_project(scheme, scheme_container_project_path) 25 | raise "failed to find #{scheme_name} scheme's main archivable target" unless @main_target 26 | @platform = @main_target.platform_name 27 | 28 | @targets = collect_dependent_targets(@main_target) 29 | @targets = unique_targets(@targets) unless @targets.empty? 30 | raise "failed to collect #{@main_target}'s dependent targets" if @targets.empty? 31 | 32 | # ensure configuration exist 33 | action = scheme.archive_action 34 | raise "archive action not defined for scheme: #{scheme_name}" unless action 35 | default_configuration_name = action.build_configuration 36 | raise "archive action's configuration not found for scheme: #{scheme_name}" unless default_configuration_name 37 | 38 | if configuration_name.empty? || configuration_name == default_configuration_name 39 | @configuration_name = default_configuration_name 40 | elsif configuration_name != default_configuration_name 41 | targets.each do |target_obj| 42 | configuration = target_obj.build_configuration_list.build_configurations.find { |c| configuration_name.to_s == c.name } 43 | raise "build configuration (#{configuration_name}) not defined for target: #{@main_target.name}" unless configuration 44 | end 45 | 46 | Log.warn("Using defined build configuration: #{configuration_name} instead of the scheme's default one (#{default_configuration_name})") 47 | @configuration_name = configuration_name 48 | end 49 | 50 | @build_settings_by_target = {} 51 | end 52 | 53 | def uses_xcode_auto_codesigning? 54 | main_target = @targets[0] 55 | 56 | # target attributes 57 | target_id = main_target.uuid 58 | 59 | project = Xcodeproj::Project.open(@targets_container_project_path) 60 | attributes = project.root_object.attributes['TargetAttributes'] 61 | if attributes 62 | target_attributes = attributes[target_id] || {} 63 | return true if target_attributes['ProvisioningStyle'] == 'Automatic' 64 | end 65 | 66 | # target build settings 67 | main_target.build_configuration_list.build_configurations.each do |build_configuration| 68 | next unless build_configuration.name == @configuration_name 69 | 70 | build_settings = build_configuration.build_settings 71 | return true if build_settings['CODE_SIGN_STYLE'] == 'Automatic' 72 | end 73 | 74 | false 75 | end 76 | 77 | def project_codesign_identity 78 | codesign_identity = nil 79 | 80 | @targets.each do |target| 81 | target_name = target.name 82 | 83 | target_identity = target_codesign_identity(target_name) 84 | Log.debug("#{target_name} codesign identity: #{target_identity} ") 85 | 86 | if target_identity.to_s.empty? 87 | Log.warn("no CODE_SIGN_IDENTITY build settings found for target: #{target_name}") 88 | next 89 | end 90 | 91 | if codesign_identity.nil? 92 | codesign_identity = target_identity 93 | next 94 | end 95 | 96 | unless codesign_identites_match?(codesign_identity, target_identity) 97 | Log.warn("target codesign identity: #{target_identity} does not match to the already registered codesign identity: #{codesign_identity}") 98 | codesign_identity = nil 99 | break 100 | end 101 | 102 | codesign_identity = exact_codesign_identity(codesign_identity, target_identity) 103 | end 104 | 105 | raise 'failed to determine project code sign identity' unless codesign_identity 106 | 107 | codesign_identity 108 | end 109 | 110 | def project_team_id 111 | team_id = nil 112 | 113 | project = Xcodeproj::Project.open(@targets_container_project_path) 114 | attributes = project.root_object.attributes['TargetAttributes'] || {} 115 | 116 | @targets.each do |target| 117 | target_name = target.name 118 | 119 | current_team_id = target_team_id(target_name) 120 | Log.debug("#{target_name} target build settings team id: #{current_team_id}") 121 | 122 | unless current_team_id 123 | Log.warn("no DEVELOPMENT_TEAM build settings found for target: #{target_name}, checking target attributes...") 124 | 125 | target_attributes = attributes[target.uuid] if attributes 126 | target_attributes_team_id = target_attributes['DevelopmentTeam'] if target_attributes 127 | Log.debug("#{target_name} target attributes team id: #{target_attributes_team_id}") 128 | 129 | unless target_attributes_team_id 130 | Log.warn("no DevelopmentTeam target attribute found for target: #{target_name}") 131 | next 132 | end 133 | 134 | current_team_id = target_attributes_team_id 135 | end 136 | 137 | if team_id.nil? 138 | team_id = current_team_id 139 | next 140 | end 141 | 142 | next if team_id == current_team_id 143 | 144 | Log.warn("target team id: #{current_team_id} does not match to the already registered team id: #{team_id}") 145 | team_id = nil 146 | break 147 | end 148 | 149 | team_id 150 | end 151 | 152 | def target_bundle_id(target_name) 153 | build_settings = xcodebuild_target_build_settings(target_name) 154 | 155 | bundle_id = build_settings['PRODUCT_BUNDLE_IDENTIFIER'] 156 | return bundle_id if bundle_id 157 | 158 | Log.debug("PRODUCT_BUNDLE_IDENTIFIER env not found in 'xcodebuild -showBuildSettings -project \"#{@targets_container_project_path}\" -target \"#{target_name}\" -configuration \"#{@configuration_name}\"' command's output") 159 | Log.debug("checking the Info.plist file's CFBundleIdentifier property...") 160 | 161 | info_plist_path = build_settings['INFOPLIST_FILE'] 162 | raise 'failed to to determine bundle id: xcodebuild -showBuildSettings does not contains PRODUCT_BUNDLE_IDENTIFIER nor INFOPLIST_FILE' unless info_plist_path 163 | 164 | info_plist_path = File.expand_path(info_plist_path, File.dirname(@targets_container_project_path)) 165 | info_plist = Plist.parse_xml(info_plist_path) 166 | bundle_id = info_plist['CFBundleIdentifier'] 167 | raise 'failed to to determine bundle id: xcodebuild -showBuildSettings does not contains PRODUCT_BUNDLE_IDENTIFIER nor Info.plist' if bundle_id.to_s.empty? 168 | 169 | return bundle_id unless bundle_id.to_s.include?('$') 170 | 171 | Log.warn("CFBundleIdentifier defined with variable: #{bundle_id}, trying to resolve it...") 172 | resolved = resolve_bundle_id(bundle_id, build_settings) 173 | Log.warn("resolved CFBundleIdentifier: #{resolved}") 174 | resolved 175 | end 176 | 177 | def target_entitlements(target_name) 178 | settings = xcodebuild_target_build_settings(target_name) 179 | entitlements_path = settings['CODE_SIGN_ENTITLEMENTS'] 180 | return if entitlements_path.to_s.empty? 181 | 182 | project_dir = File.dirname(@targets_container_project_path) 183 | entitlements_path = File.join(project_dir, entitlements_path) 184 | Plist.parse_xml(entitlements_path) 185 | end 186 | 187 | def force_code_sign_properties(target_name, development_team, code_sign_identity, provisioning_profile_uuid) 188 | target_found = false 189 | configuration_found = false 190 | 191 | project = Xcodeproj::Project.open(@targets_container_project_path) 192 | project.targets.each do |target_obj| 193 | next unless target_obj.name == target_name 194 | target_found = true 195 | 196 | # force target attributes 197 | target_id = target_obj.uuid 198 | attributes = project.root_object.attributes['TargetAttributes'] 199 | if attributes 200 | target_attributes = attributes[target_id] 201 | if target_attributes 202 | target_attributes['ProvisioningStyle'] = 'Manual' 203 | target_attributes['DevelopmentTeam'] = development_team 204 | target_attributes['DevelopmentTeamName'] = '' 205 | end 206 | end 207 | 208 | # force target build settings 209 | target_obj.build_configuration_list.build_configurations.each do |build_configuration| 210 | next unless build_configuration.name == @configuration_name 211 | configuration_found = true 212 | 213 | build_settings = build_configuration.build_settings 214 | codesign_settings = { 215 | 'CODE_SIGN_STYLE' => 'Manual', 216 | 'DEVELOPMENT_TEAM' => development_team, 217 | 218 | 'CODE_SIGN_IDENTITY' => code_sign_identity, 219 | 'CODE_SIGN_IDENTITY[sdk=iphoneos*]' => code_sign_identity, 220 | 221 | 'PROVISIONING_PROFILE_SPECIFIER' => '', 222 | 'PROVISIONING_PROFILE' => provisioning_profile_uuid, 223 | 'PROVISIONING_PROFILE[sdk=iphoneos*]' => provisioning_profile_uuid 224 | } 225 | build_settings.merge!(codesign_settings) 226 | 227 | Log.print(JSON.pretty_generate(codesign_settings)) 228 | end 229 | end 230 | 231 | raise "target (#{target_name}) not found in project: #{@targets_container_project_path}" unless target_found 232 | raise "configuration (#{@configuration_name}) does not exist in project: #{@targets_container_project_path}" unless configuration_found 233 | 234 | project.save 235 | end 236 | 237 | private 238 | 239 | def unique_targets(targets) 240 | names = {} 241 | targets.reject do |target| 242 | found = names.key?(target.name) 243 | names[target.name] = true 244 | found 245 | end 246 | end 247 | 248 | def read_scheme_and_container_project(scheme_name) 249 | project_paths = [@project_path] 250 | project_paths += contained_projects if workspace? 251 | 252 | project_paths.each do |project_path| 253 | schema_path = File.join(project_path, 'xcshareddata', 'xcschemes', scheme_name + '.xcscheme') 254 | next unless File.exist?(schema_path) 255 | 256 | return Xcodeproj::XCScheme.new(schema_path), project_path 257 | end 258 | 259 | raise "project (#{@project_path}) does not contain scheme: #{scheme_name}" 260 | end 261 | 262 | def archivable_target_and_container_project(buildable_references, scheme_container_project_dir) 263 | buildable_references.each do |reference| 264 | next if reference.target_name.to_s.empty? 265 | next if reference.target_referenced_container.to_s.empty? 266 | 267 | container = reference.target_referenced_container.sub(/^container:/, '') 268 | next if container.empty? 269 | 270 | target_project_path = File.expand_path(container, scheme_container_project_dir) 271 | next unless File.exist?(target_project_path) 272 | 273 | project = Xcodeproj::Project.open(target_project_path) 274 | target = project.targets.find { |t| t.name == reference.target_name } 275 | next unless target 276 | next unless runnable_target?(target) 277 | 278 | return target, target_project_path 279 | end 280 | end 281 | 282 | def read_scheme_archivable_target_and_container_project(scheme, scheme_container_project_path) 283 | build_action = scheme.build_action 284 | return nil unless build_action 285 | 286 | entries = build_action.entries || [] 287 | return nil if entries.empty? 288 | 289 | entries = entries.select(&:build_for_archiving?) || [] 290 | return nil if entries.empty? 291 | 292 | scheme_container_project_dir = File.dirname(scheme_container_project_path) 293 | 294 | entries.each do |entry| 295 | buildable_references = entry.buildable_references || [] 296 | next if buildable_references.empty? 297 | 298 | target, target_project_path = archivable_target_and_container_project(buildable_references, scheme_container_project_dir) 299 | next if target.nil? || target_project_path.nil? 300 | 301 | return target, target_project_path 302 | end 303 | 304 | nil 305 | end 306 | 307 | def collect_dependent_targets(target, dependent_targets = []) 308 | dependent_targets << target 309 | 310 | dependencies = target.dependencies || [] 311 | return dependent_targets if dependencies.empty? 312 | 313 | dependencies.each do |dependency| 314 | dependent_target = dependency.target 315 | next unless dependent_target 316 | next unless runnable_target?(dependent_target) 317 | 318 | collect_dependent_targets(dependent_target, dependent_targets) 319 | end 320 | 321 | dependent_targets 322 | end 323 | 324 | def target_codesign_identity(target_name) 325 | settings = xcodebuild_target_build_settings(target_name) 326 | settings['CODE_SIGN_IDENTITY'] 327 | end 328 | 329 | def target_team_id(target_name) 330 | settings = xcodebuild_target_build_settings(target_name) 331 | settings['DEVELOPMENT_TEAM'] 332 | end 333 | 334 | def workspace? 335 | extname = File.extname(@project_path) 336 | extname == '.xcworkspace' 337 | end 338 | 339 | def contained_projects 340 | return [@project_path] unless workspace? 341 | 342 | workspace = Xcodeproj::Workspace.new_from_xcworkspace(@project_path) 343 | workspace_dir = File.dirname(@project_path) 344 | project_paths = [] 345 | workspace.file_references.each do |ref| 346 | pth = ref.path 347 | next unless File.extname(pth) == '.xcodeproj' 348 | next if pth.end_with?('Pods/Pods.xcodeproj') 349 | 350 | project_path = File.expand_path(pth, workspace_dir) 351 | project_paths << project_path 352 | end 353 | 354 | project_paths 355 | end 356 | 357 | def runnable_target?(target) 358 | return false unless target.is_a?(Xcodeproj::Project::Object::PBXNativeTarget) 359 | 360 | product_reference = target.product_reference 361 | return false unless product_reference 362 | 363 | product_reference.path.end_with?('.app', '.appex') 364 | end 365 | 366 | def project_targets_map 367 | project_targets = {} 368 | 369 | project_paths = contained_projects 370 | project_paths.each do |project_path| 371 | targets = [] 372 | 373 | project = Xcodeproj::Project.open(project_path) 374 | project.targets.each do |target| 375 | next unless runnable_target?(target) 376 | 377 | targets.push(target.name) 378 | end 379 | 380 | project_targets[project_path] = targets 381 | end 382 | 383 | project_targets 384 | end 385 | 386 | def xcodebuild_target_build_settings(target) 387 | raise 'xcodebuild -showBuildSettings failed: target not specified' if target.to_s.empty? 388 | 389 | settings = @build_settings_by_target[target] 390 | return settings if settings 391 | 392 | cmd = [ 393 | 'xcodebuild', 394 | '-showBuildSettings', 395 | '-project', 396 | "\"#{@targets_container_project_path}\"", 397 | '-target', 398 | "\"#{target}\"", 399 | '-configuration', 400 | "\"#{@configuration_name}\"" 401 | ].join(' ') 402 | 403 | Log.debug("$ #{cmd}") 404 | out = `#{cmd}` 405 | raise "#{cmd} failed, out: #{out}" unless $CHILD_STATUS.success? 406 | 407 | settings = {} 408 | lines = out.split(/\n/) 409 | lines.each do |line| 410 | line = line.strip 411 | next unless line.include?(' = ') 412 | 413 | split = line.split(' = ') 414 | next unless split.length == 2 415 | 416 | value = split[1].strip 417 | next if value.empty? 418 | 419 | key = split[0].strip 420 | next if key.empty? 421 | 422 | settings[key] = value 423 | end 424 | 425 | @build_settings_by_target[target] = settings 426 | settings 427 | end 428 | 429 | def resolve_bundle_id(bundle_id, build_settings) 430 | # Bitrise.$(PRODUCT_NAME:rfc1034identifier) 431 | pattern = /(.*)\$\((.*)\)(.*)/ 432 | matches = bundle_id.match(pattern) 433 | raise "failed to resolve bundle id (#{bundle_id}): does not conforms to: /(.*)$\(.*\)(.*)/" unless matches 434 | 435 | captures = matches.captures 436 | prefix = captures[0] 437 | suffix = captures[2] 438 | env_key = captures[1] 439 | split = env_key.split(':') 440 | raise "failed to resolve bundle id (#{bundle_id}): failed to determine settings key" if split.empty? 441 | 442 | env_key = split[0] 443 | env_value = build_settings[env_key] 444 | raise "failed to resolve bundle id (#{bundle_id}): build settings not found with key: (#{env_key})" if env_value.to_s.empty? 445 | 446 | prefix + env_value + suffix 447 | end 448 | 449 | # 'iPhone Developer' should match to 'iPhone Developer: Bitrise Bot (ABCD)' 450 | def codesign_identites_match?(identity1, identity2) 451 | return true if identity1.downcase.include?(identity2.downcase) 452 | return true if identity2.downcase.include?(identity1.downcase) 453 | false 454 | end 455 | 456 | # 'iPhone Developer: Bitrise Bot (ABCD)' is exact compared to 'iPhone Developer' 457 | def exact_codesign_identity(identity1, identity2) 458 | return nil unless codesign_identites_match?(identity1, identity2) 459 | identity1.length > identity2.length ? identity1 : identity2 460 | end 461 | end 462 | -------------------------------------------------------------------------------- /spec/fixtures/project/foo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 138AC3B7206AB34200A6A7BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AC3B6206AB34200A6A7BF /* AppDelegate.swift */; }; 11 | 138AC3B9206AB34200A6A7BF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AC3B8206AB34200A6A7BF /* ViewController.swift */; }; 12 | 138AC3BC206AB34200A6A7BF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 138AC3BA206AB34200A6A7BF /* Main.storyboard */; }; 13 | 138AC3BE206AB34200A6A7BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 138AC3BD206AB34200A6A7BF /* Assets.xcassets */; }; 14 | 138AC3C1206AB34200A6A7BF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 138AC3BF206AB34200A6A7BF /* LaunchScreen.storyboard */; }; 15 | 138AC3CC206AB34300A6A7BF /* fooTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AC3CB206AB34300A6A7BF /* fooTests.swift */; }; 16 | 138AC3D7206AB34300A6A7BF /* fooUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AC3D6206AB34300A6A7BF /* fooUITests.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 138AC3C8206AB34300A6A7BF /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 138AC3AB206AB34200A6A7BF /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = 138AC3B2206AB34200A6A7BF; 25 | remoteInfo = foo; 26 | }; 27 | 138AC3D3206AB34300A6A7BF /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 138AC3AB206AB34200A6A7BF /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 138AC3B2206AB34200A6A7BF; 32 | remoteInfo = foo; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | 138AC3B3206AB34200A6A7BF /* foo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = foo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 138AC3B6206AB34200A6A7BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 39 | 138AC3B8206AB34200A6A7BF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 40 | 138AC3BB206AB34200A6A7BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 41 | 138AC3BD206AB34200A6A7BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 42 | 138AC3C0206AB34200A6A7BF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 43 | 138AC3C2206AB34200A6A7BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 138AC3C7206AB34300A6A7BF /* fooTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fooTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 138AC3CB206AB34300A6A7BF /* fooTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fooTests.swift; sourceTree = ""; }; 46 | 138AC3CD206AB34300A6A7BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 138AC3D2206AB34300A6A7BF /* fooUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = fooUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 138AC3D6206AB34300A6A7BF /* fooUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = fooUITests.swift; sourceTree = ""; }; 49 | 138AC3D8206AB34300A6A7BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 138AC3B0206AB34200A6A7BF /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 138AC3C4206AB34300A6A7BF /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 138AC3CF206AB34300A6A7BF /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 138AC3AA206AB34200A6A7BF = { 78 | isa = PBXGroup; 79 | children = ( 80 | 138AC3B5206AB34200A6A7BF /* foo */, 81 | 138AC3CA206AB34300A6A7BF /* fooTests */, 82 | 138AC3D5206AB34300A6A7BF /* fooUITests */, 83 | 138AC3B4206AB34200A6A7BF /* Products */, 84 | ); 85 | sourceTree = ""; 86 | }; 87 | 138AC3B4206AB34200A6A7BF /* Products */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 138AC3B3206AB34200A6A7BF /* foo.app */, 91 | 138AC3C7206AB34300A6A7BF /* fooTests.xctest */, 92 | 138AC3D2206AB34300A6A7BF /* fooUITests.xctest */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | 138AC3B5206AB34200A6A7BF /* foo */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 138AC3B6206AB34200A6A7BF /* AppDelegate.swift */, 101 | 138AC3B8206AB34200A6A7BF /* ViewController.swift */, 102 | 138AC3BA206AB34200A6A7BF /* Main.storyboard */, 103 | 138AC3BD206AB34200A6A7BF /* Assets.xcassets */, 104 | 138AC3BF206AB34200A6A7BF /* LaunchScreen.storyboard */, 105 | 138AC3C2206AB34200A6A7BF /* Info.plist */, 106 | ); 107 | path = foo; 108 | sourceTree = ""; 109 | }; 110 | 138AC3CA206AB34300A6A7BF /* fooTests */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 138AC3CB206AB34300A6A7BF /* fooTests.swift */, 114 | 138AC3CD206AB34300A6A7BF /* Info.plist */, 115 | ); 116 | path = fooTests; 117 | sourceTree = ""; 118 | }; 119 | 138AC3D5206AB34300A6A7BF /* fooUITests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 138AC3D6206AB34300A6A7BF /* fooUITests.swift */, 123 | 138AC3D8206AB34300A6A7BF /* Info.plist */, 124 | ); 125 | path = fooUITests; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 138AC3B2206AB34200A6A7BF /* foo */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 138AC3DB206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "foo" */; 134 | buildPhases = ( 135 | 138AC3AF206AB34200A6A7BF /* Sources */, 136 | 138AC3B0206AB34200A6A7BF /* Frameworks */, 137 | 138AC3B1206AB34200A6A7BF /* Resources */, 138 | ); 139 | buildRules = ( 140 | ); 141 | dependencies = ( 142 | ); 143 | name = foo; 144 | productName = foo; 145 | productReference = 138AC3B3206AB34200A6A7BF /* foo.app */; 146 | productType = "com.apple.product-type.application"; 147 | }; 148 | 138AC3C6206AB34300A6A7BF /* fooTests */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = 138AC3DE206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "fooTests" */; 151 | buildPhases = ( 152 | 138AC3C3206AB34300A6A7BF /* Sources */, 153 | 138AC3C4206AB34300A6A7BF /* Frameworks */, 154 | 138AC3C5206AB34300A6A7BF /* Resources */, 155 | ); 156 | buildRules = ( 157 | ); 158 | dependencies = ( 159 | 138AC3C9206AB34300A6A7BF /* PBXTargetDependency */, 160 | ); 161 | name = fooTests; 162 | productName = fooTests; 163 | productReference = 138AC3C7206AB34300A6A7BF /* fooTests.xctest */; 164 | productType = "com.apple.product-type.bundle.unit-test"; 165 | }; 166 | 138AC3D1206AB34300A6A7BF /* fooUITests */ = { 167 | isa = PBXNativeTarget; 168 | buildConfigurationList = 138AC3E1206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "fooUITests" */; 169 | buildPhases = ( 170 | 138AC3CE206AB34300A6A7BF /* Sources */, 171 | 138AC3CF206AB34300A6A7BF /* Frameworks */, 172 | 138AC3D0206AB34300A6A7BF /* Resources */, 173 | ); 174 | buildRules = ( 175 | ); 176 | dependencies = ( 177 | 138AC3D4206AB34300A6A7BF /* PBXTargetDependency */, 178 | ); 179 | name = fooUITests; 180 | productName = fooUITests; 181 | productReference = 138AC3D2206AB34300A6A7BF /* fooUITests.xctest */; 182 | productType = "com.apple.product-type.bundle.ui-testing"; 183 | }; 184 | /* End PBXNativeTarget section */ 185 | 186 | /* Begin PBXProject section */ 187 | 138AC3AB206AB34200A6A7BF /* Project object */ = { 188 | isa = PBXProject; 189 | attributes = { 190 | LastSwiftUpdateCheck = 0920; 191 | LastUpgradeCheck = 0920; 192 | ORGANIZATIONNAME = Bitrise; 193 | TargetAttributes = { 194 | 138AC3B2206AB34200A6A7BF = { 195 | CreatedOnToolsVersion = 9.2; 196 | ProvisioningStyle = Automatic; 197 | }; 198 | 138AC3C6206AB34300A6A7BF = { 199 | CreatedOnToolsVersion = 9.2; 200 | ProvisioningStyle = Automatic; 201 | TestTargetID = 138AC3B2206AB34200A6A7BF; 202 | }; 203 | 138AC3D1206AB34300A6A7BF = { 204 | CreatedOnToolsVersion = 9.2; 205 | ProvisioningStyle = Automatic; 206 | TestTargetID = 138AC3B2206AB34200A6A7BF; 207 | }; 208 | }; 209 | }; 210 | buildConfigurationList = 138AC3AE206AB34200A6A7BF /* Build configuration list for PBXProject "foo" */; 211 | compatibilityVersion = "Xcode 8.0"; 212 | developmentRegion = en; 213 | hasScannedForEncodings = 0; 214 | knownRegions = ( 215 | en, 216 | Base, 217 | ); 218 | mainGroup = 138AC3AA206AB34200A6A7BF; 219 | productRefGroup = 138AC3B4206AB34200A6A7BF /* Products */; 220 | projectDirPath = ""; 221 | projectRoot = ""; 222 | targets = ( 223 | 138AC3B2206AB34200A6A7BF /* foo */, 224 | 138AC3C6206AB34300A6A7BF /* fooTests */, 225 | 138AC3D1206AB34300A6A7BF /* fooUITests */, 226 | ); 227 | }; 228 | /* End PBXProject section */ 229 | 230 | /* Begin PBXResourcesBuildPhase section */ 231 | 138AC3B1206AB34200A6A7BF /* Resources */ = { 232 | isa = PBXResourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | 138AC3C1206AB34200A6A7BF /* LaunchScreen.storyboard in Resources */, 236 | 138AC3BE206AB34200A6A7BF /* Assets.xcassets in Resources */, 237 | 138AC3BC206AB34200A6A7BF /* Main.storyboard in Resources */, 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | 138AC3C5206AB34300A6A7BF /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | 138AC3D0206AB34300A6A7BF /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXResourcesBuildPhase section */ 256 | 257 | /* Begin PBXSourcesBuildPhase section */ 258 | 138AC3AF206AB34200A6A7BF /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | 138AC3B9206AB34200A6A7BF /* ViewController.swift in Sources */, 263 | 138AC3B7206AB34200A6A7BF /* AppDelegate.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | 138AC3C3206AB34300A6A7BF /* Sources */ = { 268 | isa = PBXSourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 138AC3CC206AB34300A6A7BF /* fooTests.swift in Sources */, 272 | ); 273 | runOnlyForDeploymentPostprocessing = 0; 274 | }; 275 | 138AC3CE206AB34300A6A7BF /* Sources */ = { 276 | isa = PBXSourcesBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | 138AC3D7206AB34300A6A7BF /* fooUITests.swift in Sources */, 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | /* End PBXSourcesBuildPhase section */ 284 | 285 | /* Begin PBXTargetDependency section */ 286 | 138AC3C9206AB34300A6A7BF /* PBXTargetDependency */ = { 287 | isa = PBXTargetDependency; 288 | target = 138AC3B2206AB34200A6A7BF /* foo */; 289 | targetProxy = 138AC3C8206AB34300A6A7BF /* PBXContainerItemProxy */; 290 | }; 291 | 138AC3D4206AB34300A6A7BF /* PBXTargetDependency */ = { 292 | isa = PBXTargetDependency; 293 | target = 138AC3B2206AB34200A6A7BF /* foo */; 294 | targetProxy = 138AC3D3206AB34300A6A7BF /* PBXContainerItemProxy */; 295 | }; 296 | /* End PBXTargetDependency section */ 297 | 298 | /* Begin PBXVariantGroup section */ 299 | 138AC3BA206AB34200A6A7BF /* Main.storyboard */ = { 300 | isa = PBXVariantGroup; 301 | children = ( 302 | 138AC3BB206AB34200A6A7BF /* Base */, 303 | ); 304 | name = Main.storyboard; 305 | sourceTree = ""; 306 | }; 307 | 138AC3BF206AB34200A6A7BF /* LaunchScreen.storyboard */ = { 308 | isa = PBXVariantGroup; 309 | children = ( 310 | 138AC3C0206AB34200A6A7BF /* Base */, 311 | ); 312 | name = LaunchScreen.storyboard; 313 | sourceTree = ""; 314 | }; 315 | /* End PBXVariantGroup section */ 316 | 317 | /* Begin XCBuildConfiguration section */ 318 | 138AC3D9206AB34300A6A7BF /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 325 | CLANG_CXX_LIBRARY = "libc++"; 326 | CLANG_ENABLE_MODULES = YES; 327 | CLANG_ENABLE_OBJC_ARC = YES; 328 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_COMMA = YES; 331 | CLANG_WARN_CONSTANT_CONVERSION = YES; 332 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 333 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 334 | CLANG_WARN_EMPTY_BODY = YES; 335 | CLANG_WARN_ENUM_CONVERSION = YES; 336 | CLANG_WARN_INFINITE_RECURSION = YES; 337 | CLANG_WARN_INT_CONVERSION = YES; 338 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 339 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 342 | CLANG_WARN_STRICT_PROTOTYPES = YES; 343 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 344 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 345 | CLANG_WARN_UNREACHABLE_CODE = YES; 346 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 347 | CODE_SIGN_IDENTITY = "iPhone Developer"; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = dwarf; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | ENABLE_TESTABILITY = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu11; 353 | GCC_DYNAMIC_NO_PIC = NO; 354 | GCC_NO_COMMON_BLOCKS = YES; 355 | GCC_OPTIMIZATION_LEVEL = 0; 356 | GCC_PREPROCESSOR_DEFINITIONS = ( 357 | "DEBUG=1", 358 | "$(inherited)", 359 | ); 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 367 | MTL_ENABLE_DEBUG_INFO = YES; 368 | ONLY_ACTIVE_ARCH = YES; 369 | SDKROOT = iphoneos; 370 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 371 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 372 | }; 373 | name = Debug; 374 | }; 375 | 138AC3DA206AB34300A6A7BF /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ALWAYS_SEARCH_USER_PATHS = NO; 379 | CLANG_ANALYZER_NONNULL = YES; 380 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 390 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 397 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 398 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 399 | CLANG_WARN_STRICT_PROTOTYPES = YES; 400 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 401 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | CODE_SIGN_IDENTITY = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 407 | ENABLE_NS_ASSERTIONS = NO; 408 | ENABLE_STRICT_OBJC_MSGSEND = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu11; 410 | GCC_NO_COMMON_BLOCKS = YES; 411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 413 | GCC_WARN_UNDECLARED_SELECTOR = YES; 414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 415 | GCC_WARN_UNUSED_FUNCTION = YES; 416 | GCC_WARN_UNUSED_VARIABLE = YES; 417 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 418 | MTL_ENABLE_DEBUG_INFO = NO; 419 | SDKROOT = iphoneos; 420 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 421 | VALIDATE_PRODUCT = YES; 422 | }; 423 | name = Release; 424 | }; 425 | 138AC3DC206AB34300A6A7BF /* Debug */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 429 | CODE_SIGN_STYLE = Automatic; 430 | INFOPLIST_FILE = foo/Info.plist; 431 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 432 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.foo; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SWIFT_VERSION = 4.0; 435 | TARGETED_DEVICE_FAMILY = "1,2"; 436 | }; 437 | name = Debug; 438 | }; 439 | 138AC3DD206AB34300A6A7BF /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CODE_SIGN_STYLE = Automatic; 444 | INFOPLIST_FILE = foo/Info.plist; 445 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 446 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.foo; 447 | PRODUCT_NAME = "$(TARGET_NAME)"; 448 | SWIFT_VERSION = 4.0; 449 | TARGETED_DEVICE_FAMILY = "1,2"; 450 | }; 451 | name = Release; 452 | }; 453 | 138AC3DF206AB34300A6A7BF /* Debug */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 457 | BUNDLE_LOADER = "$(TEST_HOST)"; 458 | CODE_SIGN_STYLE = Automatic; 459 | INFOPLIST_FILE = fooTests/Info.plist; 460 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 461 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.fooTests; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | SWIFT_VERSION = 4.0; 464 | TARGETED_DEVICE_FAMILY = "1,2"; 465 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/foo.app/foo"; 466 | }; 467 | name = Debug; 468 | }; 469 | 138AC3E0206AB34300A6A7BF /* Release */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 473 | BUNDLE_LOADER = "$(TEST_HOST)"; 474 | CODE_SIGN_STYLE = Automatic; 475 | INFOPLIST_FILE = fooTests/Info.plist; 476 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 477 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.fooTests; 478 | PRODUCT_NAME = "$(TARGET_NAME)"; 479 | SWIFT_VERSION = 4.0; 480 | TARGETED_DEVICE_FAMILY = "1,2"; 481 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/foo.app/foo"; 482 | }; 483 | name = Release; 484 | }; 485 | 138AC3E2206AB34300A6A7BF /* Debug */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 489 | CODE_SIGN_STYLE = Automatic; 490 | INFOPLIST_FILE = fooUITests/Info.plist; 491 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 492 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.fooUITests; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | SWIFT_VERSION = 4.0; 495 | TARGETED_DEVICE_FAMILY = "1,2"; 496 | TEST_TARGET_NAME = foo; 497 | }; 498 | name = Debug; 499 | }; 500 | 138AC3E3206AB34300A6A7BF /* Release */ = { 501 | isa = XCBuildConfiguration; 502 | buildSettings = { 503 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 504 | CODE_SIGN_STYLE = Automatic; 505 | INFOPLIST_FILE = fooUITests/Info.plist; 506 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 507 | PRODUCT_BUNDLE_IDENTIFIER = io.bitrise.fooUITests; 508 | PRODUCT_NAME = "$(TARGET_NAME)"; 509 | SWIFT_VERSION = 4.0; 510 | TARGETED_DEVICE_FAMILY = "1,2"; 511 | TEST_TARGET_NAME = foo; 512 | }; 513 | name = Release; 514 | }; 515 | /* End XCBuildConfiguration section */ 516 | 517 | /* Begin XCConfigurationList section */ 518 | 138AC3AE206AB34200A6A7BF /* Build configuration list for PBXProject "foo" */ = { 519 | isa = XCConfigurationList; 520 | buildConfigurations = ( 521 | 138AC3D9206AB34300A6A7BF /* Debug */, 522 | 138AC3DA206AB34300A6A7BF /* Release */, 523 | ); 524 | defaultConfigurationIsVisible = 0; 525 | defaultConfigurationName = Release; 526 | }; 527 | 138AC3DB206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "foo" */ = { 528 | isa = XCConfigurationList; 529 | buildConfigurations = ( 530 | 138AC3DC206AB34300A6A7BF /* Debug */, 531 | 138AC3DD206AB34300A6A7BF /* Release */, 532 | ); 533 | defaultConfigurationIsVisible = 0; 534 | defaultConfigurationName = Release; 535 | }; 536 | 138AC3DE206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "fooTests" */ = { 537 | isa = XCConfigurationList; 538 | buildConfigurations = ( 539 | 138AC3DF206AB34300A6A7BF /* Debug */, 540 | 138AC3E0206AB34300A6A7BF /* Release */, 541 | ); 542 | defaultConfigurationIsVisible = 0; 543 | defaultConfigurationName = Release; 544 | }; 545 | 138AC3E1206AB34300A6A7BF /* Build configuration list for PBXNativeTarget "fooUITests" */ = { 546 | isa = XCConfigurationList; 547 | buildConfigurations = ( 548 | 138AC3E2206AB34300A6A7BF /* Debug */, 549 | 138AC3E3206AB34300A6A7BF /* Release */, 550 | ); 551 | defaultConfigurationIsVisible = 0; 552 | defaultConfigurationName = Release; 553 | }; 554 | /* End XCConfigurationList section */ 555 | }; 556 | rootObject = 138AC3AB206AB34200A6A7BF /* Project object */; 557 | } 558 | --------------------------------------------------------------------------------