krausefx-shenzhen 0.14.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/Gemfile +3 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +19 -0
- data/README.md +198 -0
- data/Rakefile +9 -0
- data/lib/shenzhen.rb +4 -0
- data/lib/shenzhen/agvtool.rb +17 -0
- data/lib/shenzhen/commands.rb +16 -0
- data/lib/shenzhen/commands/build.rb +213 -0
- data/lib/shenzhen/commands/distribute.rb +30 -0
- data/lib/shenzhen/commands/info.rb +94 -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 +145 -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/shenzhen.gemspec +37 -0
- metadata +264 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'net/ftp'
|
2
|
+
require 'net/sftp'
|
3
|
+
|
4
|
+
module Shenzhen::Plugins
|
5
|
+
module FTP
|
6
|
+
class Client
|
7
|
+
|
8
|
+
def initialize(host, port, user, password)
|
9
|
+
@host, @port, @user, @password = host, port, user, password
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload(ipa, options = {})
|
13
|
+
connection = Net::FTP.new
|
14
|
+
connection.passive = true
|
15
|
+
connection.connect(@host, @port)
|
16
|
+
|
17
|
+
path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
|
18
|
+
|
19
|
+
begin
|
20
|
+
connection.login(@user, @password) rescue raise "Login authentication failed"
|
21
|
+
|
22
|
+
if options[:mkdir]
|
23
|
+
components, pwd = path.split(/\//).reject(&:empty?), nil
|
24
|
+
components.each do |component|
|
25
|
+
pwd = File.join(*[pwd, component].compact)
|
26
|
+
|
27
|
+
begin
|
28
|
+
connection.mkdir pwd
|
29
|
+
rescue => exception
|
30
|
+
raise exception unless /File exists/ === exception.message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
connection.chdir path unless path.empty?
|
36
|
+
connection.putbinaryfile ipa, File.basename(ipa)
|
37
|
+
connection.putbinaryfile(options[:dsym], File.basename(options[:dsym])) if options[:dsym]
|
38
|
+
ensure
|
39
|
+
connection.close
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def expand_path_with_substitutions_from_ipa_plist(ipa, path)
|
46
|
+
substitutions = path.scan(/\{CFBundle[^}]+\}/)
|
47
|
+
return path if substitutions.empty?
|
48
|
+
|
49
|
+
Dir.mktmpdir do |dir|
|
50
|
+
system "unzip -q #{ipa} -d #{dir} 2> /dev/null"
|
51
|
+
|
52
|
+
plist = Dir["#{dir}/**/*.app/Info.plist"].last
|
53
|
+
|
54
|
+
substitutions.uniq.each do |substitution|
|
55
|
+
key = substitution[1...-1]
|
56
|
+
value = Shenzhen::PlistBuddy.print(plist, key)
|
57
|
+
|
58
|
+
path.gsub!(Regexp.new(substitution), value) if value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
return path
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module SFTP
|
68
|
+
class Client < Shenzhen::Plugins::FTP::Client
|
69
|
+
def upload(ipa, options = {})
|
70
|
+
session = Net::SSH.start(@host, @user, :password => @password, :port => @port)
|
71
|
+
connection = Net::SFTP::Session.new(session).connect!
|
72
|
+
|
73
|
+
path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
|
74
|
+
|
75
|
+
begin
|
76
|
+
connection.stat!(path) do |response|
|
77
|
+
connection.mkdir! path if options[:mkdir] and not response.ok?
|
78
|
+
|
79
|
+
connection.upload! ipa, determine_file_path(File.basename(ipa), path)
|
80
|
+
connection.upload! options[:dsym], determine_file_path(File.basename(options[:dsym]), path) if options[:dsym]
|
81
|
+
end
|
82
|
+
ensure
|
83
|
+
connection.close_channel
|
84
|
+
session.shutdown!
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def determine_file_path(file_name, path)
|
89
|
+
if path.empty?
|
90
|
+
file_name
|
91
|
+
else
|
92
|
+
"#{path}/#{file_name}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
command :'distribute:ftp' do |c|
|
100
|
+
c.syntax = "ipa distribute:ftp [options]"
|
101
|
+
c.summary = "Distribute an .ipa file over FTP"
|
102
|
+
c.description = ""
|
103
|
+
|
104
|
+
c.example '', '$ ipa distribute:ftp --host 127.0.0.1 -f ./file.ipa -u username --path "/path/to/folder/{CFBundleVersion}/" --mkdir'
|
105
|
+
|
106
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
107
|
+
c.option '-d', '--dsym FILE', "zipped .dsym package for the build"
|
108
|
+
c.option '-h', '--host HOST', "FTP host"
|
109
|
+
c.option '-u', '--user USER', "FTP user"
|
110
|
+
c.option '-p', '--password PASS', "FTP password"
|
111
|
+
c.option '-P', '--path PATH', "FTP path. Values from Info.plist will be substituted for keys wrapped in {} \n\t\t e.g. \"/path/to/folder/{CFBundleVersion}/\" would be evaluated as \"/path/to/folder/1.0.0/\""
|
112
|
+
c.option '--port PORT', "FTP port"
|
113
|
+
c.option '--protocol [PROTOCOL]', [:ftp, :sftp], "Protocol to use (ftp, sftp)"
|
114
|
+
c.option '--[no-]mkdir', "Create directories on FTP if they don't already exist"
|
115
|
+
|
116
|
+
c.action do |args, options|
|
117
|
+
options.default :mkdir => true
|
118
|
+
|
119
|
+
determine_file! unless @file = options.file
|
120
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
121
|
+
|
122
|
+
determine_dsym! unless @dsym = options.dsym
|
123
|
+
say_warning "Specified dSYM.zip file doesn't exist" unless @dsym and File.exist?(@dsym)
|
124
|
+
|
125
|
+
determine_host! unless @host = options.host
|
126
|
+
say_error "Missing FTP host" and abort unless @host
|
127
|
+
|
128
|
+
determine_port!(options.protocol) unless @port = options.port
|
129
|
+
|
130
|
+
determine_user! unless @user = options.user
|
131
|
+
say_error "Missing FTP user" and abort unless @user
|
132
|
+
|
133
|
+
@password = options.password
|
134
|
+
if !@password && options.protocol != :sftp
|
135
|
+
determine_password!
|
136
|
+
say_error "Missing FTP password" and abort unless @password
|
137
|
+
end
|
138
|
+
|
139
|
+
@path = options.path || ""
|
140
|
+
|
141
|
+
client = case options.protocol
|
142
|
+
when :sftp
|
143
|
+
Shenzhen::Plugins::SFTP::Client.new(@host, @port, @user, @password)
|
144
|
+
else
|
145
|
+
Shenzhen::Plugins::FTP::Client.new(@host, @port, @user, @password)
|
146
|
+
end
|
147
|
+
|
148
|
+
begin
|
149
|
+
client.upload @file, {:path => @path, :dsym => @dsym, :mkdir => !!options.mkdir}
|
150
|
+
say_ok "Build successfully uploaded to FTP"
|
151
|
+
rescue => exception
|
152
|
+
say_error "Error while uploading to FTP: #{exception}"
|
153
|
+
raise if options.trace
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def determine_host!
|
160
|
+
@host ||= ask "FTP Host:"
|
161
|
+
end
|
162
|
+
|
163
|
+
def determine_port!(protocol)
|
164
|
+
@port = case protocol
|
165
|
+
when :sftp
|
166
|
+
Net::SSH::Transport::Session::DEFAULT_PORT
|
167
|
+
else
|
168
|
+
Net::FTP::FTP_PORT
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def determine_user!
|
173
|
+
@user ||= ask "Username:"
|
174
|
+
end
|
175
|
+
|
176
|
+
def determine_password!
|
177
|
+
@password ||= password "Password:"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
alias_command :'distribute:sftp', :'distribute:ftp', '--protocol', 'sftp'
|
@@ -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
|