pilot 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a669f4e5d2466f5d2bf0a596fb69a002f90058e2
4
- data.tar.gz: ae38d3e57214c987e094f3c38219e81351b6d550
3
+ metadata.gz: 3a252d1f974787effe53dd857fabc664f5266bd9
4
+ data.tar.gz: 7c878a216f10538ce757c88f4cb66bcf047316ca
5
5
  SHA512:
6
- metadata.gz: 88d326c652d0eae87aefc9df359eb2b1ba5b6c748fbee35bda9981fb3a008dea8d8d7190199b81bc4a70513839f71801cf29ad8b4a354b27f8ca8199a0254b99
7
- data.tar.gz: 69ac8e4342b4c87b420b3cc32542af9e07bc20c4cc17e22eff755d24e8bb6da7ab4e555763a36ce8e97713aaa7dae5b26e315b68008115e748be1c1db0322b52
6
+ metadata.gz: c41ab0d075f516c1764755ea9c5f986e815884a53f91622c5f0790c50508d3b0f3bc6af7e326f57d44560778d05cd1d92451e1f20bc18670e6c3172142639def
7
+ data.tar.gz: 8f46022dac75b6c64e5082f2447214d17eda3df4bb2496c375ccae279af49624e658112c5d9a6d45b9add0c50530d0e913eb6d99709acacf3fd79b3a3d4c8444
data/README.md CHANGED
@@ -13,43 +13,246 @@
13
13
  <a href="https://github.com/KrauseFx/sigh">sigh</a> &bull;
14
14
  <a href="https://github.com/KrauseFx/produce">produce</a> &bull;
15
15
  <a href="https://github.com/KrauseFx/cert">cert</a> &bull;
16
- <a href="https://github.com/KrauseFx/codes">codes</a>
16
+ <a href="https://github.com/KrauseFx/codes">codes</a> &bull;
17
+ <a href="https://github.com/fastlane/spaceship">spaceship</a> &bull;
18
+ <b>pilot</b> &bull;
19
+ <a href="https://github.com/fastlane/boarding">boarding</a>
17
20
  </p>
18
21
  -------
19
22
 
20
23
  <p align="center">
21
- <img src="assets/deliver.png">
24
+ <img src="assets/PilotTextTransparentSmall.png" width="500">
22
25
  </p>
23
26
 
24
- TestFlight CLI
27
+ Pilot
25
28
  ============
29
+ [![Twitter: @KauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx)
30
+ [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/fastlane/pilot/blob/master/LICENSE)
31
+ [![Gem](https://img.shields.io/gem/v/pilot.svg?style=flat)](http://rubygems.org/gems/pilot)
26
32
 
27
- This gem allows you to manage all important features of Apple TestFlight using a CLI.
28
33
 
29
- This includes
34
+ ###### The best way to manage your TestFlight testers and builds from your terminal
35
+
36
+ This tool allows you to manage all important features of Apple TestFlight using your terminal.
30
37
 
31
38
  - Upload new builds and distribute them to all testers
32
- - Set build information like changelog for new builds
33
- - Add new testers to your team
39
+ - List all available builds
40
+ - Add and remove beta testers
41
+ - Get information about testers, like the registered devices
42
+ - Export and import all your testers
43
+
44
+ Get in contact with the developer on Twitter: [@KrauseFx](https://twitter.com/KrauseFx)
45
+
46
+ `pilot` uses [spaceship.airforce](https://spaceship.airforce) to interact with iTunes Connect :rocket:
47
+
48
+ -------
49
+ <p align="center">
50
+ <a href="#installation">Installation</a> &bull;
51
+ <a href="#usage">Usage</a> &bull;
52
+ <a href="#tips">Tips</a> &bull;
53
+ <a href="#need-help">Need help?</a>
54
+ </p>
55
+
56
+ -------
57
+
58
+ <h5 align="center"><code>pilot</code> is part of <a href="https://fastlane.tools">fastlane</a>: connect all deployment tools into one streamlined workflow.</h5>
59
+
60
+ # Installation
61
+
62
+ sudo gem install pilot
63
+
64
+ # Usage
65
+
66
+ For all commands you can specify the Apple ID to use using `-u felix@krausefx.com`. If you execute `pilot` in a project already using [fastlane](https://fastlane.tools) the username and app identifier will automatically be determined.
67
+
68
+ ## Uploading builds
34
69
 
35
70
  To upload a new build, just run
36
71
 
37
72
  ```
38
- pizzacutter
73
+ pilot upload
39
74
  ```
40
75
 
41
76
  This will automatically look for an `ipa` in your current directory and tries to fetch the login credentials from your [fastlane setup](https://fastlane.tools).
42
77
 
43
- You'll be asked for any missing information. Additionally, you can pass all kinds of parameters to `pizzacutter`:
78
+ You'll be asked for any missing information. Additionally, you can pass all kinds of parameters to `pilot`:
44
79
 
45
80
  ```
46
- pizzacutter -u "felix@krausefx.com"
47
- --changelog "This build is better"
81
+ pilot upload -u felix@krausefx.com
48
82
  ```
49
83
 
50
- `pizzacutter` does all kinds of magic for you:
84
+ Additionally you can skip the submission of the binary, which means, the `ipa` file will only be uploaded and not distributet to testers:
85
+
86
+ ```
87
+ pilot upload --skip_submission
88
+ ```
89
+
90
+ `pilot` does all kinds of magic for you:
51
91
 
52
92
  - Automatically detects the bundle identifier from your `ipa` file
53
93
  - Automatically fetch the AppID of your app based on the bundle identifier
54
94
 
55
- This gem uses [spaceship](https://spaceship.airforce) to submit the build metadata and iTunes Transporter to upload the binary.
95
+ `pilot` uses [spaceship.airforce](https://spaceship.airforce) to submit the build metadata and the iTunes Transporter to upload the binary :rocket:
96
+
97
+ ## List builds
98
+
99
+ To list all builds for specific application use
100
+
101
+ ```
102
+ pilot list
103
+ ```
104
+
105
+ The result lists all active builds and processing builds:
106
+
107
+ ```
108
+ +-----------+---------+----------+----------+----------+
109
+ | Great App Builds |
110
+ +-----------+---------+----------+----------+----------+
111
+ | Version # | Build # | Testing | Installs | Sessions |
112
+ +-----------+---------+----------+----------+----------+
113
+ | 0.9.13 | 1 | Expired | 1 | 0 |
114
+ | 0.9.13 | 2 | Expired | 0 | 0 |
115
+ | 0.9.20 | 3 | Expired | 0 | 0 |
116
+ | 0.9.20 | 4 | Internal | 5 | 3 |
117
+ +-----------+---------+----------+----------+----------+
118
+ ```
119
+
120
+ ## Managing beta testers
121
+
122
+ ### List of Testers
123
+
124
+ This command will list all your testers, both internal and external.
125
+
126
+ ```
127
+ pilot list
128
+ ```
129
+
130
+ The output will look like this:
131
+
132
+ ```
133
+ +--------+--------+--------------------------+-----------+
134
+ | Internal Testers |
135
+ +--------+--------+--------------------------+-----------+
136
+ | First | Last | Email | # Devices |
137
+ +--------+--------+--------------------------+-----------+
138
+ | Felix | Krause | felix@krausefx.com | 2 |
139
+ +--------+--------+--------------------------+-----------+
140
+
141
+ +-----------+---------+----------------------------+-----------+
142
+ | External Testers |
143
+ +-----------+---------+----------------------------+-----------+
144
+ | First | Last | Email | # Devices |
145
+ +-----------+---------+----------------------------+-----------+
146
+ | Max | Manfred | email@email.com | 0 |
147
+ | Detlef | Müller | detlef@krausefx.com | 1 |
148
+ +-----------+---------+----------------------------+-----------+
149
+ ```
150
+
151
+ ### Add a new tester
152
+
153
+ To add a new tester to both your iTunes Connect account and to your app (if given), use the `pilot add` command. This will create a new tester (if necesssary) or add an existing tester to the app to test.
154
+
155
+ ```
156
+ pilot add email@invite.com
157
+ ```
158
+
159
+ Additionally you can specify the app identifier (if necessary):
160
+
161
+ ```
162
+ pilot add -e email@email.com -a com.krausefx.app
163
+ ```
164
+
165
+ ### Find a tester
166
+
167
+ To find a specific tester use
168
+
169
+ ```
170
+ pilot find felix@krausefx.com
171
+ ```
172
+
173
+ The resulting output will look like this:
174
+
175
+ ```
176
+ +---------------------+---------------------+
177
+ | felix@krausefx.com |
178
+ +---------------------+---------------------+
179
+ | First name | Felix |
180
+ | Last name | Krause |
181
+ | Email | felix@krausefx.com |
182
+ | Latest Version | 0.9.14 (23 |
183
+ | Latest Install Date | 03/28/15 19:00 |
184
+ | 2 Devices | • iPhone 6, iOS 8.3 |
185
+ | | • iPhone 5, iOS 7.0 |
186
+ +---------------------+---------------------+
187
+ ```
188
+
189
+ ### Remove a tester
190
+
191
+ This command will only remove external beta testers.
192
+
193
+ ```
194
+ pilot remove felix@krausefx.com
195
+ ```
196
+
197
+ ### Export testers
198
+
199
+ To export all external testers to a CSV file. Useful if you need to import tester info to another system or a new account.
200
+
201
+ ```
202
+ pilot export
203
+ ```
204
+
205
+ ### Import testers
206
+
207
+ Add external testers from a CSV file. Sample CSV file available [here](https://itunesconnect.apple.com/itc/docs/tester_import.csv).
208
+
209
+ ```
210
+ pilot import
211
+ ```
212
+
213
+ You can also specify the directory using
214
+
215
+ ```
216
+ pilot export -c ~/Desktop/testers.csv
217
+ pilot import -c ~/Desktop/testers.csv
218
+ ```
219
+
220
+ # Tips
221
+
222
+ ## [`fastlane`](https://fastlane.tools) Toolchain
223
+
224
+ - [`fastlane`](https://fastlane.tools): Connect all deployment tools into one streamlined workflow
225
+ - [`deliver`](https://github.com/KrauseFx/deliver): Upload screenshots, metadata and your app to the App Store using a single command
226
+ - [`snapshot`](https://github.com/KrauseFx/snapshot): Automate taking localized screenshots of your iOS app on every device
227
+ - [`frameit`](https://github.com/KrauseFx/frameit): Quickly put your screenshots into the right device frames
228
+ - [`PEM`](https://github.com/KrauseFx/pem): Automatically generate and renew your push notification profiles
229
+ - [`produce`](https://github.com/KrauseFx/produce): Create new iOS apps on iTunes Connect and Dev Portal using the command line
230
+ - [`cert`](https://github.com/KrauseFx/cert): Automatically create and maintain iOS code signing certificates
231
+ - [`spaceship`](https://github.com/fastlane/spaceship): Ruby library to access the Apple Dev Center and iTunes Connect
232
+ - [`pilot`](https://github.com/fastlane/pilot): The best way to manage your TestFlight testers and builds from your terminal
233
+ - [`boarding`](https://github.com/fastlane/boarding): The easiest way to invite your TestFlight beta testers
234
+
235
+ ##### [Like this tool? Be the first to know about updates and new fastlane tools](https://tinyletter.com/krausefx)
236
+
237
+ ## How is my password stored?
238
+
239
+ `pilot` uses the [CredentialsManager](https://github.com/fastlane/CredentialsManager) from `fastlane`.
240
+
241
+ # Need help?
242
+ - If there is a technical problem with `pilot`, submit an issue.
243
+ - I'm available for contract work - drop me an email: pilot@krausefx.com
244
+
245
+ # License
246
+ This project is licensed under the terms of the MIT license. See the LICENSE file.
247
+
248
+ > This project and all fastlane tools are in no way affiliated with Apple Inc. This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. All fastlane tools run on your own computer or server, so your credentials or other sensitive information will never leave your own computer. You are responsible for how you use fastlane tools.
249
+
250
+ # Contributing
251
+
252
+ 1. Create an issue to start a discussion about your idea
253
+ 2. Fork it (https://github.com/fastlane/pilot/fork)
254
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
255
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
256
+ 5. Push to the branch (`git push origin my-new-feature`)
257
+ 6. Create a new Pull Request
258
+
data/bin/pilot CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
- $:.push File.expand_path("../../lib", __FILE__)
2
+ $LOAD_PATH.push File.expand_path("../../lib", __FILE__)
3
3
 
4
- require 'pilot'
5
- Pilot::CommandsGenerator.start
4
+ require "pilot"
5
+ require "pilot/commands_generator"
6
+ Pilot::CommandsGenerator.start
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <package xmlns="http://apple.com/itunes/importer" version="software4.7">
3
+ <software_assets apple_id="<%= @data[:apple_id] %>">
4
+ <asset type="bundle">
5
+ <data_file>
6
+ <size><%= @data[:file_size] %></size>
7
+ <file_name><%= @data[:ipa_path] %></file_name>
8
+ <checksum type="md5"><%= @data[:md5] %></checksum>
9
+ </data_file>
10
+ </asset>
11
+ </software_assets>
12
+ </package>
data/lib/pilot.rb CHANGED
@@ -1,7 +1,15 @@
1
- require 'json'
2
- require 'pilot/version'
1
+ require "json"
2
+ require "pilot/version"
3
+ require "pilot/manager"
4
+ require "pilot/build_manager"
5
+ require "pilot/tester_manager"
6
+ require "pilot/tester_importer"
7
+ require "pilot/tester_exporter"
8
+ require "pilot/package_builder"
3
9
 
4
- require 'fastlane_core'
10
+ require "fastlane_core"
11
+ require "spaceship"
12
+ require "terminal-table"
5
13
 
6
14
  module Pilot
7
15
  Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
@@ -0,0 +1,87 @@
1
+ module Pilot
2
+ class BuildManager < Manager
3
+ def upload(options)
4
+ start(options)
5
+
6
+ Helper.log.info "Ready to upload new build to TestFlight (App: #{config[:apple_id]})...".green
7
+
8
+ package_path = PackageBuilder.new.generate(apple_id: config[:apple_id],
9
+ ipa_path: config[:ipa],
10
+ package_path: "/tmp") # TODO: Config
11
+
12
+ result = FastlaneCore::ItunesTransporter.new.upload(config[:apple_id], package_path)
13
+ if result
14
+ Helper.log.info "Successfully uploaded the new binary to iTunes Connect"
15
+
16
+ unless config[:skip_submission]
17
+ upload_date = wait_for_processing_build
18
+ distribute_build(upload_date)
19
+
20
+ Helper.log.info "Successfully distribute build to beta testers 🚀"
21
+ end
22
+ else
23
+ raise "Error uploading ipa file, more information see above".red
24
+ end
25
+ end
26
+
27
+ def list(options)
28
+ start(options)
29
+ if config[:apple_id].to_s.length == 0 and config[:app_identifier].to_s.length == 0
30
+ config[:app_identifier] = ask("App Identifier: ")
31
+ end
32
+
33
+ rows = app.all_processing_builds.collect { |build| describe_build(build) }
34
+ rows = rows + app.builds.collect { |build| describe_build(build) }
35
+
36
+ puts Terminal::Table.new(
37
+ title: "#{app.name} Builds".green,
38
+ headings: ["Version #", "Build #", "Testing", "Installs", "Sessions"],
39
+ rows: rows
40
+ )
41
+ end
42
+
43
+ private
44
+ def describe_build(build)
45
+ row = [build.train_version,
46
+ build.build_version,
47
+ build.testing_status,
48
+ build.install_count,
49
+ build.session_count]
50
+
51
+ return row
52
+ end
53
+
54
+ # This method will takes care of checking for the processing builds every few seconds
55
+ # @return [Integer] The upload date
56
+ def wait_for_processing_build
57
+ # the upload date of the new buid
58
+ # we use it to identify the build
59
+ upload_date = nil
60
+ loop do
61
+ Helper.log.info "Waiting for iTunes Connect to process the new build"
62
+ sleep 5
63
+ builds = app.all_processing_builds
64
+ break if builds.count == 0
65
+ upload_date = builds.last.upload_date
66
+ end
67
+
68
+ if upload_date
69
+ Helper.log.info "Build successfully processed by iTunes Connect".green
70
+ return upload_date
71
+ else
72
+ raise "Error: Seems like iTunes Connect didn't properly pre-process the binary".red
73
+ end
74
+ end
75
+
76
+ def distribute_build(upload_date)
77
+ Helper.log.info "Distributing new build to testers"
78
+
79
+ current_build = app.builds.find do |build|
80
+ build.upload_date == upload_date
81
+ end
82
+
83
+ # First, enable TestFlight beta testing for this train
84
+ current_build.build_train.update_testing_status!(true)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,123 @@
1
+ require "commander"
2
+ require "pilot/options"
3
+ require "fastlane_core"
4
+
5
+ HighLine.track_eof = false
6
+
7
+ module Pilot
8
+ class CommandsGenerator
9
+ include Commander::Methods
10
+
11
+ FastlaneCore::CommanderGenerator.new.generate(Pilot::Options.available_options)
12
+
13
+ def self.start
14
+ FastlaneCore::UpdateChecker.start_looking_for_update("pilot")
15
+ new.run
16
+ ensure
17
+ FastlaneCore::UpdateChecker.show_update_status("pilot", Pilot::VERSION)
18
+ end
19
+
20
+ def convert_options(options)
21
+ o = options.__hash__.dup
22
+ o.delete(:verbose)
23
+ o
24
+ end
25
+
26
+ def handle_email(config, _args)
27
+ config[:email] ||= _args.first
28
+ config[:email] ||= ask("Email address of the tester: ".yellow)
29
+ end
30
+
31
+ def run
32
+ program :version, Pilot::VERSION
33
+ program :description, Pilot::DESCRIPTION
34
+ program :help, "Author", "Felix Krause <pilot@krausefx.com>"
35
+ program :help, "Website", "https://fastlane.tools"
36
+ program :help, "GitHub", "https://github.com/fastlane/pilot"
37
+ program :help_formatter, :compact
38
+
39
+ global_option("--verbose") { $verbose = true }
40
+
41
+ always_trace!
42
+
43
+ command :upload do |c|
44
+ c.syntax = "pilot upload"
45
+ c.description = "Uploads a new binary to Apple TestFlight"
46
+ c.action do |_args, options|
47
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
48
+ Pilot::BuildManager.new.upload(config)
49
+ end
50
+ end
51
+
52
+ command :builds do |c|
53
+ c.syntax = "pilot builds"
54
+ c.description = "Lists all builds for given application"
55
+ c.action do |_args, options|
56
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
57
+ Pilot::BuildManager.new.list(config)
58
+ end
59
+ end
60
+
61
+ command :add do |c|
62
+ c.syntax = "pilot add"
63
+ c.description = "Adds a new external tester to a specific app (if given). This will also add an existing tester to an app."
64
+ c.action do |_args, options|
65
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
66
+ handle_email(config, _args)
67
+ Pilot::TesterManager.new.add_tester(config)
68
+ end
69
+ end
70
+
71
+ command :list do |c|
72
+ c.syntax = "pilot list"
73
+ c.description = "Lists all registered testers, both internal and external"
74
+ c.action do |_args, options|
75
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
76
+ Pilot::TesterManager.new.list_testers(config)
77
+ end
78
+ end
79
+
80
+ command :find do |c|
81
+ c.syntax = "pilot find"
82
+ c.description = "Find a tester (internal or external) by their email address"
83
+ c.action do |_args, options|
84
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
85
+ handle_email(config, _args)
86
+ Pilot::TesterManager.new.find_tester(config)
87
+ end
88
+ end
89
+
90
+ command :remove do |c|
91
+ c.syntax = "pilot remove"
92
+ c.description = "Remove an external tester by their email address"
93
+ c.action do |_args, options|
94
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
95
+ handle_email(config, _args)
96
+ Pilot::TesterManager.new.remove_tester(config)
97
+ end
98
+ end
99
+
100
+ command :export do |c|
101
+ c.syntax = "pilot export"
102
+ c.description = "Exports all external testers to a CSV file"
103
+ c.action do |_args, options|
104
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
105
+ Pilot::TesterExporter.new.export_testers(config)
106
+ end
107
+ end
108
+
109
+ command :import do |c|
110
+ c.syntax = "pilot import"
111
+ c.description = "Create external testers from a CSV file"
112
+ c.action do |_args, options|
113
+ config = FastlaneCore::Configuration.create(Pilot::Options.available_options, convert_options(options))
114
+ Pilot::TesterImporter.new.import_testers(config)
115
+ end
116
+ end
117
+
118
+ default_command :help
119
+
120
+ run!
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,61 @@
1
+ require "fastlane_core"
2
+
3
+ module Pilot
4
+ class Manager
5
+ def start(options)
6
+ return if @config # to not login multiple times
7
+ @config = options
8
+ login
9
+ end
10
+
11
+ def login
12
+ user = config[:username]
13
+ user ||= ENV["DELIVER_USERNAME"]
14
+ user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
15
+ CredentialsManager::PasswordManager.shared_manager(user) if user
16
+
17
+ Helper.log.info "Login to iTunes Connect"
18
+ Spaceship::Tunes.login(user, CredentialsManager::PasswordManager.shared_manager(user).password)
19
+ Helper.log.info "Login successful"
20
+ end
21
+
22
+ # The app object we're currently using
23
+ def app
24
+ @apple_id ||= fetch_app_id
25
+
26
+ unless (@app ||= Spaceship::Application.find(@apple_id))
27
+ raise "Could not find app with #{(config[:apple_id] || config[:app_identifier])}"
28
+ end
29
+ return @app
30
+ end
31
+
32
+ # Access the current configuration
33
+ def config
34
+ @config
35
+ end
36
+
37
+ # Config Related
38
+ ################
39
+
40
+ def fetch_app_id
41
+ return @apple_id if @apple_id
42
+ config[:app_identifier] = fetch_app_identifier
43
+
44
+ if config[:app_identifier]
45
+ @app ||= Spaceship::Application.find(config[:app_identifier])
46
+ app_id ||= @app.apple_id
47
+ end
48
+
49
+ app_id ||= ask("Could not automatically find the app ID, please enter it here (e.g. 956814360): ")
50
+
51
+ return app_id
52
+ end
53
+
54
+ def fetch_app_identifier
55
+ result = config[:app_identifier]
56
+ result ||= FastlaneCore::IpaFileAnalyser.fetch_app_identifier(config[:ipa])
57
+ result ||= ask("Please enter the app's bundle identifier: ")
58
+ return result
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,88 @@
1
+ require "fastlane_core"
2
+ require "credentials_manager"
3
+
4
+ module Pilot
5
+ class Options
6
+ def self.available_options
7
+ @@options ||= [
8
+ FastlaneCore::ConfigItem.new(key: :username,
9
+ short_option: "-u",
10
+ env_name: "PILOT_USERNAME",
11
+ description: "Your Apple ID Username",
12
+ default_value: ENV["DELIVER_USER"] || CredentialsManager::AppfileConfig.try_fetch_value(:apple_id),
13
+ verify_block: proc do |value|
14
+ CredentialsManager::PasswordManager.shared_manager(value)
15
+ end),
16
+
17
+ FastlaneCore::ConfigItem.new(key: :ipa,
18
+ short_option: "-i",
19
+ env_name: "PILOT_IPA",
20
+ description: "Path to the ipa file to upload",
21
+ default_value: Dir["*.ipa"].first,
22
+ verify_block: proc do |value|
23
+ fail "Could not find ipa file at path '#{value}'" unless File.exist? value
24
+ fail "'#{value}' doesn't seem to be an ipa file" unless value.end_with? ".ipa"
25
+ end),
26
+ FastlaneCore::ConfigItem.new(key: :skip_submission,
27
+ short_option: "-s",
28
+ env_name: "PILOT_SKIP_SUBMISSION",
29
+ description: "Skip the distributing action of pilot and only upload the ipa file",
30
+ default_value: false),
31
+ FastlaneCore::ConfigItem.new(key: :app_identifier,
32
+ short_option: "-a",
33
+ env_name: "PILOT_APP_IDENTIFIER",
34
+ description: "The bundle identifier of the app to upload or manage testers (optional)",
35
+ optional: true,
36
+ default_value: ENV["TESTFLIGHT_APP_IDENTITIFER"] || CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier),
37
+ verify_block: proc do |_value|
38
+ end),
39
+ FastlaneCore::ConfigItem.new(key: :apple_id,
40
+ short_option: "-p",
41
+ env_name: "PILOT_APPLE_ID",
42
+ description: "The unique App ID provided by iTunes Connect",
43
+ optional: true,
44
+ default_value: ENV["TESTFLIGHT_APPLE_ID"],
45
+ verify_block: proc do |_value|
46
+ end),
47
+ FastlaneCore::ConfigItem.new(key: :first_name,
48
+ short_option: "-f",
49
+ env_name: "PILOT_TESTER_FIRST_NAME",
50
+ description: "The tester's first name",
51
+ optional: true,
52
+ verify_block: proc do |_value|
53
+ end),
54
+ FastlaneCore::ConfigItem.new(key: :last_name,
55
+ short_option: "-l",
56
+ env_name: "PILOT_TESTER_LAST_NAME",
57
+ description: "The tester's last name",
58
+ optional: true,
59
+ verify_block: proc do |_value|
60
+ end),
61
+ FastlaneCore::ConfigItem.new(key: :email,
62
+ short_option: "-e",
63
+ env_name: "PILOT_TESTER_EMAIL",
64
+ description: "The tester's email",
65
+ optional: true,
66
+ verify_block: proc do |_value|
67
+ raise "Please pass a valid email address" unless _value.include?"@"
68
+ end),
69
+ FastlaneCore::ConfigItem.new(key: :group_name,
70
+ short_option: "-g",
71
+ env_name: "PILOT_TESTER_GROUP",
72
+ description: "Group to add the tester to",
73
+ optional: true,
74
+ verify_block: proc do |_value|
75
+ end),
76
+ FastlaneCore::ConfigItem.new(key: :testers_file_path,
77
+ short_option: "-c",
78
+ env_name: "PILOT_TESTERS_FILE",
79
+ description: "Path to a CSV file of testers",
80
+ default_value: "./testers.csv",
81
+ optional: true,
82
+ verify_block: proc do |_value|
83
+ end)
84
+
85
+ ]
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
1
+ require "digest/md5"
2
+
3
+ module Pilot
4
+ class PackageBuilder
5
+ METADATA_FILE_NAME = "metadata.xml"
6
+
7
+ attr_accessor :package_path
8
+
9
+ def generate(apple_id: nil, ipa_path: nil, package_path: nil)
10
+ self.package_path = File.join(package_path, "#{apple_id}.itmsp")
11
+ FileUtils.rm_rf self.package_path rescue nil
12
+ FileUtils.mkdir_p self.package_path
13
+
14
+ lib_path = Helper.gem_path("pilot")
15
+
16
+ ipa_path = copy_ipa(ipa_path)
17
+ @data = {
18
+ apple_id: apple_id,
19
+ file_size: File.size(ipa_path),
20
+ ipa_path: File.basename(ipa_path), # this is only the base name as the ipa is inside the package
21
+ md5: Digest::MD5.hexdigest(File.read(ipa_path))
22
+ }
23
+
24
+ xml_path = File.join(lib_path, "lib/assets/XMLTemplate.xml.erb")
25
+ xml = ERB.new(File.read(xml_path)).result(binding) # http://www.rrn.dk/rubys-erb-templating-system
26
+
27
+ File.write(File.join(self.package_path, METADATA_FILE_NAME), xml)
28
+ Helper.log.info "Wrote XML data to '#{self.package_path}'".green if $verbose
29
+
30
+ return package_path
31
+ end
32
+
33
+ private
34
+ def copy_ipa(ipa_path)
35
+ ipa_file_name = Digest::MD5.hexdigest(ipa_path)
36
+ resulting_path = File.join(self.package_path, "#{ipa_file_name}.ipa")
37
+ FileUtils.cp(ipa_path, resulting_path)
38
+
39
+ return resulting_path
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ require "fastlane_core"
2
+
3
+ module Pilot
4
+ class TesterExporter < Manager
5
+ def export_testers(options)
6
+
7
+ raise "Export file path is required".red unless options[:testers_file_path]
8
+
9
+ start(options)
10
+ require 'csv'
11
+
12
+ testers = Spaceship::Tunes::Tester::External.all
13
+
14
+ file = config[:testers_file_path]
15
+
16
+ CSV.open(file, "w") do |csv|
17
+
18
+ csv << ['First', 'Last', 'Email', 'Devices', 'Groups', 'Installed Version', 'Install Date']
19
+
20
+ testers.each do |tester|
21
+
22
+ groups = tester.raw_data.get("groups")
23
+
24
+ group_names = ""
25
+ if groups && groups.length > 0
26
+ names = groups.map { |group| group["name"]["value"] }
27
+ group_names = names.join(';')
28
+ end
29
+
30
+ csv << [tester.first_name, tester.last_name, tester.email, tester.devices.count, group_names]
31
+ end
32
+
33
+ Helper.log.info "Successfully exported CSV to #{file}".green
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ require "fastlane_core"
2
+
3
+ module Pilot
4
+ class TesterImporter < Manager
5
+ def import_testers(options)
6
+ raise "Import file path is required".red unless options[:testers_file_path]
7
+
8
+ start(options)
9
+
10
+ require 'csv'
11
+
12
+ file = config[:testers_file_path]
13
+ tester_manager = Pilot::TesterManager.new
14
+ imported_tester_count = 0
15
+
16
+ is_first = true
17
+ CSV.foreach(file, "r") do |row|
18
+ if is_first
19
+ is_first = false
20
+ next
21
+ end
22
+
23
+ first_name, last_name, email = row
24
+
25
+ unless email
26
+ Helper.log.error "No email found in row: #{row}".red
27
+ next
28
+ end
29
+
30
+ # Add this the existing config hash to pass it to the TesterManager
31
+ config[:first_name] = first_name
32
+ config[:last_name] = last_name
33
+ config[:email] = email
34
+
35
+ begin
36
+ tester_manager.add_tester(config)
37
+ imported_tester_count += 1
38
+ rescue => ex
39
+ # do nothing, move on to the next row
40
+ end
41
+
42
+ end
43
+
44
+ Helper.log.info "Successfully imported #{imported_tester_count} testers from #{file}".green
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,137 @@
1
+ require "fastlane_core"
2
+
3
+ module Pilot
4
+ class TesterManager < Manager
5
+ def add_tester(options)
6
+ start(options)
7
+
8
+ begin
9
+ tester = Spaceship::Tunes::Tester::Internal.find(config[:email])
10
+ tester ||= Spaceship::Tunes::Tester::External.find(config[:email])
11
+
12
+ if tester
13
+ Helper.log.info "Existing tester #{tester.email}".green
14
+ else
15
+ tester = Spaceship::Tunes::Tester::External.create!(email: config[:email],
16
+ first_name: config[:first_name],
17
+ last_name: config[:last_name],
18
+ group: config[:group_name])
19
+ Helper.log.info "Successfully invited tester: #{tester.email}".green
20
+ end
21
+
22
+ app_filter = (config[:apple_id] || config[:app_identifier])
23
+ if app_filter
24
+ begin
25
+ app = Spaceship::Application.find(app_filter)
26
+ raise "Couldn't find app with '#{app_filter}'" unless app
27
+ tester.add_to_app!(app.apple_id)
28
+ Helper.log.info "Successfully added tester to app #{app_filter}".green
29
+ rescue => ex
30
+ Helper.log.error "Could not add #{tester.email} to app: #{ex}".red
31
+ raise ex
32
+ end
33
+ end
34
+ rescue => ex
35
+ Helper.log.error "Could not create tester #{config[:email]}".red
36
+ raise ex
37
+ end
38
+ end
39
+
40
+ def find_tester(options)
41
+ start(options)
42
+
43
+ tester = Spaceship::Tunes::Tester::Internal.find(config[:email])
44
+ tester ||= Spaceship::Tunes::Tester::External.find(config[:email])
45
+
46
+ raise "Tester #{config[:email]} not found".red unless tester
47
+
48
+ describe_tester(tester)
49
+ return tester
50
+ end
51
+
52
+ def remove_tester(options)
53
+ start(options)
54
+
55
+ tester = Spaceship::Tunes::Tester::External.find(config[:email])
56
+ tester ||= Spaceship::Tunes::Tester::Internal.find(config[:email])
57
+
58
+ if tester
59
+ tester.delete!
60
+ Helper.log.info "Successully removed tester #{tester.email}".green
61
+ else
62
+ Helper.log.error "Tester not found: #{config[:email]}".red
63
+ end
64
+ end
65
+
66
+ def list_testers(options)
67
+ start(options)
68
+ require 'terminal-table'
69
+
70
+ list(Spaceship::Tunes::Tester::Internal.all, "Internal Testers")
71
+ puts "" # new line
72
+ list(Spaceship::Tunes::Tester::External.all, "External Testers")
73
+ end
74
+
75
+ private
76
+
77
+ def list(all_testers, title)
78
+ rows = []
79
+ all_testers.each do |tester|
80
+ rows << [tester.first_name, tester.last_name, tester.email, tester.devices.count]
81
+ end
82
+
83
+ puts Terminal::Table.new(
84
+ title: title.green,
85
+ headings: ["First", "Last", "Email", "Devices"],
86
+ rows: rows
87
+ )
88
+ end
89
+
90
+ # Print out all the details of a specific tester
91
+ def describe_tester(tester)
92
+ return unless tester
93
+
94
+ rows = []
95
+
96
+ rows << ["First name", tester.first_name]
97
+ rows << ["Last name", tester.last_name]
98
+ rows << ["Email", tester.email]
99
+
100
+ groups = tester.raw_data.get("groups")
101
+
102
+ if groups && groups.length > 0
103
+ group_names = groups.map { |group| group["name"]["value"] }
104
+ rows << ["Groups", group_names.join(', ')]
105
+ end
106
+
107
+ if tester.latest_install_date
108
+ latest_installed_version = tester.latest_installed_version_number
109
+ latest_installed_short_version = tester.latest_installed_build_number
110
+ pretty_date = Time.at((tester.latest_install_date / 1000)).strftime("%m/%d/%y %H:%M")
111
+
112
+ rows << ["Latest Version", "#{latest_installed_version} (#{latest_installed_short_version})"]
113
+ rows << ["Latest Install Date", pretty_date]
114
+ end
115
+
116
+ if tester.devices.length == 0
117
+ rows << ["Devices", "No devices"]
118
+ else
119
+ rows << ["#{tester.devices.count} Devices", ""]
120
+ tester.devices.each do |device|
121
+ current = "\u2022 #{device['model']}, iOS #{device['osVersion']}"
122
+
123
+ if rows.last[1].length == 0
124
+ rows.last[1] = current
125
+ else
126
+ rows << ["", current]
127
+ end
128
+ end
129
+ end
130
+
131
+ puts Terminal::Table.new(
132
+ title: tester.email.green,
133
+ rows: rows
134
+ )
135
+ end
136
+ end
137
+ end
data/lib/pilot/version.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  module Pilot
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
+ DESCRIPTION = "The best way to manage your TestFlight testers and builds from your terminal"
3
4
  end
metadata CHANGED
@@ -1,142 +1,170 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pilot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felix Krause
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-14 00:00:00.000000000 Z
11
+ date: 2015-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fastlane_core
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.7.2
19
+ version: 0.9.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.7.2
26
+ version: 0.9.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: spaceship
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.3.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: credentials_manager
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
- - - '>='
45
+ - - ">="
32
46
  - !ruby/object:Gem::Version
33
47
  version: 0.3.0
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
- - - '>='
52
+ - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: 0.3.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.5
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.5
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: bundler
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
- - - '>='
73
+ - - ">="
46
74
  - !ruby/object:Gem::Version
47
75
  version: '0'
48
76
  type: :development
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
- - - '>='
80
+ - - ">="
53
81
  - !ruby/object:Gem::Version
54
82
  version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rake
57
85
  requirement: !ruby/object:Gem::Requirement
58
86
  requirements:
59
- - - '>='
87
+ - - ">="
60
88
  - !ruby/object:Gem::Version
61
89
  version: '0'
62
90
  type: :development
63
91
  prerelease: false
64
92
  version_requirements: !ruby/object:Gem::Requirement
65
93
  requirements:
66
- - - '>='
94
+ - - ">="
67
95
  - !ruby/object:Gem::Version
68
96
  version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rspec
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
- - - ~>
101
+ - - "~>"
74
102
  - !ruby/object:Gem::Version
75
103
  version: 3.1.0
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
- - - ~>
108
+ - - "~>"
81
109
  - !ruby/object:Gem::Version
82
110
  version: 3.1.0
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: pry
85
113
  requirement: !ruby/object:Gem::Requirement
86
114
  requirements:
87
- - - '>='
115
+ - - ">="
88
116
  - !ruby/object:Gem::Version
89
117
  version: '0'
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
- - - '>='
122
+ - - ">="
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: yard
99
127
  requirement: !ruby/object:Gem::Requirement
100
128
  requirements:
101
- - - ~>
129
+ - - "~>"
102
130
  - !ruby/object:Gem::Version
103
131
  version: 0.8.7.4
104
132
  type: :development
105
133
  prerelease: false
106
134
  version_requirements: !ruby/object:Gem::Requirement
107
135
  requirements:
108
- - - ~>
136
+ - - "~>"
109
137
  - !ruby/object:Gem::Version
110
138
  version: 0.8.7.4
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: webmock
113
141
  requirement: !ruby/object:Gem::Requirement
114
142
  requirements:
115
- - - ~>
143
+ - - "~>"
116
144
  - !ruby/object:Gem::Version
117
145
  version: 1.19.0
118
146
  type: :development
119
147
  prerelease: false
120
148
  version_requirements: !ruby/object:Gem::Requirement
121
149
  requirements:
122
- - - ~>
150
+ - - "~>"
123
151
  - !ruby/object:Gem::Version
124
152
  version: 1.19.0
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: coveralls
127
155
  requirement: !ruby/object:Gem::Requirement
128
156
  requirements:
129
- - - '>='
157
+ - - ">="
130
158
  - !ruby/object:Gem::Version
131
159
  version: '0'
132
160
  type: :development
133
161
  prerelease: false
134
162
  version_requirements: !ruby/object:Gem::Requirement
135
163
  requirements:
136
- - - '>='
164
+ - - ">="
137
165
  - !ruby/object:Gem::Version
138
166
  version: '0'
139
- description: The unofficial TestFlight CLI to upload builds and manage testers
167
+ description: The best way to manage your TestFlight testers and builds from your terminal
140
168
  email:
141
169
  - pilot@krausefx.com
142
170
  executables:
@@ -147,7 +175,16 @@ files:
147
175
  - LICENSE
148
176
  - README.md
149
177
  - bin/pilot
178
+ - lib/assets/XMLTemplate.xml.erb
150
179
  - lib/pilot.rb
180
+ - lib/pilot/build_manager.rb
181
+ - lib/pilot/commands_generator.rb
182
+ - lib/pilot/manager.rb
183
+ - lib/pilot/options.rb
184
+ - lib/pilot/package_builder.rb
185
+ - lib/pilot/tester_exporter.rb
186
+ - lib/pilot/tester_importer.rb
187
+ - lib/pilot/tester_manager.rb
151
188
  - lib/pilot/version.rb
152
189
  homepage: https://fastlane.tools
153
190
  licenses:
@@ -159,19 +196,19 @@ require_paths:
159
196
  - lib
160
197
  required_ruby_version: !ruby/object:Gem::Requirement
161
198
  requirements:
162
- - - '>='
199
+ - - ">="
163
200
  - !ruby/object:Gem::Version
164
201
  version: 2.0.0
165
202
  required_rubygems_version: !ruby/object:Gem::Requirement
166
203
  requirements:
167
- - - '>='
204
+ - - ">="
168
205
  - !ruby/object:Gem::Version
169
206
  version: '0'
170
207
  requirements: []
171
208
  rubyforge_project:
172
- rubygems_version: 2.4.7
209
+ rubygems_version: 2.4.6
173
210
  signing_key:
174
211
  specification_version: 4
175
- summary: The unofficial TestFlight CLI to upload builds and manage testers
212
+ summary: The best way to manage your TestFlight testers and builds from your terminal
176
213
  test_files: []
177
214
  has_rdoc: