testflight 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -2,46 +2,56 @@
2
2
 
3
3
  If you are an iOS developer, like me, you probably have dealt with the tedious
4
4
  tasks of manually building, packaging, tagging, commiting and deploying your
5
- iOS applications to testflightapp.com. Your probably wondered if there is an easier way to do so.
5
+ iOS applications to testflightapp.com. You are probably wondering if there is a better way to do so.
6
6
 
7
7
  Well, you are absolutely right! This gem is going to make your life much easier. Here is what it does:
8
8
 
9
- 1. Reads your XCode project defintion to get your app version and build number
9
+ 1. Reads your XCode project info file to get your app version and build number
10
10
  2. Commits all your changes to the git repository
11
11
  3. Builds your project (or workspace)
12
12
  4. Packages your application into an IPA file
13
13
  5. Uploads the IPA file to testflightapp.com
14
14
  6. Tags your project with the version and build
15
15
  7. Increments your app build number and commits it to the repo
16
+ 8. Keeps a log of all your deployments to testflightapp.com in a FLIGHTLOG file
16
17
 
17
18
  All that with a simple command:
18
19
 
19
- $ takeoff
20
+ $ testflight takeoff
20
21
 
21
22
 
22
23
  = Configuration
23
24
 
24
- All you have to do is add .tesflight file to your project folder with the following content:
25
+ To setup your project with the deployment script, run the following command:
26
+
27
+ $ testflight checkin
28
+
29
+ This command will ask you a few questions that will guide you through a configuration process.
30
+ If you don't like being asked questions and prefer to do it manually, just create a .tesflight file
31
+ in your iOS project folder and provide the following information:
25
32
 
26
33
  build:
27
- developer_name: # This must be your company name or your name as it appears in your certificate
28
- increment_bundle: true
29
- commit_changes: true
34
+ developer_name: # This must be your company name or your name as it appears in your .cer from Apple
35
+ increment_bundle: true # If you want each deployment to increment your build number
36
+ git:
37
+ commit_changes: true # if you are using git and would like to push changes before each build
38
+ tag_build: true # will tag each build in git
30
39
  testflight:
31
40
  api_token: # Get it from https://testflightapp.com/account/#api
32
41
  team_token: # Get it from https://testflightapp.com/dashboard/team/edit/
33
- distribution_lists: ["Internal", "Everyone"] # Configure your distribution lists on https://testflightapp.com
42
+ distribution_lists: ["Internal", "Everyone"] # Configure your distribution lists at https://testflightapp.com
34
43
 
35
44
 
36
- Then go to your project folder and simple type:
45
+ = Execution
37
46
 
38
- $ takeoff
47
+ Once you have checked in (you only need to do it once per project), you are ready for takeoff. Go to your project folder and simple type:
39
48
 
49
+ $ testflight takeoff
40
50
 
41
51
  Have a safe flight!
42
52
 
43
53
 
44
- = Contributions
54
+ = Contribution
45
55
 
46
56
  Clone the repository, make any changes you like and send me a pull request.
47
57
 
data/bin/testflight ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #--
4
+ # Copyright (c) 2012 Michael Berkovich
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ # Abort beautifully with ctrl+c.
27
+ Signal.trap(:INT) { abort "\nAborting testflight task." }
28
+
29
+ ['cli.rb', 'builder.rb', 'config.rb'].each do |f|
30
+ require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/#{f}"))
31
+ end
32
+
33
+ # require 'testflight'
34
+ Testflight::Cli.start
@@ -0,0 +1,10 @@
1
+ build:
2
+ developer_name: "<%= @company_name %>"
3
+ increment_bundle: <%= @should_increment_bundle == true %>
4
+ git:
5
+ commit_changes: <%= @should_commit_changes == true %>
6
+ tag_build: <%= @should_tag_build == true %>
7
+ testflight:
8
+ api_token: <%= @api_token %>
9
+ team_token: <%= @team_token %>
10
+ distribution_lists: [<%=@teams.collect{|t| "\"#{t}\""}.join(", ")%>]
data/lib/testflight.rb ADDED
@@ -0,0 +1,28 @@
1
+ #--
2
+ # Copyright (c) 2012 Michael Berkovich
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module Testflight
25
+ autoload :Cli, 'testflight/cli'
26
+ autoload :Builder, 'testflight/builder'
27
+ autoload :Config, 'testflight/config'
28
+ end
@@ -0,0 +1,212 @@
1
+ #--
2
+ # Copyright (c) 2012 Michael Berkovich
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'appscript'
25
+ require 'yaml'
26
+
27
+ module Testflight
28
+ class Builder
29
+
30
+ XCODE_BUILDER = "/usr/bin/xcodebuild"
31
+ XCODE_PACKAGER = "/usr/bin/xcrun"
32
+ TESTFLIGHT_ENDPOINT = "http://testflightapp.com/api/builds.json"
33
+
34
+ include Appscript
35
+
36
+ def initialize(opts = {})
37
+ t0 = Time.now
38
+
39
+ if Testflight::Config.commit_changes?
40
+ commit_changes(opts)
41
+ end
42
+
43
+ if Testflight::Config.tag_build?
44
+ tag_build(opts)
45
+ end
46
+
47
+ if Testflight::Config.workspace?
48
+ build_workspace(opts)
49
+ package_workspace(opts)
50
+ else
51
+ build_project(opts)
52
+ package_project(opts)
53
+ end
54
+
55
+ upload_to_testflightapp(opts)
56
+
57
+ append_log_entry(opts)
58
+
59
+ if Testflight::Config.increment_bundle?
60
+ increment_bundle_version(opts)
61
+ if Testflight::Config.commit_changes?
62
+ commit_changes(opts.merge(:message => "Incrementing build number to #{Testflight::Config.project_version}"))
63
+ end
64
+ end
65
+
66
+ t1 = Time.now
67
+
68
+ puts("Build took #{t1-t0} seconds.")
69
+ end
70
+
71
+ ####################################################################################
72
+ ## Building Project
73
+ ####################################################################################
74
+
75
+ def build_workspace(opts = {})
76
+ cmd = "#{XCODE_BUILDER} -workspace '#{Testflight::Config.workspace_name}' "
77
+ cmd << "-scheme '#{Testflight::Config.application_name}' "
78
+ cmd << "-sdk 'iphoneos6.0' "
79
+ cmd << "-configuration 'AdHoc' "
80
+ cmd << "-arch 'armv6 armv7' "
81
+ cmd << "CONFIGURATION_BUILD_DIR='#{Testflight::Config.build_dir}' "
82
+ execute(cmd, opts)
83
+ end
84
+
85
+ def build_project(opts = {})
86
+ cmd = "#{XCODE_BUILDER} -target '#{Testflight::Config.application_name}' "
87
+ cmd << "-sdk 'iphoneos6.0' "
88
+ cmd << "-configuration 'AdHoc' "
89
+ execute(cmd, opts)
90
+ end
91
+
92
+ ####################################################################################
93
+ ## Packaging Project
94
+ ####################################################################################
95
+
96
+ def package_workspace(opts = {})
97
+ cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
98
+ cmd << "-v '#{Testflight::Config.build_dir}/#{Testflight::Config.application_name}.app' "
99
+ cmd << "-o '#{Testflight::Config.distribution_file}' "
100
+ cmd << "--sign '#{Testflight::Config.developer_name}' "
101
+ cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
102
+ execute(cmd, opts)
103
+ end
104
+
105
+ def package_project(opts = {})
106
+ cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
107
+ cmd << "-v '#{Testflight::Config.build_dir}/AdHoc-iphoneos/#{Testflight::Config.application_name}.app' "
108
+ cmd << "-o '#{Testflight::Config.distribution_file}' "
109
+ cmd << "--sign '#{Testflight::Config.developer_name}' "
110
+ cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
111
+ execute(cmd, opts)
112
+ end
113
+
114
+ ####################################################################################
115
+ ## Uploading Project
116
+ ####################################################################################
117
+
118
+ def upload_to_testflightapp(opts = {})
119
+ cmd = "curl #{TESTFLIGHT_ENDPOINT} "
120
+ cmd << "-F file=@#{Testflight::Config.distribution_file} "
121
+ cmd << "-F api_token=#{Testflight::Config.api_token} "
122
+ cmd << "-F team_token=#{Testflight::Config.team_token} "
123
+ cmd << "-F notify=#{opts[:notify]} "
124
+ cmd << "-F distribution_lists=#{opts[:teams].join(",")} "
125
+ cmd << "-F notes='#{opts[:message]}'"
126
+ execute(cmd, opts)
127
+ end
128
+
129
+ ####################################################################################
130
+ ## Project Versioning
131
+ ####################################################################################
132
+
133
+ def increment_bundle_version(opts = {})
134
+ # Check if build numbers are numeric
135
+ build_number = Testflight::Config.build_number.to_i
136
+ build_number += 1
137
+
138
+ puts("\r\nIncrementing build number to #{build_number}...")
139
+
140
+ return if opts[:cold]
141
+ Testflight::Config.project_info["CFBundleVersion"] = build_number.to_s
142
+
143
+ File.open(Testflight::Config.project_info_path, "w") do |f|
144
+ f.write(Testflight::Config.project_info.to_plist)
145
+ end
146
+ end
147
+
148
+ ####################################################################################
149
+ ## Git Support
150
+ ####################################################################################
151
+ def commit_changes(opts = {})
152
+ execute("git add .", opts.merge(:ignore_result => true))
153
+ execute("git add . --update", opts.merge(:ignore_result => true))
154
+ execute("git commit -m '#{opts[:message]}'", opts.merge(:ignore_result => true))
155
+ execute("git push", opts.merge(:ignore_result => true))
156
+ end
157
+
158
+ def tag_build(opts = {})
159
+ execute("git tag -a #{Testflight::Config.project_version_short} -m 'Release #{Testflight::Config.project_version}'", opts.merge(:ignore_result => true))
160
+ end
161
+
162
+ ####################################################################################
163
+ ## FlightLog Support
164
+ ####################################################################################
165
+ def append_log_entry(opts)
166
+ lines = []
167
+ lines << "#{Testflight::Config.application_name} #{Testflight::Config.project_version}"
168
+ lines << "Deplyed On: #{Time.now}"
169
+ lines << "Distributed To: #{opts[:teams].join(", ")}"
170
+ lines << "Notified By Email: #{opts[:notify]}"
171
+ lines << "Notes: #{opts[:message]}"
172
+ lines << "---------------------------------------------------------------------------"
173
+
174
+ log = "FLIGHTLOG"
175
+ tmp = log + "~"
176
+
177
+ File.open(tmp, "w") do |newfile|
178
+ lines.each do |line|
179
+ newfile.puts(line)
180
+ end
181
+
182
+ if File.exist?(log)
183
+ File.open(log, "r+") do |oldfile|
184
+ oldfile.each_line do |line|
185
+ newfile.puts(line)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ File.delete(log) if File.exist?(log)
192
+ File.rename(tmp, log)
193
+ end
194
+
195
+ ####################################################################################
196
+ ## Command Support
197
+ ####################################################################################
198
+ def execute(cmd, opts = {})
199
+ puts("\r\n$ " + cmd)
200
+ return if opts[:cold]
201
+
202
+ result = system(cmd)
203
+ return if opts[:ignore_result]
204
+
205
+ unless result
206
+ puts("Build failed.")
207
+ exit 1
208
+ end
209
+ end
210
+
211
+ end
212
+ end
@@ -0,0 +1,195 @@
1
+ #--
2
+ # Copyright (c) 2012 Michael Berkovich
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'thor'
25
+
26
+ module Testflight
27
+ class Cli < Thor
28
+ include Thor::Actions
29
+
30
+ class << self
31
+ def source_root
32
+ File.expand_path('../../',__FILE__)
33
+ end
34
+ end
35
+
36
+ map 'c' => :checkin
37
+ map 't' => :takeoff
38
+
39
+ desc 'checkin', 'Initializes project configuration file and prepares you for takeoff'
40
+ def checkin
41
+ return unless is_project_folder?
42
+
43
+ say("\r\nConfiguring #{Testflight::Config.application_name} #{Testflight::Config.type} for deployment to testflightapp.com...")
44
+ say("Please answer the following questions to prepare you for takeoff.")
45
+
46
+ ["Provisioning", "Distributions", "build"].each do |name|
47
+ create_folder(name)
48
+ end
49
+
50
+ @company_name = ask("\r\nWhat is the name of your company, as it appears in your .cer file from Apple?")
51
+ @should_increment_bundle = yes?("\r\nWould you like to automatically increment build numbers (bundle value) for every deployment? (y/n)")
52
+
53
+ if yes?("\r\nDo you use git as your SCM? (y/n)")
54
+ @should_commit_changes = yes?("\r\nWould you like to commit and push your changes to git as the first step of your deployment? (y/n)")
55
+ if @should_increment_bundle
56
+ @should_tag_build = yes?("\r\nWould you like to tag every build in git with the version/build number? (y/n)")
57
+ end
58
+ end
59
+
60
+ say("\r\nFor the next question, please get your API TOKEN from the following URL: https://testflightapp.com/account/#api")
61
+ @api_token = ask("Paste your API TOKEN here:")
62
+
63
+ say("\r\nFor the next question, please get your TEAM TOKEN from the following URL: https://testflightapp.com/dashboard/team/edit")
64
+ @team_token = ask("Paste your TEAM TOKEN here:")
65
+
66
+ @teams = ask("\r\nPlease enter your distribution lists as you have set them up on testflight (separate team names with a comma):")
67
+ @teams = @teams.split(",").collect{|t| t.strip}
68
+
69
+ template 'templates/testflight.yml.erb', "./#{Testflight::Config.path}"
70
+
71
+ update_git_ignore
72
+
73
+ say("\r\nConfiguration file '.testflight' has been created. You can edit it manually or run the init command again.")
74
+ say("\r\nOnly a few steps left, but make sure you do them all.")
75
+ say("\r\n1). Copy your Ad Hoc Provisioning Profile (.mobileprovision) into the 'Provisioning' folder.")
76
+ say("2). Create a new build profile called AdHoc and make sure you set it as the default profile for command line builds.")
77
+ say("\r\nOnce you are done, you can run: testflight takeoff")
78
+ end
79
+
80
+ desc 'takeoff', 'Builds and deploys your project based on your configuration.'
81
+ method_option :cold, :type => :boolean, :aliases => "c", :required => false
82
+ def takeoff
83
+ return unless ready_for_takeoff?
84
+
85
+ say("Preparing #{Testflight::Config.application_name} #{Testflight::Config.type} #{Testflight::Config.project_version} for deployment to testflightapp.com...")
86
+
87
+ @message = ask("\r\nWhat changes did you make in this build?")
88
+ @teams = ask_with_multiple_options("\r\nWhich team(s) would you like to distirbute the build to? Provide team number(s, separated by a comma)",
89
+ Testflight::Config.distribution_lists)
90
+
91
+ @notify = yes?("\r\nWould you like to notify the team members by email about this build? (y/n)")
92
+
93
+ Testflight::Builder.new(:message => @message, :teams => @teams, :notify => @notify, :cold => options['cold'])
94
+
95
+ say("\r\nCongratulations, the build has been deployed! Have a safe flight!")
96
+ say("")
97
+ end
98
+
99
+ protected
100
+
101
+ def is_project_folder?
102
+ unless Testflight::Config.application_name
103
+ say("This folder does not contain an xCode project or a workspace.")
104
+ say("Please run this command in the folder where you have the project or workspace you want to deploy.")
105
+ return false
106
+ end
107
+
108
+ true
109
+ end
110
+
111
+ def ready_for_takeoff?
112
+ return false unless is_project_folder?
113
+
114
+ unless File.exists?(Testflight::Config.path)
115
+ say("Project configuration file does not exist. Please run 'testflight checkin' first.")
116
+ return false
117
+ end
118
+
119
+ unless Testflight::Config.ad_hoc_provisioning_name
120
+ say("Please copy your Ad Hoc Provisioning Profile into the Provisioning folder.")
121
+ return false
122
+ end
123
+
124
+ unless Testflight::Config.project_info_path
125
+ say("Cannot locate #{Testflight::Config.project_info_file_name} file. Please make sure such file exists in your project.")
126
+ return false
127
+ end
128
+
129
+ true
130
+ end
131
+
132
+ def create_folder(name)
133
+ return if File.exist?("#{Dir.pwd}/#{name}")
134
+ say("Creating folder: #{name}")
135
+ FileUtils.mkdir(name)
136
+ end
137
+
138
+ # Remove the new files from git commits
139
+ def update_git_ignore
140
+ lines = File.exist?('.gitignore') ? File.open('.gitignore').readlines : []
141
+
142
+ changed = false
143
+ ['build', 'Distributions', 'Provisioning', '.testflight'].each do |name|
144
+ next if lines.include?(name)
145
+ lines << name
146
+ changed = true
147
+ end
148
+ return unless changed
149
+
150
+ File.open('.gitignore', "w") do |f|
151
+ lines.each do |line|
152
+ f.write(line + "\n")
153
+ end
154
+ end
155
+ end
156
+
157
+ def ask_for_info(question=nil, prompt="> ", allow_blank=false)
158
+ say(question) if question
159
+
160
+ $stdout.print(prompt)
161
+
162
+ $stdin.each_line do |line|
163
+ value = line.strip.downcase
164
+ return value if allow_blank
165
+ return value unless value.empty?
166
+ $stdout.print(prompt)
167
+ end
168
+ end
169
+
170
+ def collect_option_selections(opts)
171
+ lists = []
172
+ vals = ask_for_info(nil, prompt="? ")
173
+ vals.split(",").each do |index|
174
+ index = index.to_i - 1
175
+ return nil if index<0 or index>=opts.size
176
+ lists << opts[index]
177
+ end
178
+ lists
179
+ end
180
+
181
+ def ask_with_multiple_options(question, opts = [])
182
+ say(question)
183
+ opts.each_with_index{ |opt, index| say("\t#{index+1}) #{opt}") }
184
+
185
+ vals = collect_option_selections(opts)
186
+ while vals.nil?
187
+ say("Invalid selection, please try again.")
188
+ vals = collect_option_selections(opts)
189
+ end
190
+
191
+ vals
192
+ end
193
+
194
+ end
195
+ end
@@ -23,7 +23,6 @@
23
23
 
24
24
  require 'fileutils'
25
25
  require 'yaml'
26
- require 'pp'
27
26
  require 'plist'
28
27
 
29
28
  module Testflight
@@ -33,69 +32,57 @@ module Testflight
33
32
  '.testflight'
34
33
  end
35
34
 
36
- def self.defaults
37
- {
38
- "build" => {
39
- "developer_name" => "As it appears in your Apple certificate",
40
- "increment_bundle" => true,
41
- "commit_changes" => true
42
- },
43
- "testflight" => {
44
- "api_token" => "Get it from https://testflightapp.com/account/#api",
45
- "team_token" => "Get it from https://testflightapp.com/dashboard/team/edit/",
46
- "distribution_lists" => [""]
47
- }
48
- }
49
- end
35
+ ##################################################
36
+ ## Configuration Attributes
37
+ ##################################################
50
38
 
51
39
  def self.config
52
- @config ||= begin
53
- if File.exist?(path)
54
- YAML::load(File.open(path))
55
- else
56
- File.open(path, "w") do |f|
57
- f.write(defaults.to_yaml)
58
- end
59
- raise "Please update #{path} with all necessary properties."
60
- end
61
- end
40
+ @config ||= YAML::load(File.open(path))
62
41
  end
63
42
 
64
- def self.commit_changes?
65
- config["build"]["commit_changes"]
43
+ def self.build
44
+ config["build"]
45
+ end
46
+
47
+ def self.developer_name
48
+ build["developer_name"]
66
49
  end
67
50
 
68
51
  def self.increment_bundle?
69
- config["build"]["increment_bundle"]
52
+ build["increment_bundle"]
70
53
  end
71
54
 
72
- def self.developer_name
73
- config["build"]["developer_name"]
55
+ def self.git
56
+ config["git"]
57
+ end
58
+
59
+ def self.commit_changes?
60
+ git["commit_changes"]
61
+ end
62
+
63
+ def self.tag_build?
64
+ git["tag_build"]
65
+ end
66
+
67
+ def self.testflight
68
+ config["testflight"]
74
69
  end
75
70
 
76
71
  def self.distribution_lists
77
- config["testflight"]["distribution_lists"]
72
+ testflight["distribution_lists"]
78
73
  end
79
74
 
80
75
  def self.api_token
81
- config["testflight"]["api_token"]
76
+ testflight["api_token"]
82
77
  end
83
78
 
84
79
  def self.team_token
85
- config["testflight"]["team_token"]
80
+ testflight["team_token"]
86
81
  end
87
82
 
88
- def self.valid?
89
- return false unless config
90
- return false if config.empty?
91
- return false if config["build"].nil?
92
- return false if config["build"]["developer_name"].nil?
93
- return false if config["testflight"].nil?
94
- return false if config["testflight"]["api_token"].nil?
95
- return false if config["testflight"]["team_token"].nil?
96
- return false if config["testflight"]["distribution_lists"].nil?
97
- true
98
- end
83
+ ##################################################
84
+ ## Helper Methods
85
+ ##################################################
99
86
 
100
87
  def self.project_dir
101
88
  Dir.pwd
@@ -158,42 +145,16 @@ module Testflight
158
145
  end
159
146
 
160
147
  def self.distribution_file
161
- "#{distributions_dir}/#{application_name}.ipa"
148
+ "#{distributions_dir}/#{application_name}_#{project_version_short}.ipa"
162
149
  end
163
150
 
164
- def self.setup
165
- unless application_name
166
- pp "This folder does not contain an xCode project or a workspace."
167
- exit 1
168
- end
169
-
170
- unless valid?
171
- pp "Ensure that you have provided all of the information in the #{config_file} config file"
172
- exit 1
173
- end
174
-
175
- unless project_files.include?("Distributions")
176
- FileUtils.mkdir("Distributions")
177
- @project_files = nil
178
- end
179
-
180
- unless project_files.include?("Provisioning")
181
- FileUtils.mkdir("Provisioning")
182
- @project_files = nil
183
- end
184
-
185
- unless ad_hoc_provisioning_name
186
- pp "Please copy your Ad Hoc Provisioning Profile into the provisioning folder."
187
- exit 1
188
- end
151
+ def self.project_info_file_name
152
+ "#{application_name}-Info.plist"
189
153
  end
190
154
 
191
155
  def self.project_info_path
192
- files = Dir["**/#{application_name}-Info.plist"]
193
- if files.empty?
194
- pp "Cannot locate #{application_name}-Info.plist file. Please make sure such file exists in your project."
195
- exit 1
196
- end
156
+ files = Dir["**/#{project_info_file_name}"]
157
+ return nil if files.empty?
197
158
  files.first
198
159
  end
199
160
 
@@ -22,7 +22,7 @@
22
22
  #++
23
23
 
24
24
  module Testflight
25
- VERSION = "0.1.2"
25
+ VERSION = "0.1.4"
26
26
  end
27
27
 
28
28
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testflight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-30 00:00:00.000000000 Z
12
+ date: 2012-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.16.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.16.0
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: plist
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -27,19 +43,22 @@ dependencies:
27
43
  - - ~>
28
44
  - !ruby/object:Gem::Version
29
45
  version: 3.1.0
30
- description: Mechanism for building, packaging, tagging and deploying XCode projects
46
+ description: Mechanism for building, packaging, tagging and deploying xCode projects
31
47
  to testflightapp.com
32
48
  email:
33
49
  - theiceberk@gmail.com
34
50
  executables:
35
- - takeoff
51
+ - testflight
36
52
  extensions: []
37
53
  extra_rdoc_files: []
38
54
  files:
39
- - bin/takeoff
55
+ - bin/testflight
56
+ - lib/templates/testflight.yml.erb
57
+ - lib/testflight/builder.rb
58
+ - lib/testflight/cli.rb
40
59
  - lib/testflight/config.rb
41
- - lib/testflight/prompter.rb
42
60
  - lib/testflight/version.rb
61
+ - lib/testflight.rb
43
62
  - LICENSE
44
63
  - README.rdoc
45
64
  homepage: https://github.com/berk/testflight
data/bin/takeoff DELETED
@@ -1,182 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'pp'
4
-
5
- require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/config.rb"))
6
- require File.expand_path(File.join(File.dirname(__FILE__), "../lib/testflight/prompter.rb"))
7
-
8
- XCODE_BUILDER = "/usr/bin/xcodebuild"
9
- XCODE_PACKAGER = "/usr/bin/xcrun"
10
- TESTFLIGHT_ENDPOINT = "http://testflightapp.com/api/builds.json"
11
-
12
- # read project definition
13
- # read app version and build number
14
- # add all files to git and commit files
15
- # tag git with the version and build
16
- # build files
17
- # package files
18
- # deploy to testflight app
19
-
20
- ####################################################################################
21
- ## Building Project
22
- ####################################################################################
23
-
24
- def build_workspace
25
- cmd = "#{XCODE_BUILDER} -workspace '#{Testflight::Config.workspace_name}' "
26
- cmd << "-scheme '#{Testflight::Config.application_name}' "
27
- cmd << "-sdk 'iphoneos6.0' "
28
- cmd << "-configuration 'AdHoc' "
29
- cmd << "-arch 'armv6 armv7' "
30
- cmd << "CONFIGURATION_BUILD_DIR='#{Testflight::Config.build_dir}' "
31
- pp "Building Workspace..."
32
- pp cmd
33
- unless system(cmd)
34
- pp "Failed to build workspace"
35
- exit 1
36
- end
37
- end
38
-
39
- def build_project
40
- cmd = "#{XCODE_BUILDER} -target '#{Testflight::Config.application_name}' "
41
- cmd << "-sdk 'iphoneos6.0' "
42
- cmd << "-configuration 'AdHoc' "
43
-
44
- pp "Building Project..."
45
- pp cmd
46
- unless system(cmd)
47
- pp "Failed to build project"
48
- exit 1
49
- end
50
- end
51
-
52
- ####################################################################################
53
- ## Packaging Project
54
- ####################################################################################
55
-
56
- def package_workspace
57
- cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
58
- cmd << "-v '#{Testflight::Config.build_dir}/#{Testflight::Config.application_name}.app' "
59
- cmd << "-o '#{Testflight::Config.distribution_file}' "
60
- cmd << "--sign '#{Testflight::Config.developer_name}' "
61
- cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
62
-
63
- pp "Packaging Workspace..."
64
- pp cmd
65
- unless system(cmd)
66
- pp "Failed to package workspace"
67
- exit 1
68
- end
69
- end
70
-
71
- def package_project
72
- cmd = "#{XCODE_PACKAGER} -sdk iphoneos PackageApplication "
73
- cmd << "-v '#{Testflight::Config.build_dir}/AdHoc-iphoneos/#{Testflight::Config.application_name}.app' "
74
- cmd << "-o '#{Testflight::Config.distribution_file}' "
75
- cmd << "--sign '#{Testflight::Config.developer_name}' "
76
- cmd << "--embed '#{Testflight::Config.provisioning_dir}/#{Testflight::Config.ad_hoc_provisioning_name}'"
77
-
78
- pp "Packaging Project..."
79
- pp cmd
80
- unless system(cmd)
81
- pp "Failed to package project"
82
- exit 1
83
- end
84
- end
85
-
86
- ####################################################################################
87
- ## Uploading Project
88
- ####################################################################################
89
-
90
- def upload_to_testflightapp
91
- pp "Uploading to TestFlightApp..."
92
-
93
- notes = Testflight::Prompter.read_info("What has changed in this build?")
94
-
95
- lists = Testflight::Prompter.select_set("Which distribution lists would you like to send this build to? Select one or more lists, separated with comma.", Testflight::Config.distribution_lists)
96
-
97
- notify = Testflight::Prompter.read_options("Would you like to notify your team members by email about this build?")
98
- ["y", "yes"].include?(notify) ? notify = "true" : notify = "false"
99
-
100
- cmd = "curl #{TESTFLIGHT_ENDPOINT} "
101
- cmd << "-F file=@#{Testflight::Config.distribution_file} "
102
- cmd << "-F api_token=#{Testflight::Config.api_token} "
103
- cmd << "-F team_token=#{Testflight::Config.team_token} "
104
- cmd << "-F notify=#{notify} "
105
- cmd << "-F distribution_lists=#{lists.join(",")} "
106
- cmd << "-F notes='#{notes}'"
107
-
108
- pp cmd
109
- unless system(cmd)
110
- pp "Failed to upload to testflight"
111
- exit 1
112
- end
113
- end
114
-
115
- ####################################################################################
116
- ## Project Versioning
117
- ####################################################################################
118
-
119
- def increment_bundle_version
120
- Testflight::Config.project_info["CFBundleVersion"] = (Testflight::Config.build_number.to_i + 1).to_s
121
- pp "Incrementing bundle version to #{Testflight::Config.build_number}..."
122
-
123
- File.open(Testflight::Config.project_info_path, "w") do |f|
124
- f.write(Testflight::Config.project_info.to_plist)
125
- end
126
- end
127
-
128
- ####################################################################################
129
- ## Git Support
130
- ####################################################################################
131
- def update_git_ignore
132
- # build
133
- # Distributions
134
- # Provisioning
135
- # .testflight
136
- end
137
-
138
- def commit_changes(msg)
139
- system("git add .")
140
- system("git add . --update")
141
- system("git commit -m '#{msg}'")
142
- system("git push")
143
-
144
- system("git tag -a #{Testflight::Config.project_version_short} -m 'Release #{Testflight::Config.project_version}'")
145
- end
146
-
147
- ####################################################################################
148
- ## Main
149
- ####################################################################################
150
-
151
- def deploy
152
- Testflight::Config.setup
153
-
154
- if Testflight::Config.commit_changes?
155
- commit_changes("Preparing build #{Testflight::Config.project_version}")
156
- end
157
-
158
- if Testflight::Config.workspace?
159
- build_workspace
160
- package_workspace
161
- else
162
- build_project
163
- package_project
164
- end
165
-
166
- upload_to_testflightapp
167
-
168
- pp ""
169
-
170
- if Testflight::Config.increment_bundle?
171
- increment_bundle_version
172
- if Testflight::Config.commit_changes?
173
- commit_changes("Incrementing build number to #{Testflight::Config.project_version}")
174
- end
175
- end
176
-
177
- pp "Congratulations! The app has been deployed!"
178
- end
179
-
180
- deploy
181
-
182
-
@@ -1,81 +0,0 @@
1
- #--
2
- # Copyright (c) 2012 Michael Berkovich
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #++
23
-
24
- module Testflight
25
- class Prompter
26
-
27
- def self.read_options(question=nil, opts=["Yes", "No"], vals=["y", "yes", "n", "no"], joiner = "/")
28
- prompt = "(#{opts.join(joiner)})? "
29
- pp question if question
30
-
31
- $stdout.print(prompt)
32
-
33
- $stdin.each_line do |line|
34
- value = line.strip.downcase
35
- return value if vals.include?(value)
36
- $stdout.print(prompt)
37
- end
38
- end
39
-
40
- def self.read_info(question=nil, prompt="> ", allow_blank=false)
41
- pp question if question
42
-
43
- $stdout.print(prompt)
44
-
45
- $stdin.each_line do |line|
46
- value = line.strip.downcase
47
- return value if allow_blank
48
- return value unless value.empty?
49
- $stdout.print(prompt)
50
- end
51
- end
52
-
53
- def self.read_set_values(opts)
54
- lists = []
55
- vals = read_info(nil, prompt="? ")
56
- vals.split(",").each do |index|
57
- index = index.to_i - 1
58
- return nil if index<0 or index>=opts.size
59
- lists << opts[index]
60
- end
61
- lists
62
- end
63
-
64
- def self.select_set(question, opts = [])
65
- pp question
66
-
67
- opts.each_with_index do |opt, index|
68
- pp " #{index+1}) #{opt}"
69
- end
70
-
71
- vals = read_set_values(opts)
72
- while vals.nil?
73
- pp "Invalid selection, please try again."
74
- vals = read_set_values(opts)
75
- end
76
-
77
- vals
78
- end
79
-
80
- end
81
- end