├── .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 | [](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 |
--------------------------------------------------------------------------------