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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 920b0c5553ebb02953f85441f6ab8a4bba712003435f354e3f11458c0c23f7f9
|
4
|
+
data.tar.gz: 64d293694d00bd694df462fbd5ee5e16ed12359c93e18817e9bf9fe8a92f9c5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
35
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
98
|
+
if (Time.now - startTime) > params[:waitForDsymProcessingTimeout]
|
99
|
+
UI.user_error!("Timeout during dSYM download. Try increasing :waitForDsymProcessingTimeout.")
|
100
|
+
end
|
73
101
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
-
dsym_paths << params[:symbolsfile] if params[:symbolsfile]
|
107
|
+
raise 'No dSYM paths found!'
|
82
108
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
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.
|
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
|
15
|
-
UI.
|
16
|
-
|
17
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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.
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
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.
|
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-
|
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.
|
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
|