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,30 @@
|
|
1
|
+
private
|
2
|
+
|
3
|
+
def determine_file!
|
4
|
+
files = Dir['*.ipa']
|
5
|
+
@file ||= case files.length
|
6
|
+
when 0 then nil
|
7
|
+
when 1 then files.first
|
8
|
+
else
|
9
|
+
@file = choose "Select an .ipa File:", *files
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def determine_dsym!
|
14
|
+
dsym_files = Dir['*.dSYM.zip']
|
15
|
+
@dsym ||= case dsym_files.length
|
16
|
+
when 0 then nil
|
17
|
+
when 1 then dsym_files.first
|
18
|
+
else
|
19
|
+
dsym_files.detect do |dsym|
|
20
|
+
File.basename(dsym, ".app.dSYM.zip") == File.basename(@file, ".ipa")
|
21
|
+
end or choose "Select a .dSYM.zip file:", *dsym_files
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def determine_notes!
|
26
|
+
placeholder = %{What's new in this release: }
|
27
|
+
|
28
|
+
@notes = ask_editor placeholder
|
29
|
+
@notes = nil if @notes == placeholder
|
30
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'plist'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'zip'
|
4
|
+
require 'zip/filesystem'
|
5
|
+
|
6
|
+
command :info do |c|
|
7
|
+
c.syntax = 'ipa info [options]'
|
8
|
+
c.summary = 'Show mobile provisioning information about an .ipa file'
|
9
|
+
c.description = ''
|
10
|
+
|
11
|
+
c.action do |args, options|
|
12
|
+
say_error "`security` command not found in $PATH" and abort if `which security` == ""
|
13
|
+
say_error "`codesign` command not found in $PATH" and abort if `which codesign` == ""
|
14
|
+
|
15
|
+
determine_file! unless @file = args.pop
|
16
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and ::File.exist?(@file)
|
17
|
+
|
18
|
+
Zip::File.open(@file) do |zipfile|
|
19
|
+
app_entry = zipfile.find_entry("Payload/#{File.basename(@file, File.extname(@file))}.app")
|
20
|
+
provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
|
21
|
+
|
22
|
+
if (!provisioning_profile_entry)
|
23
|
+
zipfile.dir.entries("Payload").each do |dir_entry|
|
24
|
+
if dir_entry =~ /.app$/
|
25
|
+
say "Using .app: #{dir_entry}"
|
26
|
+
app_entry = zipfile.find_entry("Payload/#{dir_entry}")
|
27
|
+
provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
say_error "Embedded mobile provisioning file not found in #{@file}" and abort unless provisioning_profile_entry
|
34
|
+
|
35
|
+
tempdir = ::File.new(Dir.mktmpdir)
|
36
|
+
begin
|
37
|
+
zipfile.each do |zip_entry|
|
38
|
+
temp_entry_path = ::File.join(tempdir.path, zip_entry.name)
|
39
|
+
|
40
|
+
FileUtils.mkdir_p(::File.dirname(temp_entry_path))
|
41
|
+
zipfile.extract(zip_entry, temp_entry_path) unless ::File.exist?(temp_entry_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
temp_provisioning_profile = ::File.new(::File.join(tempdir.path, provisioning_profile_entry.name))
|
45
|
+
temp_app_directory = ::File.new(::File.join(tempdir.path, app_entry.name))
|
46
|
+
|
47
|
+
plist = Plist::parse_xml(`security cms -D -i #{temp_provisioning_profile.path}`)
|
48
|
+
|
49
|
+
codesign = `codesign -dv "#{temp_app_directory.path}" 2>&1`
|
50
|
+
codesigned = /Signed Time/ === codesign
|
51
|
+
|
52
|
+
table = Terminal::Table.new do |t|
|
53
|
+
plist.each do |key, value|
|
54
|
+
next if key == "DeveloperCertificates"
|
55
|
+
|
56
|
+
columns = []
|
57
|
+
columns << key
|
58
|
+
columns << case value
|
59
|
+
when Hash
|
60
|
+
value.collect{|k, v| "#{k}: #{v}"}.join("\n")
|
61
|
+
when Array
|
62
|
+
value.join("\n")
|
63
|
+
else
|
64
|
+
value.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
t << columns
|
68
|
+
end
|
69
|
+
|
70
|
+
t << ["Codesigned", codesigned.to_s.capitalize]
|
71
|
+
end
|
72
|
+
|
73
|
+
puts table
|
74
|
+
|
75
|
+
rescue => e
|
76
|
+
say_error e.message
|
77
|
+
ensure
|
78
|
+
FileUtils.remove_entry_secure tempdir
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def determine_file!
|
86
|
+
files = Dir['*.ipa']
|
87
|
+
@file ||= case files.length
|
88
|
+
when 0 then nil
|
89
|
+
when 1 then files.first
|
90
|
+
else
|
91
|
+
@file = choose "Select an .ipa File:", *files
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Shenzhen::Plugins
|
4
|
+
module Crashlytics
|
5
|
+
class Client
|
6
|
+
|
7
|
+
def initialize(crashlytics_path, api_token, build_secret)
|
8
|
+
@api_token, @build_secret = api_token, build_secret
|
9
|
+
|
10
|
+
@crashlytics_path = Pathname.new("#{crashlytics_path}/submit").cleanpath.to_s
|
11
|
+
say_error "Path to Crashlytics.framework/submit is invalid" and abort unless File.exists?(@crashlytics_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def upload_build(ipa, options)
|
15
|
+
command = "#{@crashlytics_path} #{@api_token} #{@build_secret} -ipaPath '#{options[:file]}'"
|
16
|
+
command += " -notesPath '#{options[:notes]}'" if options[:notes]
|
17
|
+
command += " -emails #{options[:emails]}" if options[:emails]
|
18
|
+
command += " -groupAliases #{options[:groups]}" if options[:groups]
|
19
|
+
command += " -notifications #{options[:notifications] ? 'YES' : 'NO'}"
|
20
|
+
|
21
|
+
system command
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
command :'distribute:crashlytics' do |c|
|
28
|
+
c.syntax = "ipa distribute:crashlytics [options]"
|
29
|
+
c.summary = "Distribute an .ipa file over Crashlytics"
|
30
|
+
c.description = ""
|
31
|
+
c.option '-c', '--crashlytics_path PATH', "/path/to/Crashlytics.framework/"
|
32
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
33
|
+
c.option '-a', '--api_token TOKEN', "API Token. Available at https://www.crashlytics.com/settings/organizations"
|
34
|
+
c.option '-s', '--build_secret SECRET', "Build Secret. Available at https://www.crashlytics.com/settings/organizations"
|
35
|
+
c.option '-m', '--notes PATH', "Path to release notes file"
|
36
|
+
c.option '-e', '--emails EMAIL1,EMAIL2', "Emails of users for access"
|
37
|
+
c.option '-g', '--groups GROUPS', "Groups for users for access"
|
38
|
+
c.option '-n', '--notifications [YES | NO]', "Should send notification email to testers?"
|
39
|
+
|
40
|
+
c.action do |args, options|
|
41
|
+
determine_file! unless @file = options.file
|
42
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
43
|
+
|
44
|
+
determine_crashlytics_path! unless @crashlytics_path = options.crashlytics_path || ENV['CRASHLYTICS_FRAMEWORK_PATH']
|
45
|
+
say_error "Missing path to Crashlytics.framework" and abort unless @crashlytics_path
|
46
|
+
|
47
|
+
determine_crashlytics_api_token! unless @api_token = options.api_token || ENV['CRASHLYTICS_API_TOKEN']
|
48
|
+
say_error "Missing API Token" and abort unless @api_token
|
49
|
+
|
50
|
+
determine_crashlytics_build_secret! unless @build_secret = options.build_secret || ENV['CRASHLYTICS_BUILD_SECRET']
|
51
|
+
say_error "Missing Build Secret" and abort unless @build_secret
|
52
|
+
|
53
|
+
parameters = {}
|
54
|
+
parameters[:file] = @file
|
55
|
+
parameters[:notes] = options.notes if options.notes
|
56
|
+
parameters[:emails] = options.emails if options.emails
|
57
|
+
parameters[:groups] = options.groups if options.groups
|
58
|
+
parameters[:notifications] = options.notifications == 'YES' if options.notifications
|
59
|
+
|
60
|
+
client = Shenzhen::Plugins::Crashlytics::Client.new(@crashlytics_path, @api_token, @build_secret)
|
61
|
+
|
62
|
+
if client.upload_build(@file, parameters)
|
63
|
+
say_ok "Build successfully uploaded to Crashlytics"
|
64
|
+
else
|
65
|
+
say_error "Error uploading to Crashlytics" and abort
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def determine_crashlytics_path!
|
72
|
+
@crashlytics_path ||= ask "Path to Crashlytics.framework:"
|
73
|
+
end
|
74
|
+
|
75
|
+
def determine_crashlytics_api_token!
|
76
|
+
@api_token ||= ask "API Token:"
|
77
|
+
end
|
78
|
+
|
79
|
+
def determine_crashlytics_build_secret!
|
80
|
+
@build_secret ||= ask "Build Secret:"
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module DeployGate
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'deploygate.com'
|
10
|
+
|
11
|
+
def initialize(api_token, user_name)
|
12
|
+
@api_token, @user_name = api_token, user_name
|
13
|
+
@connection = Faraday.new(:url => "https://#{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
|
+
:token => @api_token,
|
25
|
+
:file => Faraday::UploadIO.new(ipa, 'application/octet-stream'),
|
26
|
+
:message => options[:message] || ''
|
27
|
+
})
|
28
|
+
|
29
|
+
@connection.post("/api/users/#{@user_name}/apps", 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 https://deploygate.com/ to see if the upload was completed." and abort
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
command :'distribute:deploygate' do |c|
|
41
|
+
c.syntax = "ipa distribute:deploygate [options]"
|
42
|
+
c.summary = "Distribute an .ipa file over deploygate"
|
43
|
+
c.description = ""
|
44
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
45
|
+
c.option '-a', '--api_token TOKEN', "API Token. Available at https://deploygate.com/settings"
|
46
|
+
c.option '-u', '--user_name USER_NAME', "User Name. Available at https://deploygate.com/settings"
|
47
|
+
c.option '-m', '--message MESSAGE', "Release message for the build"
|
48
|
+
c.option '-d', '--distribution_key DESTRIBUTION_KEY', "distribution key for distribution page"
|
49
|
+
c.option '-n', '--disable_notify', "disable notification"
|
50
|
+
c.option '-r', '--release_note RELEASE_NOTE', "release note for distribution page"
|
51
|
+
c.option '-v', '--visibility (private|public)', "privacy setting ( require public for personal free account)"
|
52
|
+
|
53
|
+
c.action do |args, options|
|
54
|
+
determine_file! unless @file = options.file
|
55
|
+
say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
|
56
|
+
|
57
|
+
determine_deploygate_api_token! unless @api_token = options.api_token || ENV['DEPLOYGATE_API_TOKEN']
|
58
|
+
say_error "Missing API Token" and abort unless @api_token
|
59
|
+
|
60
|
+
determine_deploygate_user_name! unless @user_name = options.user_name || ENV['DEPLOYGATE_USER_NAME']
|
61
|
+
say_error "Missing User Name" and abort unless @api_token
|
62
|
+
|
63
|
+
@message = options.message
|
64
|
+
@distribution_key = options.distribution_key || ENV['DEPLOYGATE_DESTRIBUTION_KEY']
|
65
|
+
@release_note = options.release_note
|
66
|
+
@disable_notify = ! options.disable_notify.nil? ? "yes" : nil
|
67
|
+
@visibility = options.visibility
|
68
|
+
@message = options.message
|
69
|
+
|
70
|
+
parameters = {}
|
71
|
+
parameters[:file] = @file
|
72
|
+
parameters[:message] = @message
|
73
|
+
parameters[:distribution_key] = @distribution_key if @distribution_key
|
74
|
+
parameters[:release_note] = @release_note if @release_note
|
75
|
+
parameters[:disable_notify] = @disable_notify if @disable_notify
|
76
|
+
parameters[:visibility] = @visibility if @visibility
|
77
|
+
parameters[:replace] = "true" if options.replace
|
78
|
+
|
79
|
+
client = Shenzhen::Plugins::DeployGate::Client.new(@api_token, @user_name)
|
80
|
+
response = client.upload_build(@file, parameters)
|
81
|
+
if (200...300) === response.status and not response.body["error"]
|
82
|
+
say_ok "Build successfully uploaded to DeployGate"
|
83
|
+
else
|
84
|
+
say_error "Error uploading to DeployGate: #{response.body["error"] || "(Unknown Error)"}" and abort
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def determine_deploygate_api_token!
|
91
|
+
@api_token ||= ask "API Token:"
|
92
|
+
end
|
93
|
+
|
94
|
+
def determine_deploygate_user_name!
|
95
|
+
@user_name ||= ask "User Name:"
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'openssl'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
module Shenzhen::Plugins
|
7
|
+
module Fir
|
8
|
+
class Client
|
9
|
+
HOSTNAME = 'fir.im'
|
10
|
+
VERSION = 'v2'
|
11
|
+
|
12
|
+
def initialize(user_token)
|
13
|
+
@user_token = user_token
|
14
|
+
|
15
|
+
@connection = Faraday.new(:url => "http://#{HOSTNAME}") do |builder|
|
16
|
+
builder.request :url_encoded
|
17
|
+
builder.response :json
|
18
|
+
builder.use FaradayMiddleware::FollowRedirects
|
19
|
+
builder.adapter :net_http
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_app_info(app_id)
|
24
|
+
options = {
|
25
|
+
:type => 'ios',
|
26
|
+
:token => @user_token,
|
27
|
+
}
|
28
|
+
|
29
|
+
@connection.get("/api/#{VERSION}/app/info/#{app_id}", options) do |env|
|
30
|
+
yield env[:status], env[:body] if block_given?
|
31
|
+
end
|
32
|
+
rescue Faraday::Error::TimeoutError
|
33
|
+
say_error "Timed out while geting app info." and abort
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_app_info(app_id, options)
|
37
|
+
@connection.put("/api/#{VERSION}/app/#{app_id}?token=#{@user_token}", options) do |env|
|
38
|
+
yield env[:status], env[:body] if block_given?
|
39
|
+
end
|
40
|
+
rescue Faraday::Error::TimeoutError
|
41
|
+
say_error "Timed out while geting app info." and abort
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload_build(ipa, options)
|
45
|
+
connection = Faraday.new(:url => options['url'], :request => { :timeout => 360 }) do |builder|
|
46
|
+
builder.request :multipart
|
47
|
+
builder.response :json
|
48
|
+
builder.use FaradayMiddleware::FollowRedirects
|
49
|
+
builder.adapter :net_http
|
50
|
+
end
|
51
|
+
|
52
|
+
options = {
|
53
|
+
:key => options['key'],
|
54
|
+
:token => options['token'],
|
55
|
+
:file => Faraday::UploadIO.new(ipa, 'application/octet-stream')
|
56
|
+
}
|
57
|
+
|
58
|
+
connection.post('/', options).on_complete do |env|
|
59
|
+
yield env[:status], env[:body] if block_given?
|
60
|
+
end
|
61
|
+
rescue Errno::EPIPE
|
62
|
+
say_error "Upload failed. Check internet connection is ok." and abort
|
63
|
+
rescue Faraday::Error::TimeoutError
|
64
|
+
say_error "Timed out while uploading build. Check https://fir.im// to see if the upload was completed." and abort
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
command :'distribute:fir' do |c|
|
71
|
+
c.syntax = "ipa distribute:fir [options]"
|
72
|
+
c.summary = "Distribute an .ipa file over fir.im"
|
73
|
+
c.description = ""
|
74
|
+
c.option '-f', '--file FILE', ".ipa file for the build"
|
75
|
+
c.option '-u', '--user_token TOKEN', "User Token. Available at http://fir.im/user/info"
|
76
|
+
c.option '-a', '--app_id APPID', "App Id (iOS Bundle identifier)"
|
77
|
+
c.option '-n', '--notes NOTES', "Release notes for the build"
|
78
|
+
c.option '-V', '--app_version VERSION', "App Version"
|
79
|
+
c.option '-S', '--short_version SHORT', "App Short Version"
|
80
|
+
|
81
|
+
c.action do |args, options|
|
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_fir_user_token! unless @user_token = options.user_token || ENV['FIR_USER_TOKEN']
|
86
|
+
say_error "Missing User Token" and abort unless @user_token
|
87
|
+
|
88
|
+
determine_fir_app_id! unless @app_id = options.app_id || ENV['FIR_APP_ID']
|
89
|
+
say_error "Missing App Id" and abort unless @app_id
|
90
|
+
|
91
|
+
determine_notes! unless @notes = options.notes
|
92
|
+
say_error "Missing release notes" and abort unless @notes
|
93
|
+
|
94
|
+
determine_app_version! unless @app_version = options.app_version
|
95
|
+
|
96
|
+
determine_short_version! unless @short_version = options.short_version
|
97
|
+
|
98
|
+
client = Shenzhen::Plugins::Fir::Client.new(@user_token)
|
99
|
+
app_response = client.get_app_info(@app_id)
|
100
|
+
if app_response.status == 200
|
101
|
+
upload_response = client.upload_build(@file, app_response.body['bundle']['pkg'])
|
102
|
+
|
103
|
+
if upload_response.status == 200
|
104
|
+
oid = upload_response.body['appOid']
|
105
|
+
today = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
106
|
+
@notes ||= "Upload on #{today}"
|
107
|
+
|
108
|
+
app_response = client.update_app_info(oid, {
|
109
|
+
:changelog => @notes,
|
110
|
+
:version => @app_version,
|
111
|
+
:versionShort => @short_version
|
112
|
+
})
|
113
|
+
|
114
|
+
if app_response.status == 200
|
115
|
+
app_short_uri = app_response.body['short']
|
116
|
+
say_ok "Build successfully uploaded to Fir, visit url: http://fir.im/#{app_short_uri}"
|
117
|
+
else
|
118
|
+
say_error "Error updating build information: #{app_response.body[:error]}" and abort
|
119
|
+
end
|
120
|
+
else
|
121
|
+
say_error "Error uploading to Fir: #{upload_response.body[:error]}" and abort
|
122
|
+
end
|
123
|
+
else
|
124
|
+
say_error "Error getting app information: #{response.body[:error]}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def determine_fir_user_token!
|
131
|
+
@user_token ||= ask "User Token:"
|
132
|
+
end
|
133
|
+
|
134
|
+
def determine_fir_app_id!
|
135
|
+
@app_id ||= ask "App Id:"
|
136
|
+
end
|
137
|
+
|
138
|
+
def determine_app_version!
|
139
|
+
@app_version ||= ask "App Version:"
|
140
|
+
end
|
141
|
+
|
142
|
+
def determine_short_version!
|
143
|
+
@short_version ||= ask "Short Version:"
|
144
|
+
end
|
145
|
+
end
|