fastlane-plugin-dynatrace 1.0.1 → 1.0.2

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