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