TestFlightExporter 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +2 -0
- data/bin/testflight_exporter +40 -0
- data/lib/helpers.rb +50 -0
- data/lib/testflight_exporter/version.rb +3 -0
- data/lib/testflight_exporter.rb +264 -0
- data/testflight_exporter.gemspec +26 -0
- data/tfexplorer.rb +157 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 47abbef6ad6404d51f0b614e50de932db2717d82
|
4
|
+
data.tar.gz: 29e77927e625b7d47870f4e5c8d2452135e7ef87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2ea24bf47268d11890ef98c6e3b97d18296f18da34e0bbe17f3ca7f6a1a8c2940ef4444ec2e348136aa8adc45654eb7855bde03e350012621357666a095f3e68
|
7
|
+
data.tar.gz: b4e15239a2ba25da69590ae2a3446b3bdc4079eb87860e436b5d334791412372cf1ff9cfa1ff114c3d76b268c2a17303f54baa86b416aa7efa1fde5ad4cb04aa
|
data/.gitignore
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
### Ruby template
|
2
|
+
*.gem
|
3
|
+
*.rbc
|
4
|
+
/.config
|
5
|
+
/coverage/
|
6
|
+
/InstalledFiles
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
## Documentation cache and generated files:
|
14
|
+
/.yardoc/
|
15
|
+
/_yardoc/
|
16
|
+
/doc/
|
17
|
+
/rdoc/
|
18
|
+
|
19
|
+
## Environment normalisation:
|
20
|
+
/.bundle/
|
21
|
+
/lib/bundler/man/
|
22
|
+
|
23
|
+
# for a library or gem, you might want to ignore these files since the code is
|
24
|
+
# intended to run in multiple environments; otherwise, check them in:
|
25
|
+
Gemfile.lock
|
26
|
+
# .ruby-version
|
27
|
+
# .ruby-gemset
|
28
|
+
|
29
|
+
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Touchwonders B.V.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# TestFlight Exporter
|
2
|
+
**TL;DR**: TestFlight Exporter is a simple CLI tool that downloads archived builds from your TestFlightapp.com account.
|
3
|
+
|
4
|
+
After years of faithful service, the original TestFlight is closing down on February 26th 2015. To ease the transition to a new beta distribution system, TestFlight provides a way to export existing teams and testers. A way to download your .ipa files, however, is lacking. That's where TestFlightExporter comes in!
|
5
|
+
|
6
|
+
TestFlight Exporter can download the builds, including release notes, from your account and stores them in a nice folder structure on your local machine.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
TestFlight Exporter is available as a gem, which you can easily install by executing the following command in your terminal:
|
11
|
+
|
12
|
+
$ gem install testflight_exporter
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
TestFlight Exporter is available as a simple CLI tool.
|
17
|
+
You can invoke it by typing `tfexporter` on your terminal.
|
18
|
+
|
19
|
+
Follow the setup assistent, which will configure the current TestFlight Exporter run to your needs. All selected builds and release notes will be saved in a folder named `out`.
|
20
|
+
|
21
|
+
**Warning**: Depending on the number of builds you have in your TestFlight account, TestFlight Exporter could consume a lot of data/bandwidth.
|
22
|
+
|
23
|
+
## How does this thing work?
|
24
|
+
|
25
|
+
TestFlight doesn't provide any API to perform such task like exporting your .ipa's. TestFlight Exporter uses [mechanize](https://github.com/sparklemotion/mechanize) to automatically find and follow the download links on the TestFlight website.
|
26
|
+
|
27
|
+
TestFlight Exporter uses your credentials to access your TestFlight account (over https). Other than that, your credentials do not leave your machine.
|
28
|
+
|
29
|
+
## Incorporating TestFlight Exporter into your project
|
30
|
+
If you want to use the functionality of TestFlight Exporter directly rather than as a command-line tool, you can include it into your ruby product and use it from your code.
|
31
|
+
|
32
|
+
To include it in your project, add this line to your application's Gemfile:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
gem 'testflight_exporter'
|
36
|
+
```
|
37
|
+
|
38
|
+
And then execute:
|
39
|
+
|
40
|
+
$ bundle install
|
41
|
+
|
42
|
+
## License
|
43
|
+
|
44
|
+
TestFlight exporter is available under the MIT license. See the LICENSE file for more info.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
|
5
|
+
require 'commander'
|
6
|
+
require 'highline'
|
7
|
+
require_relative '../lib/testflight_exporter/version'
|
8
|
+
require_relative '../lib/testflight_exporter'
|
9
|
+
|
10
|
+
HighLine.track_eof = false
|
11
|
+
|
12
|
+
|
13
|
+
class TestFlightExporterApplication
|
14
|
+
include Commander::Methods
|
15
|
+
|
16
|
+
def run
|
17
|
+
program :version, Osiris::VERSION
|
18
|
+
program :description, 'CLI for \'Test Flight Exporter\' - Migrate all your IPA automatically on your Mac'
|
19
|
+
program :help, 'Author', 'Fabio Milano'
|
20
|
+
program :help, 'Website', 'http://www.touchwonders.com'
|
21
|
+
program :help_formatter, :compact
|
22
|
+
|
23
|
+
always_trace!
|
24
|
+
|
25
|
+
command :migrate do |c|
|
26
|
+
c.syntax = 'tfexporter migrate'
|
27
|
+
c.description = 'Helps you setting up all requirements to run a migration.'
|
28
|
+
|
29
|
+
c.action do |args, options|
|
30
|
+
TestFlightExporter::Setup.new.setup
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
default_command :migrate
|
35
|
+
|
36
|
+
run!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
TestFlightExporterApplication.new.run
|
data/lib/helpers.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
|
4
|
+
class String
|
5
|
+
def classify
|
6
|
+
self.split('_').collect!{ |w| w.capitalize }.join
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
module TestFlightExporter
|
13
|
+
module Helper
|
14
|
+
|
15
|
+
# Logging happens using this method
|
16
|
+
def self.log
|
17
|
+
@@log ||= Logger.new(STDOUT)
|
18
|
+
|
19
|
+
@@log.formatter = proc do |severity, datetime, progname, msg|
|
20
|
+
string = "#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S.%2N')}]: "
|
21
|
+
second = "#{msg}\n"
|
22
|
+
|
23
|
+
if severity == "DEBUG"
|
24
|
+
string = string.magenta
|
25
|
+
elsif severity == "INFO"
|
26
|
+
string = string.white
|
27
|
+
elsif severity == "WARN"
|
28
|
+
string = string.yellow
|
29
|
+
elsif severity == "ERROR"
|
30
|
+
string = string.red
|
31
|
+
elsif severity == "FATAL"
|
32
|
+
string = string.red.bold
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
[string, second].join("")
|
37
|
+
end
|
38
|
+
|
39
|
+
@@log
|
40
|
+
end
|
41
|
+
|
42
|
+
# EXIT HANDLERS
|
43
|
+
|
44
|
+
# Print error text with error format and exit with in input error_code (default=1)
|
45
|
+
def self.exit_with_error (message, error_code=1)
|
46
|
+
log.error message.red
|
47
|
+
exit (error_code)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'colored'
|
4
|
+
|
5
|
+
require_relative 'helpers'
|
6
|
+
|
7
|
+
module TestFlightExporter
|
8
|
+
|
9
|
+
include Helper
|
10
|
+
|
11
|
+
class Setup
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@agent = Mechanize.new
|
15
|
+
@team_list = Hash.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
"Welcome to Osiris Migration Tool."
|
20
|
+
puts "Before we start I need some information about your TestFlight account."
|
21
|
+
|
22
|
+
@username = ask("Enter your TestFlight username: ") { |q| q.echo = true }
|
23
|
+
@password = ask("Enter your TestFlight password: ") { |q| q.echo = "*" }
|
24
|
+
@path = ask("Enter your output folder where all the IPAs will be downloaded: "){ |q| q.echo = true }
|
25
|
+
|
26
|
+
# Validate ouput folder
|
27
|
+
if File.directory?(@path)
|
28
|
+
# The in input path already exist we prefer to fail instead of using overriding policies
|
29
|
+
Helper.exit_with_error "\"#{@path}\" is an existing directory. Please specify another output folder".red
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
# Initialize Test Flight login page
|
34
|
+
login_page_url = "https://testflightapp.com/login/"
|
35
|
+
|
36
|
+
@agent.get(login_page_url) { |page| process_login_page page }
|
37
|
+
end
|
38
|
+
|
39
|
+
def process_login_page page
|
40
|
+
|
41
|
+
# Init login process
|
42
|
+
Helper.log.debug 'Login...'.yellow
|
43
|
+
|
44
|
+
login_form = page.forms.first # by pretty printing the page this is safe catch
|
45
|
+
|
46
|
+
login_field = login_form.field_with(:name => 'username')
|
47
|
+
password_field = login_form.field_with(:name => 'password')
|
48
|
+
|
49
|
+
login_field.value = @username
|
50
|
+
password_field.value = @password
|
51
|
+
|
52
|
+
login_form.submit
|
53
|
+
|
54
|
+
@agent.get("https://testflightapp.com/dashboard/applications/") do |dashboard_page|
|
55
|
+
|
56
|
+
@dashboard_page = dashboard_page
|
57
|
+
|
58
|
+
@dashboard_page.links.each do |ll|
|
59
|
+
# Retrieve current team
|
60
|
+
if ll.attributes.attributes['class']
|
61
|
+
@current_team = ll if ll.attributes.attributes['class'].text.eql? "dropdown-toggle team-menu"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Retrieve other team id list
|
65
|
+
@team_list.merge!({ll.text=>ll.attributes['data-team-id']}) if ll.attributes['data-team-id']
|
66
|
+
end
|
67
|
+
|
68
|
+
# If we don't have any current team something went wrong with the authentication process
|
69
|
+
if (@current_team.nil? || @current_team.text.empty?)
|
70
|
+
Helper.exit_with_error "Something went wrong during authentication process."
|
71
|
+
end
|
72
|
+
|
73
|
+
if (@team_list.count > 1)
|
74
|
+
|
75
|
+
# We have multiple teams for current account hence we present a nice menu selection to the user
|
76
|
+
choose do |menu|
|
77
|
+
menu.prompt = "Please choose your team? "
|
78
|
+
|
79
|
+
@team_list.each do |team_name, team_id|
|
80
|
+
menu.choice(team_name) do |choice|
|
81
|
+
process_teams false, choice
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
menu.choice("All teams") { process_teams true }
|
86
|
+
end
|
87
|
+
else
|
88
|
+
# process current team
|
89
|
+
puts ""
|
90
|
+
Helper.log.info "This could take a while... ☕️".green
|
91
|
+
Helper.log.info "Processing team: #{@current_team}".blue
|
92
|
+
|
93
|
+
process_dashboard_page dashboard_page
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def process_teams process_all_teams, team_name=nil
|
100
|
+
|
101
|
+
if process_all_teams
|
102
|
+
# process current team
|
103
|
+
puts ""
|
104
|
+
Helper.log.info "This could take a while... ☕️".green
|
105
|
+
Helper.log.info "Processing team: #{@current_team}".blue
|
106
|
+
|
107
|
+
process_dashboard_page @dashboard_page
|
108
|
+
|
109
|
+
# process other teams
|
110
|
+
@team_list.each do |team_name, team_id|
|
111
|
+
switch_to_team_id team_id, team_name
|
112
|
+
process_dashboard_page @agent.get("https://testflightapp.com/dashboard/applications/")
|
113
|
+
end
|
114
|
+
else
|
115
|
+
if @current_team.eql? team_name
|
116
|
+
# process current team
|
117
|
+
Helper.log.info "Processing team: #{@current_team}".blue
|
118
|
+
|
119
|
+
process_dashboard_page @dashboard_page
|
120
|
+
return
|
121
|
+
else
|
122
|
+
if @team_list["#{team_name}"].nil?
|
123
|
+
Helper.exit_with_error "Sorry, I can\'t find #{team_name} in your teams.".red
|
124
|
+
return
|
125
|
+
end
|
126
|
+
|
127
|
+
switch_to_team_id @team_list["#{team_name}"], team_name
|
128
|
+
|
129
|
+
# process current team
|
130
|
+
|
131
|
+
process_dashboard_page @agent.get("https://testflightapp.com/dashboard/applications/")
|
132
|
+
return
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def switch_to_team_id team_id, team_name
|
139
|
+
team_switch = @dashboard_page.forms.first
|
140
|
+
team_id_field = team_switch.field_with(:name => 'team')
|
141
|
+
team_id_field.value = team_id
|
142
|
+
|
143
|
+
begin
|
144
|
+
team_switch.submit
|
145
|
+
@current_team = team_name
|
146
|
+
Helper.log.info "Processing team: #{@current_team}".blue
|
147
|
+
rescue Exception => e
|
148
|
+
Helper.exit_with_error e
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_dashboard_page dashboard_page
|
155
|
+
app_link_pattern = /\/dashboard\/applications\/(.*?)\/token\//
|
156
|
+
dashboard_page.links.each do |link|
|
157
|
+
link.href =~ app_link_pattern
|
158
|
+
if $1 != nil
|
159
|
+
Helper.log.debug "Builds page for #{$1}...".magenta
|
160
|
+
|
161
|
+
@agent.get "https://testflightapp.com/dashboard/applications/#{$1}/builds/" do |builds_page|
|
162
|
+
|
163
|
+
# Processing app information to retrieve additional information on current application
|
164
|
+
app_information_link = builds_page.link_with(:text => "App Information")
|
165
|
+
|
166
|
+
process_app_information app_information_link.href
|
167
|
+
|
168
|
+
# Collection of all pages for current build
|
169
|
+
inner_pages = Array.new
|
170
|
+
|
171
|
+
builds_page.links.each do |ll|
|
172
|
+
inner_pages.push(ll.href) if ll.href =~ /\?page=*/ unless inner_pages.include?(ll.href)
|
173
|
+
end
|
174
|
+
|
175
|
+
number_of_pages = inner_pages.count + 1
|
176
|
+
Helper.log.debug "Page 1 of #{number_of_pages}".magenta
|
177
|
+
puts ""
|
178
|
+
|
179
|
+
# Process current build page
|
180
|
+
process_builds_page builds_page
|
181
|
+
|
182
|
+
# Cycle over remaning build pages
|
183
|
+
i = 2
|
184
|
+
inner_pages.each do |page|
|
185
|
+
puts ""
|
186
|
+
Helper.log.debug "Page #{i} of #{number_of_pages}".magenta
|
187
|
+
|
188
|
+
process_builds_page @agent.get "https://testflightapp.com#{page}"
|
189
|
+
|
190
|
+
i = i+1
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
def process_app_information app_information_page
|
200
|
+
@agent.get "https://testflightapp.com#{app_information_page}" do |app_information|
|
201
|
+
# Looking up the bundle identifier
|
202
|
+
@current_bundle_identifier = app_information.at("strong:contains('BundleID')").parent.parent.at("td").next.text
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def process_builds_page page
|
207
|
+
body = page.body
|
208
|
+
build_pages = body.scan /<tr class="goversion pointer" id="\/dashboard\/builds\/report\/(.*?)\/">/
|
209
|
+
|
210
|
+
build_pages.each do |build_id|
|
211
|
+
@agent.get "https://testflightapp.com/dashboard/builds/complete/#{build_id.first}/" do |build_page|
|
212
|
+
# Retrieve current app name
|
213
|
+
@app_name = page.at("h2").text
|
214
|
+
|
215
|
+
process_build_page build_page
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def process_build_page page
|
221
|
+
build_link = page.links_with(:dom_class => 'bitly').first
|
222
|
+
@agent.get("https://www.testflightapp.com#{build_link.href}") { |install_page| process_install_page install_page}
|
223
|
+
end
|
224
|
+
|
225
|
+
def process_install_page page
|
226
|
+
# we need to figure out what kind of build is that
|
227
|
+
release_note = page.search('.clearfix').at("p").text
|
228
|
+
|
229
|
+
Helper.log.debug "RELEASE NOTE".magenta
|
230
|
+
Helper.log.debug release_note.magenta
|
231
|
+
|
232
|
+
ipa_link = page.link_with(:text => "download the IPA.")
|
233
|
+
ipa_link = page.link_with(:text => "download the IPA") if ipa_link.nil?
|
234
|
+
|
235
|
+
if ipa_link.nil?
|
236
|
+
Helper.log.warn "No IPA link found. Do you have permission for current application?".yellow
|
237
|
+
else
|
238
|
+
download_build(ipa_link, "ipa", release_note)
|
239
|
+
end
|
240
|
+
|
241
|
+
puts ""
|
242
|
+
end
|
243
|
+
|
244
|
+
def download_build link, file_ext, release_note
|
245
|
+
|
246
|
+
link.href =~ /\/dashboard\/ipa\/(.*?)\//
|
247
|
+
filename = "#{$1}.#{file_ext}"
|
248
|
+
|
249
|
+
file_url = "https://www.testflightapp.com#{link.href}"
|
250
|
+
Helper.log.debug "Downloading #{file_url}...".magenta
|
251
|
+
|
252
|
+
dirname = File.dirname("#{@path}/#{@current_team}")
|
253
|
+
|
254
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
255
|
+
|
256
|
+
dirname = File.dirname("#{@path}/#{@current_team}/#{@current_bundle_identifier} builds")
|
257
|
+
|
258
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
259
|
+
|
260
|
+
@agent.get(file_url).save("#{@path}/#{@current_team}/#{@current_bundle_identifier} builds/#{filename}")
|
261
|
+
File.open("#{@path}/#{@current_team}/#{@current_bundle_identifier} builds/#{$1}.txt", 'w') {|f| f.write(release_note) }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'testflight_exporter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "TestFlightExporter"
|
8
|
+
spec.version = Osiris::VERSION
|
9
|
+
spec.authors = ["Fabio Milano"]
|
10
|
+
spec.email = ["fabio@touchwonders.com"]
|
11
|
+
spec.summary = %q{A simple tool that helps you migrating your TestFlight build to your local environment}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_dependency "mechanize", '~> 2.7' # to parse www pages
|
20
|
+
spec.add_dependency 'highline', '~> 1.6' # user inputs (e.g. passwords)
|
21
|
+
spec.add_dependency 'colored' # coloured terminal output
|
22
|
+
spec.add_dependency 'commander', '~> 4.2' # CLI parser
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
end
|
data/tfexplorer.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'mechanize'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
@agent = Mechanize.new
|
7
|
+
|
8
|
+
def process_login_page page
|
9
|
+
|
10
|
+
puts 'Login...'
|
11
|
+
|
12
|
+
login_form = page.forms.first # by pretty printing the page this is safe catch
|
13
|
+
|
14
|
+
login_field = login_form.field_with(:name => 'username')
|
15
|
+
password_field = login_form.field_with(:name => 'password')
|
16
|
+
|
17
|
+
login_field.value = 'fabio@touchwonders.com'
|
18
|
+
password_field.value = 'touchwondersworld'
|
19
|
+
|
20
|
+
login_form.submit
|
21
|
+
|
22
|
+
puts 'Dashboard...'
|
23
|
+
|
24
|
+
team_list = Hash.new
|
25
|
+
@current_team = String.new
|
26
|
+
|
27
|
+
@agent.get("https://testflightapp.com/dashboard/applications/") do |dashboard_page|
|
28
|
+
dashboard_page.links.each do |ll|
|
29
|
+
# Retrieve current team
|
30
|
+
if ll.attributes.attributes['class']
|
31
|
+
@current_team = ll if ll.attributes.attributes['class'].text.eql? "dropdown-toggle team-menu"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retrieve other team id list
|
35
|
+
team_list.merge!({ll.attributes['data-team-id']=>ll.text}) if ll.attributes['data-team-id']
|
36
|
+
end
|
37
|
+
|
38
|
+
# process current team
|
39
|
+
puts "Processing team: #{@current_team}"
|
40
|
+
|
41
|
+
process_dashboard_page dashboard_page
|
42
|
+
|
43
|
+
# process other teams
|
44
|
+
team_list.each do |team_id, team_name|
|
45
|
+
team_switch = dashboard_page.forms.first
|
46
|
+
team_id_field = team_switch.field_with(:name => 'team')
|
47
|
+
team_id_field.value = team_id
|
48
|
+
team_switch.submit
|
49
|
+
@current_team = team_name
|
50
|
+
|
51
|
+
process_dashboard_page @agent.get("https://testflightapp.com/dashboard/applications/")
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_dashboard_page dashboard_page
|
58
|
+
app_link_pattern = /\/dashboard\/applications\/(.*?)\/token\//
|
59
|
+
dashboard_page.links.each do |link|
|
60
|
+
link.href =~ app_link_pattern
|
61
|
+
if $1 != nil
|
62
|
+
puts "Builds page for #{$1}..."
|
63
|
+
@agent.get "https://testflightapp.com/dashboard/applications/#{$1}/builds/" do |builds_page|
|
64
|
+
|
65
|
+
# Collection of all pages for current build
|
66
|
+
inner_pages = Array.new
|
67
|
+
|
68
|
+
builds_page.links.each do |ll|
|
69
|
+
inner_pages.push(ll.href) if ll.href =~ /\?page=*/ unless inner_pages.include?(ll.href)
|
70
|
+
end
|
71
|
+
|
72
|
+
number_of_pages = inner_pages.count + 1
|
73
|
+
puts "Processing page 1 of #{number_of_pages}"
|
74
|
+
puts ""
|
75
|
+
|
76
|
+
# Process current build page
|
77
|
+
process_builds_page builds_page
|
78
|
+
|
79
|
+
# Cycle over remaning build pages
|
80
|
+
i = 2
|
81
|
+
inner_pages.each do |page|
|
82
|
+
puts ""
|
83
|
+
puts "Processing page #{i} of #{number_of_pages}"
|
84
|
+
|
85
|
+
process_builds_page @agent.get "https://testflightapp.com#{page}"
|
86
|
+
|
87
|
+
i = i+1
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_builds_page page
|
96
|
+
body = page.body
|
97
|
+
build_pages = body.scan /<tr class="goversion pointer" id="\/dashboard\/builds\/report\/(.*?)\/">/
|
98
|
+
|
99
|
+
build_pages.each do |build_id|
|
100
|
+
@agent.get "https://testflightapp.com/dashboard/builds/complete/#{build_id.first}/" do |build_page|
|
101
|
+
# Retrieve current app name
|
102
|
+
@app_name = page.at("h2").text
|
103
|
+
|
104
|
+
process_build_page build_page
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def process_build_page page
|
110
|
+
build_link = page.links_with(:dom_class => 'bitly').first
|
111
|
+
@agent.get("https://www.testflightapp.com#{build_link.href}") { |install_page| process_install_page install_page}
|
112
|
+
end
|
113
|
+
|
114
|
+
def process_install_page page
|
115
|
+
# we need to figure out what kind of build is that
|
116
|
+
release_note = page.search('.clearfix').at("p").text
|
117
|
+
|
118
|
+
puts "RELEASE NOTE"
|
119
|
+
puts release_note
|
120
|
+
|
121
|
+
ipa_link = page.link_with(:text => "download the IPA.")
|
122
|
+
ipa_link = ipa_link = page.link_with(:text => "download the IPA") if ipa_link.nil?
|
123
|
+
|
124
|
+
if ipa_link.nil?
|
125
|
+
puts "No IPA link found. Do you have permission for current application?"
|
126
|
+
else
|
127
|
+
download_build(ipa_link, "ipa", release_note)
|
128
|
+
end
|
129
|
+
|
130
|
+
puts ""
|
131
|
+
end
|
132
|
+
|
133
|
+
def download_build link, file_ext, release_note
|
134
|
+
|
135
|
+
link.href =~ /\/dashboard\/ipa\/(.*?)\//
|
136
|
+
filename = "#{$1}.#{file_ext}"
|
137
|
+
|
138
|
+
file_url = "https://www.testflightapp.com#{link.href}"
|
139
|
+
puts "Downloading #{file_url}..."
|
140
|
+
|
141
|
+
dirname = File.dirname("out/#{@current_team}")
|
142
|
+
|
143
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
144
|
+
|
145
|
+
dirname = File.dirname("out/#{@current_team}/#{@app_name}")
|
146
|
+
|
147
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
148
|
+
|
149
|
+
@agent.get(file_url).save("out/#{@current_team}/#{@app_name}/#{filename}")
|
150
|
+
File.open("out/#{@current_team}/#{@app_name}/#{$1}.txt", 'w') {|f| f.write(release_note) }
|
151
|
+
end
|
152
|
+
|
153
|
+
FileUtils.rm_rf "out"
|
154
|
+
Dir.mkdir "out"
|
155
|
+
|
156
|
+
login_page_url = "https://testflightapp.com/login/"
|
157
|
+
@agent.get(login_page_url) { |page| process_login_page page }
|
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: TestFlightExporter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fabio Milano
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mechanize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: highline
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: colored
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: commander
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- fabio@touchwonders.com
|
100
|
+
executables:
|
101
|
+
- testflight_exporter
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- bin/testflight_exporter
|
111
|
+
- lib/helpers.rb
|
112
|
+
- lib/testflight_exporter.rb
|
113
|
+
- lib/testflight_exporter/version.rb
|
114
|
+
- testflight_exporter.gemspec
|
115
|
+
- tfexplorer.rb
|
116
|
+
homepage: ''
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata: {}
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
requirements: []
|
135
|
+
rubyforge_project:
|
136
|
+
rubygems_version: 2.4.4
|
137
|
+
signing_key:
|
138
|
+
specification_version: 4
|
139
|
+
summary: A simple tool that helps you migrating your TestFlight build to your local
|
140
|
+
environment
|
141
|
+
test_files: []
|