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.
@@ -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