fastlane-plugin-dynatrace 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d33abfce5cb589461d864dbb9dd94561b35d4c70079456144c2b15a71b23493
4
- data.tar.gz: 41e48c8dc20f1258efd8ec0e0fa24768eba1b2ab344c103ec85abc4192c4f151
3
+ metadata.gz: 920b0c5553ebb02953f85441f6ab8a4bba712003435f354e3f11458c0c23f7f9
4
+ data.tar.gz: 64d293694d00bd694df462fbd5ee5e16ed12359c93e18817e9bf9fe8a92f9c5d
5
5
  SHA512:
6
- metadata.gz: 335db4d66fedc928e82ed2aa3e1394ef0c6cea0010cd8d601417bbf4b55980e6ba654c9392edfd67aca67d67eceb98d09cc6092212af8ecf79e6039f904f0a0c
7
- data.tar.gz: c4f8d63463b4edf3c5c50724784372f403d2d2ea35d7da7f3af4278631d0010a14a904bec8a0ceefcbdd4ddf41e1bf709557ca902a3f1e3cb4fa03070610719a
6
+ metadata.gz: 9be84816a06cf7df02d04d625c7b982e317a7b23a62f839de9cb1600c18ea08779c7f5d23d71de5cf802d507240c7826c61dd4daa07dd939f6e41cad9ad656ac
7
+ data.tar.gz: d452461f103ff34d7b290cd5a1bd3f4e988c01956779707cc5166572665101c817a3f880865d5ead209a547864b3a2e894875aa93e03cccdb4c3010748e560f4
data/README.md CHANGED
@@ -100,7 +100,7 @@ The full documentation for this can be found on the [fastlane docs](https://docs
100
100
 
101
101
  You can generate a session by running `fastlane spaceauth -u user@email.com` on your machine and copy the output into an environment variable `FASTLANE_SESSION` on the target system (e.g. CI).
102
102
 
103
- ###NOTE
103
+ ### NOTE
104
104
  - Session is only valid in the "region" you create it. If you CI is in a different geographical location the authentication might fail.
105
105
 
106
106
  - Generated sessions are valid up to one month. Apple's API doesn't specify details about that, so it will only be visible by a failing build.
@@ -108,6 +108,9 @@ You can generate a session by running `fastlane spaceauth -u user@email.com` on
108
108
  ## Example
109
109
  Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
110
110
 
111
+ ## Tests
112
+ This plugin includes a set of RSpec unit tests, which can be executed by running ` bundle exec rspec spec`.
113
+
111
114
  ## Issues and Feedback
112
115
  For any other issues and feedback about this plugin, please submit it to this repository or contact [Dynatrace Support](https://support.dynatrace.com).
113
116
 
@@ -2,7 +2,9 @@ require 'fastlane/action'
2
2
  require 'net/http'
3
3
  require 'open-uri'
4
4
  require 'zip'
5
- require "fileutils"
5
+ require 'fileutils'
6
+ require 'os'
7
+ require 'json'
6
8
  require_relative '../helper/dynatrace_helper'
7
9
 
8
10
  module Fastlane
@@ -26,64 +28,87 @@ module Fastlane
26
28
  UI.message "BundleID: #{bundleId}"
27
29
  end
28
30
 
31
+ if params[:os] == "android"
32
+ response = Helper::DynatraceHelper.put_android_symbols(params, bundleId)
33
+ case response.code
34
+ when '204'
35
+ UI.success "Successfully uploaded the mapping file (#{params[:symbolsfile]}) to Dynatrace."
36
+ when '400'
37
+ UI.user_error! "Failed to upload. The input is invalid."
38
+ when '401'
39
+ UI.user_error! "Invalid Dynatrace API token. See https://www.dynatrace.com/support/help/dynatrace-api/basics/dynatrace-api-authentication/#token-permissions and https://www.dynatrace.com/support/help/dynatrace-api/configuration-api/mobile-symbolication-api/"
40
+ when '413'
41
+ UI.user_error! "Failed to upload. The symbol file storage quota is exhausted. See https://www.dynatrace.com/support/help/shortlink/mobile-symbolication#manage-the-uploaded-symbol-files for more information."
42
+ else
43
+ message = JSON.parse(response.body)["error"]["message"]
44
+ if message.nil?
45
+ UI.user_error! "Symbol upload error (Response Code: #{response.code}). Please try again in a few minutes or contact the Dynatrace support (https://www.dynatrace.com/services-support/)."
46
+ else
47
+ UI.user_error! "Symbol upload error (Response Code: #{response.code}). #{message}"
48
+ end
49
+ end
50
+ return
51
+ elsif params[:os] != "ios"
52
+ UI.user_error! "Unsopported value os=#{params[:os]}"
53
+ end
54
+
55
+ # iOS workflow
29
56
  dtxDssClientPath = Helper::DynatraceHelper.get_dss_client(params)
30
57
 
31
58
  dsym_paths = []
32
59
  symbolFilesKey = "symbolsfile" # default to iOS
33
60
 
34
- if params[:os] == "ios"
35
- if params[:downloadDsyms] == true
36
- UI.message "Downloading dSYMs from App Store Connect"
37
- startTime = Time.now
61
+ if !OS.mac?
62
+ UI.user_error! "A macOS machine is required to process iOS symbols."
63
+ end
38
64
 
39
- UI.message "Checking AppFile for possible username/AppleID"
40
- username = CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
41
- if username
42
- UI.message "Using #{username} from your AppFile"
43
- else
44
- username = params[:username]
45
- UI.message "Didn't find a username in AppFile, using passed username parameter: #{params[:username]}"
46
- end
65
+ if params[:downloadDsyms] == true
66
+ UI.message "Downloading dSYMs from App Store Connect"
67
+ startTime = Time.now
47
68
 
48
- # it takes a couple of minutes until the new build is available through the API
49
- # -> retry until available
50
- while params[:waitForDsymProcessing] and # wait is active
51
- !lane_context[SharedValues::DSYM_PATHS] and # has dsym path
52
- (Time.now - startTime) < params[:waitForDsymProcessingTimeout] # is in time
53
-
54
- Actions::DownloadDsymsAction.run(wait_for_dsym_processing: params[:waitForDsymProcessing],
55
- wait_timeout: (params[:waitForDsymProcessingTimeout] - (Time.now - startTime)).round(0), # remaining timeout
56
- app_identifier: bundleId,
57
- username: username,
58
- version: params[:version],
59
- build_number: params[:versionStr],
60
- platform: :ios) # should be optional (Hint: it's not)
61
-
62
- if !lane_context[SharedValues::DSYM_PATHS] and (Time.now - startTime) < params[:waitForDsymProcessingTimeout]
63
- UI.message "Version #{params[:version]} (Build #{params[:versionStr]}) isn't listed yet, retrying in 60 seconds (timeout in #{(params[:waitForDsymProcessingTimeout] - (Time.now - startTime)).round(0)} seconds)."
64
- sleep(60)
65
- end
66
- end
69
+ UI.message "Checking AppFile for possible username/AppleID"
70
+ username = CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
71
+ if username
72
+ UI.message "Using #{username} from your AppFile"
73
+ else
74
+ username = params[:username]
75
+ UI.message "Didn't find a username in AppFile, using passed username parameter: #{params[:username]}"
76
+ end
67
77
 
68
- if (Time.now - startTime) > params[:waitForDsymProcessingTimeout]
69
- UI.user_error!("Timeout during dSYM download. Try increasing :waitForDsymProcessingTimeout.")
78
+ # it takes a couple of minutes until the new build is available through the API
79
+ # -> retry until available
80
+ while params[:waitForDsymProcessing] and # wait is active
81
+ !lane_context[SharedValues::DSYM_PATHS] and # has dsym path
82
+ (Time.now - startTime) < params[:waitForDsymProcessingTimeout] # is in time
83
+
84
+ Actions::DownloadDsymsAction.run(wait_for_dsym_processing: params[:waitForDsymProcessing],
85
+ wait_timeout: (params[:waitForDsymProcessingTimeout] - (Time.now - startTime)).round(0), # remaining timeout
86
+ app_identifier: bundleId,
87
+ username: username,
88
+ version: params[:version],
89
+ build_number: params[:versionStr],
90
+ platform: :ios) # should be optional (Hint: it's not)
91
+
92
+ if !lane_context[SharedValues::DSYM_PATHS] and (Time.now - startTime) < params[:waitForDsymProcessingTimeout]
93
+ UI.message "Version #{params[:version]} (Build #{params[:versionStr]}) isn't listed yet, retrying in 60 seconds (timeout in #{(params[:waitForDsymProcessingTimeout] - (Time.now - startTime)).round(0)} seconds)."
94
+ sleep(60)
70
95
  end
96
+ end
71
97
 
72
- dsym_paths += Actions.lane_context[SharedValues::DSYM_PATHS] if Actions.lane_context[SharedValues::DSYM_PATHS]
98
+ if (Time.now - startTime) > params[:waitForDsymProcessingTimeout]
99
+ UI.user_error!("Timeout during dSYM download. Try increasing :waitForDsymProcessingTimeout.")
100
+ end
73
101
 
74
- if dsym_paths.count > 0
75
- UI.message "Downloaded the dSYMs from App Store Connect. Paths: #{dsym_paths}"
76
- else
77
- raise 'No dSYM paths found!'
78
- end
102
+ dsym_paths += Actions.lane_context[SharedValues::DSYM_PATHS] if Actions.lane_context[SharedValues::DSYM_PATHS]
103
+
104
+ if dsym_paths.count > 0
105
+ UI.message "Downloaded the dSYMs from App Store Connect. Paths: #{dsym_paths}"
79
106
  else
80
- UI.error "dSYM download disabled, using local path (#{params[:symbolsfile]})"
81
- dsym_paths << params[:symbolsfile] if params[:symbolsfile]
107
+ raise 'No dSYM paths found!'
82
108
  end
83
-
84
- else # android
85
- dsym_paths << params[:symbolsfile] if params[:symbolsfile]
86
- symbolFilesKey = "file"
109
+ else
110
+ UI.important "dSYM download disabled, using local path (#{params[:symbolsfile]})"
111
+ dsym_paths << params[:symbolsfile] if params[:symbolsfile]
87
112
  end
88
113
 
89
114
  # check if we have dSYMs to proceed with
@@ -106,7 +131,7 @@ module Fastlane
106
131
  command << "versionStr=\"#{params[:versionStr]}\""
107
132
  command << "version=\"#{params[:version]}\""
108
133
  command << symbolFilesCommandSnippet
109
- command << "server=\"#{Helper::DynatraceHelper.get_server_base_url(params)}\""
134
+ command << "server=\"#{Helper::DynatraceHelper.get_base_url(params)}\""
110
135
  command << "DTXLogLevel=ALL -verbose" if params[:debugMode] == true
111
136
  command << "forced=1" # if the file already exists
112
137
 
@@ -1,4 +1,8 @@
1
1
  require 'fastlane_core/ui/ui'
2
+ require 'digest'
3
+ require 'net/http'
4
+ require 'tempfile'
5
+ require 'uri'
2
6
 
3
7
  module Fastlane
4
8
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
@@ -7,69 +11,166 @@ module Fastlane
7
11
  class DynatraceHelper
8
12
  def self.get_dss_client(params)
9
13
  dynatraceDir = "dynatrace"
10
- versionFile = "version"
11
14
  dtxDssClientBin = "DTXDssClient"
15
+ versionFilePath = "#{dynatraceDir}/version"
12
16
  dtxDssClientPath = "#{dynatraceDir}/#{dtxDssClientBin}"
13
17
 
14
- if (params.all_keys.include? :dtxDssClientPath and not params[:dtxDssClientPath].nil?)
15
- UI.message "DEPRECATION WARNING: DTXDssClientPath doesn't need to be specified anymore, the DTXDssClient is downloaded and updated automatically."
16
- dtxDssClientPath = params[:dtxDssClientPath]
17
- else
18
- # get latest version info
19
- clientUri = URI("#{self.get_server_base_url(params)}/api/config/v1/symfiles/dtxdss-download?Api-Token=#{params[:apitoken]}")
20
- response = Net::HTTP.get_response(clientUri)
21
-
22
- if not response.kind_of? Net::HTTPSuccess
23
- base_error = "Couldn't update DTXDssClient (invalid response: #{response.message} (#{response.code})) for URL: #{clientUri})"
24
- if File.exists?("#{dynatraceDir}/#{dtxDssClientBin}")
25
- UI.important base_error
26
- UI.important "Using cached DTXDssClient: #{dynatraceDir}/#{dtxDssClientBin}"
27
- return dtxDssClientPath
28
- else
29
- UI.user_error! base_error
30
- end
31
- end
18
+ if params.all_keys.include? :dtxDssClientPath and not params[:dtxDssClientPath].nil?
19
+ UI.important "DEPRECATION WARNING: dtxDssClientPath doesn't need to be specified anymore, the #{dtxDssClientBin} is downloaded and updated automatically."
20
+ return params[:dtxDssClientPath]
21
+ end
32
22
 
33
- remoteClientUrl = JSON.parse(response.body)["dssClientUrl"]
34
- UI.message "Remote DSS client: #{remoteClientUrl}"
23
+ # get latest version info
24
+ clientUri = URI("#{self.get_base_url(params)}/api/config/v1/symfiles/dtxdss-download?Api-Token=#{params[:apitoken]}")
25
+ response = Net::HTTP.get_response(clientUri)
35
26
 
36
- # check local state
37
- if (!File.directory?(dynatraceDir))
38
- Dir.mkdir(dynatraceDir)
27
+ # filter any http errors
28
+ if not response.kind_of? Net::HTTPSuccess
29
+ error_msg = "Couldn't update #{dtxDssClientBin} (invalid response: #{response.message} (#{response.code})) for URL: #{self.to_redacted_api_token_string(clientUri)})"
30
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
31
+ return dtxDssClientPath
32
+ end
33
+
34
+ # parse body
35
+ begin
36
+ responseJson = JSON.parse(response.body)
37
+ rescue JSON::GeneratorError,
38
+ JSON::JSONError,
39
+ JSON::NestingError,
40
+ JSON::ParserError
41
+ error_msg = "Error parsing response body: #{response.body} from URL (#{self.to_redacted_api_token_string(clientUri)}), failed with error #{$!}"
42
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
43
+ return dtxDssClientPath
44
+ end
45
+
46
+ # parse url
47
+ remoteClientUrl = responseJson["dssClientUrl"]
48
+ if remoteClientUrl.nil? or remoteClientUrl.empty?
49
+ error_msg = "No value for dssClientUrl in response body (#{response.body})."
50
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
51
+ return dtxDssClientPath
52
+ end
53
+ UI.message "Remote DSS client: #{remoteClientUrl}"
54
+
55
+ # check/update local state
56
+ if !File.directory?(dynatraceDir)
57
+ Dir.mkdir(dynatraceDir)
58
+ end
59
+
60
+ # only update if a file is missing or the local version is different
61
+ if !(File.exists?(versionFilePath) and
62
+ File.exists?(dtxDssClientPath) and
63
+ File.read(versionFilePath) == remoteClientUrl and
64
+ File.size(dtxDssClientPath) > 0)
65
+ updatedClient = false
66
+
67
+ # extract and save client
68
+ zipped_tmp = self.save_to_tempfile(remoteClientUrl)
69
+ if File.size(zipped_tmp) <= 0
70
+ error_msg = "Downloaded symbolication client archive is empty (0 bytes)."
71
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
72
+ return dtxDssClientPath
39
73
  end
40
74
 
41
- if (!File.exists?("#{dynatraceDir}/#{versionFile}") or
42
- !File.exists?("#{dynatraceDir}/#{dtxDssClientBin}") or
43
- File.read("#{dynatraceDir}/#{versionFile}") != remoteClientUrl)
44
- # update local state
45
- UI.message "Found a different remote DTXDssClient client. Updating local version."
46
- File.delete("#{dynatraceDir}/#{versionFile}") if File.exist?("#{dynatraceDir}/#{versionFile}")
47
- File.delete("#{dynatraceDir}/#{dtxDssClientBin}") if File.exist?("#{dynatraceDir}/#{dtxDssClientBin}")
48
-
49
- File.write("#{dynatraceDir}/#{versionFile}", remoteClientUrl)
50
-
51
- # get client from served archive
52
- open(remoteClientUrl) do |zipped|
53
- Zip::InputStream.open(zipped) do |unzipped|
54
- entry = unzipped.get_next_entry
55
- if (entry.name == dtxDssClientBin)
56
- IO.copy_stream(entry.get_input_stream, "#{dynatraceDir}/#{dtxDssClientBin}")
57
- FileUtils.chmod("+x", "#{dynatraceDir}/#{dtxDssClientBin}")
58
- end
75
+ begin
76
+ UI.message "Unzipping fetched file with MD5 hash: #{Digest::MD5.new << IO.read(zipped_tmp)}"
77
+ Zip::InputStream.open(zipped_tmp) do |unzipped|
78
+ entry = unzipped.get_next_entry
79
+ if (entry.name == dtxDssClientBin)
80
+ # remove old client
81
+ UI.message "Found a different remote #{dtxDssClientBin} client. Removing local version and updating."
82
+ File.delete(versionFilePath) if File.exist?(versionFilePath)
83
+ File.delete(dtxDssClientPath) if File.exist?(dtxDssClientPath)
84
+
85
+ # write new client
86
+ File.write(versionFilePath, remoteClientUrl)
87
+ IO.copy_stream(entry.get_input_stream, dtxDssClientPath)
88
+ FileUtils.chmod("+x", dtxDssClientPath)
89
+ updatedClient = true
59
90
  end
60
91
  end
92
+ rescue Zip::DecompressionError,
93
+ Zip::DestinationFileExistsError,
94
+ Zip::EntryExistsError,
95
+ Zip::EntryNameError,
96
+ Zip::EntrySizeError,
97
+ Zip::GPFBit3Error,
98
+ Zip::InternalError,
99
+ Zip::CompressionMethodError
100
+ error_msg = "Could not update/extract #{dtxDssClientBin}, please try again."
101
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
102
+ return dtxDssClientPath
103
+ end
104
+
105
+ if updatedClient
106
+ UI.success "Successfully updated DTXDssClient."
107
+ else
108
+ error_msg = "#{dtxDssClientBin} not found in served archive, please try again."
109
+ self.check_fallback_or_raise(dtxDssClientPath, error_msg)
110
+ return dtxDssClientPath
61
111
  end
62
112
  end
63
113
  return dtxDssClientPath
64
114
  end
65
115
 
66
- def self.get_server_base_url(params)
67
- if params[:server][-1] == '/'
68
- return params[:server][0..-2]
116
+ def self.get_base_url(params)
117
+ uri = URI.split(params[:server])
118
+ return uri[0] + '://' + uri[2]
119
+ end
120
+
121
+ def self.get_host_name(params)
122
+ uri = URI.split(params[:server])
123
+
124
+ unless uri[2].nil?
125
+ return uri[2]
126
+ end
127
+
128
+ # no procotol prefix -> host name is with path
129
+ if uri[5][-1] == '/'
130
+ return uri[5][0..-2] # remove trailing /
131
+ else
132
+ return uri[5]
133
+ end
134
+ end
135
+
136
+ def self.put_android_symbols(params, bundleId)
137
+ path = "/api/config/v1/symfiles/#{params[:appId]}/#{bundleId}/ANDROID/#{params[:version]}/#{params[:versionStr]}"
138
+
139
+ req = Net::HTTP::Put.new(path, initheader = { 'Content-Type' => 'text/plain',
140
+ 'Authorization' => "Api-Token #{params[:apitoken]}"} )
141
+
142
+ req.body = IO.read(params[:symbolsfile])
143
+ http = Net::HTTP.new(self.get_host_name(params), 443)
144
+ http.use_ssl = true
145
+ http.request(req)
146
+ end
147
+
148
+ private
149
+ def self.check_fallback_or_raise(fallback_client, error)
150
+ UI.important "If this error persists create an issue on our Github project (https://github.com/Dynatrace/fastlane-plugin-dynatrace/issues) or contact our support at https://www.dynatrace.com/support/contact-support/."
151
+ UI.important error
152
+ if File.exists?(fallback_client) and File.size(fallback_client) > 0
153
+ UI.important "Using cached client: #{fallback_client}"
69
154
  else
70
- return params[:server]
155
+ UI.important "No cached fallback found."
156
+ raise error
71
157
  end
72
158
  end
159
+
160
+ # assumes the token parameter is appended last (there is only one parameter anyway)
161
+ def self.to_redacted_api_token_string(url)
162
+ urlStr = url.to_s
163
+ str = "Api-Token="
164
+ idx = urlStr.index(str)
165
+ token_len = urlStr.length - idx - str.length
166
+ urlStr[idx + str.length..idx + str.length + token_len] = "-" * token_len
167
+ return urlStr
168
+ end
169
+
170
+ # for test mocking
171
+ def self.save_to_tempfile(url)
172
+ open(url)
173
+ end
73
174
  end
74
175
  end
75
176
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Dynatrace
3
- VERSION = "1.0.1"
3
+ VERSION = "1.0.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-dynatrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dynatrace LLC
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-03 00:00:00.000000000 Z
11
+ date: 2021-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
167
  - !ruby/object:Gem::Version
168
168
  version: '0'
169
169
  requirements: []
170
- rubygems_version: 3.2.3
170
+ rubygems_version: 3.1.4
171
171
  signing_key:
172
172
  specification_version: 4
173
173
  summary: This action processes and uploads your symbol files to Dynatrace