shenzhen_fir 0.14.5
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/Gemfile +3 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +19 -0
- data/README.md +198 -0
- data/Rakefile +9 -0
- data/bin/ipa +25 -0
- data/compare_varnish.jpg +0 -0
- data/lib/shenzhen/agvtool.rb +17 -0
- data/lib/shenzhen/commands/build.rb +208 -0
- data/lib/shenzhen/commands/distribute.rb +30 -0
- data/lib/shenzhen/commands/info.rb +94 -0
- data/lib/shenzhen/commands.rb +16 -0
- data/lib/shenzhen/plistbuddy.rb +9 -0
- data/lib/shenzhen/plugins/crashlytics.rb +82 -0
- data/lib/shenzhen/plugins/deploygate.rb +97 -0
- data/lib/shenzhen/plugins/fir.rb +151 -0
- data/lib/shenzhen/plugins/ftp.rb +181 -0
- data/lib/shenzhen/plugins/hockeyapp.rb +119 -0
- data/lib/shenzhen/plugins/itunesconnect.rb +142 -0
- data/lib/shenzhen/plugins/pgyer.rb +135 -0
- data/lib/shenzhen/plugins/rivierabuild.rb +81 -0
- data/lib/shenzhen/plugins/s3.rb +139 -0
- data/lib/shenzhen/plugins/testfairy.rb +99 -0
- data/lib/shenzhen/version.rb +3 -0
- data/lib/shenzhen/xcodebuild.rb +99 -0
- data/lib/shenzhen.rb +4 -0
- data/npm-debug.log +84 -0
- data/shenzhen.gemspec +37 -0
- metadata +268 -0
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module HockeyApp
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'upload.hockeyapp.net'
|
10
|
+
|
11
|
+
def initialize(api_token)
|
12
|
+
@api_token = api_token
|
13
|
+
@connection = Faraday.new(:url => "https://#{HOSTNAME}") do |builder|
|
14
|
+
builder.request :multipart
|
15
|
+
builder.request :url_encoded
|
16
|
+
builder.response :json, :content_type => /\bjson$/
|
17
|
+
builder.use FaradayMiddleware::FollowRedirects
|
18
|
+
builder.adapter :net_http
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload_build(ipa, options)
|
23
|
+
options[:ipa] = Faraday::UploadIO.new(ipa, 'application/octet-stream') if ipa and File.exist?(ipa)
|
24
|
+
|
25
|
+
if dsym_filename = options.delete(:dsym_filename)
|
26
|
+
options[:dsym] = Faraday::UploadIO.new(dsym_filename, 'application/octet-stream')
|
27
|
+
end
|
28
|
+
|
29
|
+
@connection.post do |req|
|
30
|
+
if options[:public_identifier].nil?
|
31
|
+
req.url("/api/2/apps/upload")
|
32
|
+
else
|
33
|
+
req.url("/api/2/apps/#{options.delete(:public_identifier)}/app_versions/upload")
|
34
|
+
end
|
35
|
+
req.headers['X-HockeyAppToken'] = @api_token
|
36
|
+
req.body = options
|
37
|
+
end.on_complete do |env|
|
38
|
+
yield env[:status], env[:body] if block_given?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
command :'distribute:hockeyapp' do |c|
|
46
|
+
c.syntax = "ipa distribute:hockeyapp [options]"
|
47
|
+
c.summary = "Distribute an .ipa file over HockeyApp"
|
48
|
+
c.description = ""
|
49
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
50
|
+
c.option '-d', '--dsym FILE', "zipped .dsym package for the build"
|
51
|
+
c.option '-a', '--token TOKEN', "API Token. Available at https://rink.hockeyapp.net/manage/auth_tokens"
|
52
|
+
c.option '-i', '--identifier PUBLIC_IDENTIFIER', "Public identifier of the app you are targeting, if not specified HockeyApp will use the bundle identifier to choose the right"
|
53
|
+
c.option '-m', '--notes NOTES', "Release notes for the build (Default: Textile)"
|
54
|
+
c.option '-r', '--release RELEASE', [:beta, :store, :alpha, :enterprise], "Release type: 0 - Beta, 1 - Store, 2 - Alpha , 3 - Enterprise"
|
55
|
+
c.option '--markdown', 'Notes are written with Markdown'
|
56
|
+
c.option '--tags TAGS', "Comma separated list of tags which will receive access to the build"
|
57
|
+
c.option '--teams TEAMS', "Comma separated list of team ID numbers to which this build will be restricted"
|
58
|
+
c.option '--users USERS', "Comma separated list of user ID numbers to which this build will be restricted"
|
59
|
+
c.option '--notify', "Notify permitted teammates to install the build"
|
60
|
+
c.option '--downloadOff', "Upload but don't allow download of this version just yet"
|
61
|
+
c.option '--mandatory', "Make this update mandatory"
|
62
|
+
c.option '--commit-sha SHA', "The Git commit SHA for this build"
|
63
|
+
c.option '--build-server-url URL', "The URL of the build job on your build server"
|
64
|
+
c.option '--repository-url URL', "The URL of your source repository"
|
65
|
+
|
66
|
+
c.action do |args, options|
|
67
|
+
determine_file! unless @file = options.file
|
68
|
+
say_warning "Missing or unspecified .ipa file" unless @file and File.exist?(@file)
|
69
|
+
|
70
|
+
determine_dsym! unless @dsym = options.dsym
|
71
|
+
say_warning "Specified dSYM.zip file doesn't exist" if @dsym and !File.exist?(@dsym)
|
72
|
+
|
73
|
+
determine_hockeyapp_api_token! unless @api_token = options.token || ENV['HOCKEYAPP_API_TOKEN']
|
74
|
+
say_error "Missing API Token" and abort unless @api_token
|
75
|
+
|
76
|
+
determine_notes! unless @notes = options.notes
|
77
|
+
say_error "Missing release notes" and abort unless @notes
|
78
|
+
|
79
|
+
parameters = {}
|
80
|
+
parameters[:public_identifier] = options.identifier if options.identifier
|
81
|
+
parameters[:notes] = @notes
|
82
|
+
parameters[:notes_type] = options.markdown ? "1" : "0"
|
83
|
+
parameters[:notify] = "1" if options.notify && !options.downloadOff
|
84
|
+
parameters[:status] = options.downloadOff ? "1" : "2"
|
85
|
+
parameters[:tags] = options.tags if options.tags
|
86
|
+
parameters[:teams] = options.teams if options.teams
|
87
|
+
parameters[:users] = options.users if options.users
|
88
|
+
parameters[:dsym_filename] = @dsym if @dsym
|
89
|
+
parameters[:mandatory] = "1" if options.mandatory
|
90
|
+
parameters[:release_type] = case options.release
|
91
|
+
when :beta
|
92
|
+
"0"
|
93
|
+
when :store
|
94
|
+
"1"
|
95
|
+
when :alpha
|
96
|
+
"2"
|
97
|
+
when :enterprise
|
98
|
+
"3"
|
99
|
+
end
|
100
|
+
parameters[:commit_sha] = options.commit_sha if options.commit_sha
|
101
|
+
parameters[:build_server_url] = options.build_server_url if options.build_server_url
|
102
|
+
parameters[:repository_url] = options.repository_url if options.repository_url
|
103
|
+
|
104
|
+
client = Shenzhen::Plugins::HockeyApp::Client.new(@api_token)
|
105
|
+
response = client.upload_build(@file, parameters)
|
106
|
+
case response.status
|
107
|
+
when 200...300
|
108
|
+
say_ok "Build successfully uploaded to HockeyApp"
|
109
|
+
else
|
110
|
+
say_error "Error uploading to HockeyApp: #{response.body}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def determine_hockeyapp_api_token!
|
117
|
+
@api_token ||= ask "API Token:"
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'security'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module ITunesConnect
|
8
|
+
ITUNES_CONNECT_SERVER = 'Xcode:itunesconnect.apple.com'
|
9
|
+
|
10
|
+
class Client
|
11
|
+
attr_reader :ipa, :sdk, :params
|
12
|
+
|
13
|
+
def initialize(ipa, apple_id, sdk, account, password, params = [])
|
14
|
+
@ipa = ipa
|
15
|
+
@apple_id = apple_id
|
16
|
+
@sdk = sdk
|
17
|
+
@account = account
|
18
|
+
@password = password
|
19
|
+
@params = params
|
20
|
+
@filename = File.basename(@ipa).tr(" ", "_")
|
21
|
+
end
|
22
|
+
|
23
|
+
def upload_build!
|
24
|
+
size = File.size(@ipa)
|
25
|
+
checksum = Digest::MD5.file(@ipa)
|
26
|
+
|
27
|
+
begin
|
28
|
+
FileUtils.mkdir_p("Package.itmsp")
|
29
|
+
FileUtils.copy_entry(@ipa, "Package.itmsp/#{@filename}")
|
30
|
+
|
31
|
+
File.write("Package.itmsp/metadata.xml", metadata(@apple_id, checksum, size))
|
32
|
+
|
33
|
+
raise if /(error)|(fail)/i === transport
|
34
|
+
rescue
|
35
|
+
say_error "An error occurred when trying to upload the build to iTunesConnect.\nRun with --verbose for more info." and abort
|
36
|
+
ensure
|
37
|
+
FileUtils.rm_rf("Package.itmsp", :secure => true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def transport
|
44
|
+
xcode = `xcode-select --print-path`.strip
|
45
|
+
tool = File.join(File.dirname(xcode), "Applications/Application Loader.app/Contents/MacOS/itms/bin/iTMSTransporter").gsub(/\s/, '\ ')
|
46
|
+
tool = File.join(File.dirname(xcode), "Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter").gsub(/\s/, '\ ') if !File.exist?(tool)
|
47
|
+
|
48
|
+
escaped_password = Shellwords.escape(@password)
|
49
|
+
args = [tool, "-m upload", "-f Package.itmsp", "-u #{Shellwords.escape(@account)}", "-p #{escaped_password}"]
|
50
|
+
command = args.join(' ')
|
51
|
+
|
52
|
+
puts command.sub("-p #{escaped_password}", "-p ******") if $verbose
|
53
|
+
|
54
|
+
output = `#{command} 2> /dev/null`
|
55
|
+
puts output.chomp if $verbose
|
56
|
+
|
57
|
+
raise "Failed to upload package to iTunes Connect" unless $?.exitstatus == 0
|
58
|
+
|
59
|
+
output
|
60
|
+
end
|
61
|
+
|
62
|
+
def metadata(apple_id, checksum, size)
|
63
|
+
%{<?xml version="1.0" encoding="UTF-8"?>
|
64
|
+
<package version="software4.7" xmlns="http://apple.com/itunes/importer">
|
65
|
+
<software_assets apple_id="#{apple_id}">
|
66
|
+
<asset type="bundle">
|
67
|
+
<data_file>
|
68
|
+
<file_name>#{@filename}</file_name>
|
69
|
+
<checksum type="md5">#{checksum}</checksum>
|
70
|
+
<size>#{size}</size>
|
71
|
+
</data_file>
|
72
|
+
</asset>
|
73
|
+
</software_assets>
|
74
|
+
</package>
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
command :'distribute:itunesconnect' do |c|
|
82
|
+
c.syntax = "ipa distribute:itunesconnect [options]"
|
83
|
+
c.summary = "Upload an .ipa file to iTunes Connect"
|
84
|
+
c.description = "Upload an .ipa file directly to iTunes Connect for review. Requires that the app is in the 'Waiting for upload' state and the --upload flag to be set."
|
85
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
86
|
+
c.option '-a', '--account ACCOUNT', "Apple ID used to log into https://itunesconnect.apple.com"
|
87
|
+
c.option '-p', '--password PASSWORD', "Password for the account unless already stored in the keychain"
|
88
|
+
c.option '-u', '--upload', "Actually attempt to upload the build to iTunes Connect"
|
89
|
+
c.option '-w', '--warnings', "Check for warnings when validating the ipa"
|
90
|
+
c.option '-e', '--errors', "Check for errors when validating the ipa"
|
91
|
+
c.option '-i', '--apple-id STRING', "Apple ID from iTunes Connect"
|
92
|
+
c.option '--sdk SDK', "SDK to use when validating the ipa. Defaults to 'iphoneos'"
|
93
|
+
c.option '--save-keychain', "Save the provided account in the keychain for future use"
|
94
|
+
|
95
|
+
c.action do |args, options|
|
96
|
+
options.default :upload => false, :sdk => 'iphoneos', :save_keychain => true
|
97
|
+
|
98
|
+
determine_file! unless @file = options.file
|
99
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
100
|
+
|
101
|
+
determine_itunes_connect_account! unless @account = options.account || ENV['ITUNES_CONNECT_ACCOUNT']
|
102
|
+
say_error "Missing iTunes Connect account" and abort unless @account
|
103
|
+
|
104
|
+
apple_id = options.apple_id
|
105
|
+
say_error "Missing Apple ID" and abort unless apple_id
|
106
|
+
|
107
|
+
@password = options.password || ENV['ITUNES_CONNECT_PASSWORD']
|
108
|
+
if @password.nil? && @password = Security::GenericPassword.find(:s => Shenzhen::Plugins::ITunesConnect::ITUNES_CONNECT_SERVER, :a => @account)
|
109
|
+
@password = @password.password
|
110
|
+
say_ok "Found password in keychain for account: #{@account}" if options.verbose
|
111
|
+
else
|
112
|
+
determine_itunes_connect_password! unless @password
|
113
|
+
say_error "Missing iTunes Connect password" and abort unless @password
|
114
|
+
|
115
|
+
Security::GenericPassword.add(Shenzhen::Plugins::ITunesConnect::ITUNES_CONNECT_SERVER, @account, @password, {:U => nil}) if options.save_keychain
|
116
|
+
end
|
117
|
+
|
118
|
+
unless /^[0-9a-zA-Z]*$/ === @password
|
119
|
+
say_warning "Password contains special characters, which may not be handled properly by iTMSTransporter. If you experience problems uploading to iTunes Connect, please consider changing your password to something with only alphanumeric characters."
|
120
|
+
end
|
121
|
+
|
122
|
+
parameters = []
|
123
|
+
parameters << :warnings if options.warnings
|
124
|
+
parameters << :errors if options.errors
|
125
|
+
|
126
|
+
client = Shenzhen::Plugins::ITunesConnect::Client.new(@file, apple_id, options.sdk, @account, @password, parameters)
|
127
|
+
|
128
|
+
client.upload_build!
|
129
|
+
say_ok "Upload complete."
|
130
|
+
say_warning "You may want to double check iTunes Connect to make sure it was received correctly."
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def determine_itunes_connect_account!
|
136
|
+
@account ||= ask "iTunes Connect account:"
|
137
|
+
end
|
138
|
+
|
139
|
+
def determine_itunes_connect_password!
|
140
|
+
@password ||= password "iTunes Connect password:"
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module Pgyer
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'www.pgyer.com'
|
10
|
+
|
11
|
+
def initialize(user_key, api_key)
|
12
|
+
@user_key, @api_key = user_key, api_key
|
13
|
+
@connection = Faraday.new(:url => "http://#{HOSTNAME}", :request => { :timeout => 120 }) do |builder|
|
14
|
+
builder.request :multipart
|
15
|
+
builder.request :json
|
16
|
+
builder.response :json, :content_type => /\bjson$/
|
17
|
+
builder.use FaradayMiddleware::FollowRedirects
|
18
|
+
builder.adapter :net_http
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload_build(ipa, options)
|
23
|
+
options.update({
|
24
|
+
:uKey => @user_key,
|
25
|
+
:_api_key => @api_key,
|
26
|
+
:file => Faraday::UploadIO.new(ipa, 'application/octet-stream')
|
27
|
+
})
|
28
|
+
|
29
|
+
@connection.post("/apiv1/app/upload", options).on_complete do |env|
|
30
|
+
yield env[:status], env[:body] if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
rescue Faraday::Error::TimeoutError
|
34
|
+
say_error "Timed out while uploading build. Check http://www.pgyer.com/my to see if the upload was completed." and abort
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_app_info(options)
|
38
|
+
connection = Faraday.new(:url => "http://#{HOSTNAME}", :request => { :timeout => 120 }) do |builder|
|
39
|
+
builder.request :url_encoded
|
40
|
+
builder.request :json
|
41
|
+
builder.response :logger
|
42
|
+
builder.response :json, :content_type => /\bjson$/
|
43
|
+
builder.use FaradayMiddleware::FollowRedirects
|
44
|
+
builder.adapter :net_http
|
45
|
+
end
|
46
|
+
|
47
|
+
options.update({
|
48
|
+
:uKey => @user_key,
|
49
|
+
:_api_key => @api_key,
|
50
|
+
})
|
51
|
+
|
52
|
+
connection.post("/apiv1/app/update", options) do |env|
|
53
|
+
yield env[:status], env[:body] if block_given?
|
54
|
+
end
|
55
|
+
|
56
|
+
rescue Faraday::Error::TimeoutError
|
57
|
+
say_error "Timed out while uploading build. Check http://www.pgyer.com/my to see if the upload was completed." and abort
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
command :'distribute:pgyer' do |c|
|
64
|
+
c.syntax = "ipa distribute:pgyer [options]"
|
65
|
+
c.summary = "Distribute an .ipa file over Pgyer"
|
66
|
+
c.description = ""
|
67
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
68
|
+
c.option '-a', '--api_key KEY', "API KEY. Available at http://www.pgyer.com/doc/api#uploadApp"
|
69
|
+
c.option '-u', '--user_key KEY', "USER KEY. Available at http://www.pgyer.com/doc/api#uploadApp/"
|
70
|
+
c.option '--range RANGE', "Publish range. e.g. 1 (default), 2, 3"
|
71
|
+
c.option '--[no-]public', "Allow build app on public to download. it is not public default."
|
72
|
+
c.option '--password PASSWORD', "Set password to allow visit app web page."
|
73
|
+
|
74
|
+
c.action do |args, options|
|
75
|
+
determine_file! unless @file = options.file
|
76
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
77
|
+
|
78
|
+
determine_pgyer_user_key! unless @user_key = options.user_key || ENV['PGYER_USER_KEY']
|
79
|
+
say_error "Missing User Key" and abort unless @user_key
|
80
|
+
|
81
|
+
determine_pgyer_api_key! unless @api_key = options.api_key || ENV['PGYER_API_KEY']
|
82
|
+
say_error "Missing API Key" and abort unless @api_key
|
83
|
+
|
84
|
+
determine_publish_range! unless @publish_range = options.range
|
85
|
+
say_error "Missing Publish Range" and abort unless @publish_range
|
86
|
+
|
87
|
+
determine_is_public! unless @is_public = !!options.public
|
88
|
+
@is_public = @is_public ? 1 : 2
|
89
|
+
|
90
|
+
parameters = {}
|
91
|
+
parameters[:publishRange] = @publish_range
|
92
|
+
parameters[:isPublishToPublic] = @is_public
|
93
|
+
parameters[:password] = options.password.chomp if options.password
|
94
|
+
|
95
|
+
client = Shenzhen::Plugins::Pgyer::Client.new(@user_key, @api_key)
|
96
|
+
response = client.upload_build(@file, parameters)
|
97
|
+
case response.status
|
98
|
+
when 200...300
|
99
|
+
app_id = response.body['appKey']
|
100
|
+
app_short_uri = response.body['appShortcutUrl']
|
101
|
+
|
102
|
+
app_response = client.update_app_info({
|
103
|
+
:aKey => app_id,
|
104
|
+
:appUpdateDescription => @notes
|
105
|
+
})
|
106
|
+
|
107
|
+
if app_response.status == 200
|
108
|
+
say_ok "Build successfully uploaded to Pgyer, visit url: http://www.pgyer.com/#{app_short_uri}"
|
109
|
+
else
|
110
|
+
say_error "Error update build information: #{response.body}" and abort
|
111
|
+
end
|
112
|
+
else
|
113
|
+
say_error "Error uploading to Pgyer: #{response.body}" and abort
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def determine_pgyer_api_key!
|
120
|
+
@api_key ||= ask "API Key:"
|
121
|
+
end
|
122
|
+
|
123
|
+
def determine_pgyer_user_key!
|
124
|
+
@user_key ||= ask "User Key:"
|
125
|
+
end
|
126
|
+
|
127
|
+
def determine_publish_range!
|
128
|
+
@publish_range ||= "1"
|
129
|
+
end
|
130
|
+
|
131
|
+
def determine_is_public!
|
132
|
+
@is_public ||= false
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module RivieraBuild
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'apps.rivierabuild.com'
|
10
|
+
|
11
|
+
def initialize(api_token)
|
12
|
+
@api_token = api_token
|
13
|
+
@connection = Faraday.new(:url => "https://#{HOSTNAME}", :request => { :timeout => 120 }) do |builder|
|
14
|
+
builder.request :multipart
|
15
|
+
builder.request :url_encoded
|
16
|
+
builder.response :json, :content_type => /\bjson$/
|
17
|
+
builder.use FaradayMiddleware::FollowRedirects
|
18
|
+
builder.adapter :net_http
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload_build(ipa, options)
|
23
|
+
options[:file] = Faraday::UploadIO.new(ipa, 'application/octet-stream') if ipa and File.exist?(ipa)
|
24
|
+
|
25
|
+
@connection.post do |req|
|
26
|
+
req.url("/api/upload")
|
27
|
+
req.body = options
|
28
|
+
end.on_complete do |env|
|
29
|
+
yield env[:status], env[:body] if block_given?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
command :'distribute:rivierabuild' do |c|
|
37
|
+
c.syntax = "ipa distribute:rivierabuild [options]"
|
38
|
+
c.summary = "Distribute an .ipa file over RivieraBuild"
|
39
|
+
c.description = ""
|
40
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
41
|
+
c.option '-k', '--key KEY', "API KEY. Available at https://apps.rivierabuild.com/settings"
|
42
|
+
c.option '-a', '--availability AVAILABILITY', "For how long the build will be available? More info: http://api.rivierabuild.com"
|
43
|
+
c.option '-p', '--passcode PASSCODE', "Optional passcode required to install the build on a device"
|
44
|
+
c.option '-n', '--note NOTE', "Release notes for the build, Markdown"
|
45
|
+
c.option '--commit-sha SHA', "The Git commit SHA for this build"
|
46
|
+
c.option '--app-id', "Riviera Build Application ID"
|
47
|
+
|
48
|
+
c.action do |args, options|
|
49
|
+
determine_file! unless @file = options.file
|
50
|
+
say_warning "Missing or unspecified .ipa file" unless @file and File.exist?(@file)
|
51
|
+
|
52
|
+
determine_rivierabuild_api_token! unless @api_token = options.key || ENV['RIVIERA_API_KEY']
|
53
|
+
say_error "Missing API Token" and abort unless @api_token
|
54
|
+
|
55
|
+
determine_availability! unless @availability = options.availability
|
56
|
+
say_error "Missing availability" and abort unless @availability
|
57
|
+
|
58
|
+
parameters = {}
|
59
|
+
parameters[:api_key] = @api_token
|
60
|
+
parameters[:availability] = @availability
|
61
|
+
parameters[:passcode] = options.passcode if options.passcode
|
62
|
+
parameters[:app_id] = options.app_id if options.app_id
|
63
|
+
parameters[:note] = options.note if options.note
|
64
|
+
parameters[:commit_sha] = options.commit_sha if options.commit_sha
|
65
|
+
|
66
|
+
client = Shenzhen::Plugins::RivieraBuild::Client.new(@api_token)
|
67
|
+
response = client.upload_build(@file, parameters)
|
68
|
+
case response.status
|
69
|
+
when 200...300
|
70
|
+
say_ok "Build successfully uploaded to RivieraBuild: #{response.body['file_url']}"
|
71
|
+
else
|
72
|
+
say_error "Error uploading to RivieraBuild: #{response.body}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def determine_rivierabuild_api_token!
|
79
|
+
@api_token ||= ask "API Key:"
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
|
3
|
+
module Shenzhen::Plugins
|
4
|
+
module S3
|
5
|
+
class Client
|
6
|
+
def initialize(access_key_id, secret_access_key, region)
|
7
|
+
@s3 = AWS::S3.new(:access_key_id => access_key_id,
|
8
|
+
:secret_access_key => secret_access_key,
|
9
|
+
:region => region)
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload_build(ipa, options)
|
13
|
+
path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path]) if options[:path]
|
14
|
+
|
15
|
+
@s3.buckets.create(options[:bucket]) if options[:create]
|
16
|
+
|
17
|
+
bucket = @s3.buckets[options[:bucket]]
|
18
|
+
|
19
|
+
uploaded_urls = []
|
20
|
+
|
21
|
+
files = []
|
22
|
+
files << ipa
|
23
|
+
files << options[:dsym] if options[:dsym]
|
24
|
+
files.each do |file|
|
25
|
+
basename = File.basename(file)
|
26
|
+
key = path ? File.join(path, basename) : basename
|
27
|
+
File.open(file) do |descriptor|
|
28
|
+
obj = bucket.objects.create(key, descriptor, :acl => options[:acl])
|
29
|
+
uploaded_urls << obj.public_url.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
uploaded_urls
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def expand_path_with_substitutions_from_ipa_plist(ipa, path)
|
39
|
+
substitutions = path.scan(/\{CFBundle[^}]+\}/)
|
40
|
+
return path if substitutions.empty?
|
41
|
+
|
42
|
+
Dir.mktmpdir do |dir|
|
43
|
+
system "unzip -q #{ipa} -d #{dir} 2> /dev/null"
|
44
|
+
|
45
|
+
plist = Dir["#{dir}/**/*.app/Info.plist"].last
|
46
|
+
|
47
|
+
substitutions.uniq.each do |substitution|
|
48
|
+
key = substitution[1...-1]
|
49
|
+
value = Shenzhen::PlistBuddy.print(plist, key)
|
50
|
+
|
51
|
+
path.gsub!(Regexp.new(substitution), value) if value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
return path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
command :'distribute:s3' do |c|
|
62
|
+
c.syntax = "ipa distribute:s3 [options]"
|
63
|
+
c.summary = "Distribute an .ipa file over Amazon S3"
|
64
|
+
c.description = ""
|
65
|
+
|
66
|
+
c.example '', '$ ipa distribute:s3 -f ./file.ipa -a accesskeyid --bucket bucket-name'
|
67
|
+
|
68
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
69
|
+
c.option '-d', '--dsym FILE', "zipped .dsym package for the build"
|
70
|
+
c.option '-a', '--access-key-id ACCESS_KEY_ID', "AWS Access Key ID"
|
71
|
+
c.option '-s', '--secret-access-key SECRET_ACCESS_KEY', "AWS Secret Access Key"
|
72
|
+
c.option '-b', '--bucket BUCKET', "S3 bucket"
|
73
|
+
c.option '--[no-]create', "Create bucket if it doesn't already exist"
|
74
|
+
c.option '-r', '--region REGION', "Optional AWS region (for bucket creation)"
|
75
|
+
c.option '--acl ACL', "Uploaded object permissions e.g public_read (default), private, public_read_write, authenticated_read"
|
76
|
+
c.option '--source-dir SOURCE', "Optional source directory e.g. ./build"
|
77
|
+
c.option '-P', '--path PATH', "S3 'path'. Values from Info.plist will be substituded for keys wrapped in {} \n\t\t eg. \"/path/to/folder/{CFBundleVersion}/\" could be evaluated as \"/path/to/folder/1.0.0/\""
|
78
|
+
|
79
|
+
c.action do |args, options|
|
80
|
+
Dir.chdir(options.source_dir) if options.source_dir
|
81
|
+
|
82
|
+
determine_file! unless @file = options.file
|
83
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
84
|
+
|
85
|
+
determine_dsym! unless @dsym = options.dsym
|
86
|
+
say_error "Specified dSYM.zip file doesn't exist" if @dsym and !File.exist?(@dsym)
|
87
|
+
|
88
|
+
determine_access_key_id! unless @access_key_id = options.access_key_id
|
89
|
+
say_error "Missing AWS Access Key ID" and abort unless @access_key_id
|
90
|
+
|
91
|
+
determine_secret_access_key! unless @secret_access_key = options.secret_access_key
|
92
|
+
say_error "Missing AWS Secret Access Key" and abort unless @secret_access_key
|
93
|
+
|
94
|
+
determine_bucket! unless @bucket = options.bucket
|
95
|
+
say_error "Missing bucket" and abort unless @bucket
|
96
|
+
|
97
|
+
determine_region! unless @region = options.region
|
98
|
+
|
99
|
+
determine_acl! unless @acl = options.acl
|
100
|
+
say_error "Missing ACL" and abort unless @acl
|
101
|
+
|
102
|
+
@path = options.path
|
103
|
+
|
104
|
+
client = Shenzhen::Plugins::S3::Client.new(@access_key_id, @secret_access_key, @region)
|
105
|
+
|
106
|
+
begin
|
107
|
+
urls = client.upload_build @file, {:bucket => @bucket, :create => !!options.create, :acl => @acl, :dsym => @dsym, :path => @path}
|
108
|
+
urls.each { |url| say_ok url}
|
109
|
+
say_ok "Build successfully uploaded to S3"
|
110
|
+
rescue => exception
|
111
|
+
say_error "Error while uploading to S3: #{exception}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def determine_access_key_id!
|
118
|
+
@access_key_id ||= ENV['AWS_ACCESS_KEY_ID']
|
119
|
+
@access_key_id ||= ask "Access Key ID:"
|
120
|
+
end
|
121
|
+
|
122
|
+
def determine_secret_access_key!
|
123
|
+
@secret_access_key ||= ENV['AWS_SECRET_ACCESS_KEY']
|
124
|
+
@secret_access_key ||= ask "Secret Access Key:"
|
125
|
+
end
|
126
|
+
|
127
|
+
def determine_bucket!
|
128
|
+
@bucket ||= ENV['S3_BUCKET']
|
129
|
+
@bucket ||= ask "S3 Bucket:"
|
130
|
+
end
|
131
|
+
|
132
|
+
def determine_region!
|
133
|
+
@region ||= ENV['AWS_REGION']
|
134
|
+
end
|
135
|
+
|
136
|
+
def determine_acl!
|
137
|
+
@acl ||= "public_read"
|
138
|
+
end
|
139
|
+
end
|