fastlane-plugin-dynatrace 0.1.5 → 1.0.3

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: db0adc22d10e8104c2ec6776dc2315936943ee06f91810735185b570792ee9a8
4
- data.tar.gz: a85ed623888c6bc6b3e805783d7b449a308aea8f60e61a986244076ad006060a
3
+ metadata.gz: e893f895c684763a074f06dfa448156b4f4bdf639f7d287e409cd09c437a1709
4
+ data.tar.gz: c758ebf1327254eed71f2b138efe9b50b9058a38a3e2a235411fcbeb06fcefb2
5
5
  SHA512:
6
- metadata.gz: f9f797b1415f7a9e534581f97f7425c0a5341f9c4c290efef0254fb6d5e4daac16ab07fab14cdfe4aa572c950cb1fe3cfbe2b362859e55cf21ee7adc1b921e75
7
- data.tar.gz: 911f7bf2bcef8ecdacec9ffd73250ae382bd0c9fd0297694cc16821183aadc0b0e1244e037fb4c0be998725067a6d9f65f87386ce26db6befcbd930a604d027b
6
+ metadata.gz: 3ba82e96e1e13c05504f5f732f7313ad10b743664da56f7198d4e5701e278c92a6fb4512088419200fede8b6e596f447a3ebccf921356a072fd067a2c333a978
7
+ data.tar.gz: 3f752665d4a5128e32d8340b795ce5ab323b97ae4314619f4d0f78d9482c572a273aa677db50cd6fc2786c35be1731a51e9cc330c33a09706bab22dfeba042dc
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # dynatrace plugin
1
+ # Dynatrace Fastlane plugin
2
2
 
3
3
  [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-dynatrace)
4
4
 
@@ -10,115 +10,115 @@ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To
10
10
  fastlane add_plugin dynatrace
11
11
  ```
12
12
 
13
- Make sure you have the latest version of the DSS client, which is bundled with the agent. You can download it from here [Latest iOS Agent](https://downloads.dynatrace.com/clientservices/agent?version=latest&techtype=ios).
14
-
15
- > Please note that sometimes the latest version of the DSS client may not be compatble with your environment if you have a managed cluster on an older version. In this case, please contact Dynatrace Support to obtain the correct version.
13
+ ### Dynatrace Managed
14
+ If the installation is on version 1.195 or earlier the Symbolication Client has to be manually download and specified (`dtxDssClientPath`), else it's fetched and updated automatically. A matching version can be downloaded manually with this link [https://api.mobileagent.downloads.dynatrace.com/sprint-latest-dss-client/xyz](https://api.mobileagent.downloads.dynatrace.com/sprint-latest-dss-client/xyz) by replacing `xyz` with the 3-digit sprint version of your Dynatrace Managed installation.
16
15
 
17
16
  ## About the Dynatrace fastlane plugin
18
-
19
- This plugin allows you to decode and upload symbolication files to Dynatrace. You can also use it to first download your latest dSYM files from AppStore Connect if you use Bitcode.
17
+ This plugin allows you to decode and upload symbol files (iOS) or just upload obfuscation mapping files (Android) to Dynatrace. You can also use it to first download your latest dSYM files from App Store Connect if you use Bitcode.
20
18
 
21
19
  ## Action: `dynatrace_process_symbols`
22
20
 
23
21
  | Supported Platforms | ios, android |
24
22
  |---------------------|--------------|
25
- | Author | @MANassar |
26
-
27
23
 
28
24
  ## Is your app Bitcode enabled?
29
-
30
25
  > Only applies for apps distributed via the AppStore or TestFlight.
31
26
 
32
-
33
- If your app is bitcode enabled, then the dSYMs that are generated during the Xcode build are **_not_** the dSYMs you want to upload to Dynatrace. This is because Apple recompiles your application on their servers, generating new dSYM files in the process. These newly generated dSYM files need to be downloaded from *AppStore Connect*, then processed and uploaded to Dynatrace.
27
+ If your app is bitcode enabled, then the dSYMs that are generated during the Xcode build are **_not_** the dSYMs you want to upload to Dynatrace. This is because Apple recompiles the application on their servers, generating new dSYM files in the process. These newly generated dSYM files need to be downloaded from *App Store Connect*, then processed and uploaded to Dynatrace.
34
28
 
35
29
  ### Important
30
+ There is a time gap between the application being uploaded to App Store Connect and the dSYM files to be ready. So **_we have to introduce some "wait" time in the CI to accomodate for this_**. You can do this by setting the `waitForDsymProcessing` to true. Unfortunately, Apple does not specify how long this time is. We recommend 1800 seconds (30 mins) as this is usually enough for the symbols are ready for download. You can increase this timeout if needed (`waitForDsymProcessingTimeout`).
36
31
 
37
- There is a time gap between the application being uploaded to AppStore Connect and the dSYM files to be ready. So **_you have to introduce some "sleep" or "wait" time in your CI to accomodate for this._** Unfortunately, Apple does not specify how long this time is. But the recommended minimum is 300 seconds (5 minutes).
32
+ > Notice that this timeout is only the **maximum** waiting time. If the symbol files are ready sooner, it will continue processing and will not wait for the whole duration of the timeout.
38
33
 
39
34
  ### Automatically downloading dSYMs and using AppFile for authentication
40
35
 
41
36
  #### AppFile
42
-
43
37
  ```ruby
44
- app_identifier("com.yourcompany.yourappID") # The bundle identifier of your app
45
- apple_id("user@email.com") # Your Apple email address
38
+ app_identifier("com.yourcompany.yourappID") # bundle identifier of your app
39
+ apple_id("user@email.com")
46
40
  ```
47
41
 
48
42
  #### Fastfile
49
-
50
43
  ```ruby
51
44
  dynatrace_process_symbols(
52
- downloadDsyms: true,
53
- dtxDssClientPath:"<path>/DTXDssClient",
54
- appId: "your DT appID",
55
- apitoken: "your DT API token",
56
- os: "<ios> or <android>",
57
- bundleName: "MyApp",
58
- versionStr: "1.0",
59
- version: "1",
60
- server: "<your dynatrace environment URL>",
61
- debugMode: true)
62
-
45
+ appId: "<Dynatrace application ID>",
46
+ apitoken: "<Dynatrace API token>",
47
+ os: "ios",
48
+ bundleId: "<CFBundlebundleId (iOS) / package (Android)>",
49
+ versionStr: "<Build Number (CFBundleVersion)>",
50
+ version: "<App Version (CFBundleShortVersionString)>",
51
+ server: "<Dynatrace Environment URL>",
52
+ downloadDsyms: true
53
+ )
63
54
  ```
64
55
 
65
- ## If you are NOT using Bitcode, or if you have already downloaded your new symbols from AppStore Connect manually.
66
56
 
67
- ### Supply all parameters locally
57
+ ## If you are NOT using Bitcode, or if you have already downloaded your new symbols from App Store Connect manually.
68
58
 
59
+ ### Supply all parameters locally
69
60
  ```ruby
70
61
  dynatrace_process_symbols(
71
- dtxDssClientPath:"<path>/DTXDssClient",
72
- appId: "your DT appID",
73
- apitoken: "your DT API token",
62
+ appId: "<Dynatrace application ID>",
63
+ apitoken: "<Dynatrace API Token>",
74
64
  os: "<ios> or <android>",
75
- bundleId: "com.yourcompany.yourApp",
76
- bundleName: "MyApp",
77
- versionStr: "1.0",
78
- version: "1",
79
- symbolsfile: "<path to my app>.app.dSYM",
80
- server: "<your dynatrace environment URL>",
81
- debugMode: true)
82
-
65
+ bundleId: "<CFBundlebundleId (iOS) / package (Android)>",
66
+ versionStr: "<CFBundleShortVersionString (iOS) / versionName (Android)>",
67
+ version: "<CFBundleVersion (iOS) / versionCode (Android)>",
68
+ server: "<Dynatrace Environment URL>",
69
+ symbolsfile: "<Symbols File Path>"
70
+ )
83
71
  ```
84
72
 
85
73
  ## List of all Parameters
86
-
87
- | Key | Description | default value |
88
- |------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
89
- | downloadDsyms | Boolean variable that enables downloading the dSYMs from AppStore Connect (iOS only) | false |
90
- | username | The username or the AppleID to use to download the dSYMs. You can also store this in your AppFile as "apple_id and it will be automatically retrieved." | |
91
- | dtxDssClientPath | The full path to your DTXDssClient. For example, it could be `./ios/agent/DTXDssClient` | `./DTXDssClient` |
92
- | action | The action to perform. upload/decode | `upload` |
93
- | appID | The app ID you get from your Dynatrace WebUI | |
94
- | os | The OperatingSystem of the symbol files. Either "ios" or "android" | |
95
- | apitoken | The Dynatrace API token. It should have the correct permissions. | |
96
- | bundleId | The CFBundlebundleId (iOS) / package (Android) of the Application. Usually in reverse com notation. Ex. com.your_company.your_app. This can also be stored in the AppFile as "app_identifier" and it will be automatically retrieved. | |
97
- | bundleName | The CFBundleName of the Application (iOS only) | |
98
- | versionStr | The CFBundleShortVersionString (iOS) / versionName (Android | |
99
- | version | The CFBundleVersion (iOS) / versionCode (Android). This will also be used for dSYM download. | |
100
- | symbolsfile | The path to a local symbol files to be processed and uploaded. You do not need to specify that if you use downloadDsyms. | |
101
- | server | The API endpoint for the Dynatrace environment. For example https://environmentID.live.dynatrace.com or https://dynatrace-managed.com/e/environmentID | |
102
- | debugMode | Debug logging enabled | false |
103
-
74
+ | Key | Description | default value |
75
+ |------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|
76
+ | action | *(iOS only)* Action to be performed by DTXDssClient (`upload` or `decode`). | `upload` |
77
+ | downloadDsyms | *(iOS only)* Download the dSYMs from App Store Connect. | `false` |
78
+ | waitForDsymProcessing | *(iOS only)* Wait for dSYM processing to be finished. | `true` |
79
+ | waitForDsymProcessingTimeout | *(iOS only)* Timeout in seconds to wait for the dSYMs be downloadable. | `1800` |
80
+ | username | *(iOS only)* The username/AppleID to use to download the dSYMs. Alternatively you can specify this in your AppFile as `apple_id`. | |
81
+ | os | The type of the symbol files, either `ios` or `android`. | |
82
+ | apitoken | Dynatrace API token with mobile symbolication permissions. | |
83
+ | dtxDssClientPath | **(DEPRECATED)** The path to your DTXDssClient. The DTXDssClient is downloaded and updated automatically, unless this key is set. | |
84
+ | appID | The application ID you get from your Dynatrace environment. | |
85
+ | bundleId | The CFBundlebundleId (iOS) / package (Android) of the application. Alternatively you can specify this in your AppFile as `app_identifier`. | |
86
+ | versionStr | The CFBundleShortVersionString (iOS) / versionName (Android) | |
87
+ | version | The CFBundleVersion (iOS) / versionCode (Android). Is also used for the dSYM download. | |
88
+ | symbolsfile | Path to the dSYM file to be processed. If downloadDsyms is set, this is only a fallback. | |
89
+ | server | The API endpoint for the Dynatrace environment (e.g. `https://environmentID.live.dynatrace.com` or `https://dynatrace-managed.com/e/environmentID`). | |
90
+ | debugMode | Enable debug logging. | false |
91
+
92
+ ## AppStore Connect Two-Factor-Authentication
93
+ When the plugin is used to download symbols from *AppStore Connect* automatically (`downloadDsyms: true`) valid login credentials to an account with permissions to access the dSYM files are required. The preferred method of doing so is by setting the `FASTLANE_USER` and `FASTLANE_PASSWORD` environment variables to their respective values.
94
+
95
+ [Apple announced](https://github.com/fastlane/fastlane/discussions/17655) that 2-Factor-Authentication for the *AppStore Connect* API will be enforced starting February 2021. This limits the ability to automate the symbol processing, because it will most likely involve manual interaction, which is not suitable for CI automation. The only workaround at this point in time is to pre-generate a session and cache it in CI.
96
+
97
+ ### Fastlane Session
98
+ The full documentation for this can be found on the [fastlane docs](https://docs.fastlane.tools/best-practices/continuous-integration/#two-step-or-two-factor-auth
99
+ ) under **spaceauth**.
100
+
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
+
103
+ ### NOTE
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
+
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.
104
107
 
105
108
  ## Example
106
-
107
109
  Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
108
110
 
111
+ ## Tests
112
+ This plugin includes a set of RSpec unit tests, which can be executed by running ` bundle exec rspec spec`.
109
113
 
110
114
  ## Issues and Feedback
111
-
112
- For any other issues and feedback about this plugin, please submit it to this repository or contact Dynatrace Support.
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
 
114
117
  ## Troubleshooting
115
-
116
118
  If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
117
119
 
118
120
  ## Using _fastlane_ Plugins
119
-
120
121
  For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
121
122
 
122
123
  ## About _fastlane_
123
-
124
124
  _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
@@ -1,4 +1,10 @@
1
1
  require 'fastlane/action'
2
+ require 'net/http'
3
+ require 'open-uri'
4
+ require 'zip'
5
+ require 'fileutils'
6
+ require 'os'
7
+ require 'json'
2
8
  require_relative '../helper/dynatrace_helper'
3
9
 
4
10
  module Fastlane
@@ -6,7 +12,6 @@ module Fastlane
6
12
  class DynatraceProcessSymbolsAction < Action
7
13
 
8
14
  def self.run(params)
9
- # fastlane will take care of reading in the parameter and fetching the environment variable:
10
15
  UI.message "DTXDssClientPath: #{params[:dtxDssClientPath]}"
11
16
  UI.message "Parameter API Token: #{params[:apitoken]}"
12
17
  UI.message "OS: #{params[:os]}"
@@ -14,243 +19,264 @@ module Fastlane
14
19
  UI.message "Version: #{params[:version]}"
15
20
  UI.message "Server URL: #{params[:server]}"
16
21
 
17
- UI.message("Checking AppFile for possible AppID")
22
+ UI.message "Checking AppFile for possible AppID"
18
23
  bundleId = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
19
- UI.message("Using #{bundleId} from your AppFile")
24
+ if bundleId
25
+ UI.message "Using #{bundleId} from your AppFile"
26
+ else
27
+ bundleId = params[:bundleId]
28
+ UI.message "BundleID: #{bundleId}"
29
+ end
20
30
 
21
- if !(bundleId)
22
- UI.message "BundleID: #{params[:bundleId]}"
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 = nil
44
+ unless response.body.nil?
45
+ message = JSON.parse(response.body)["error"]["message"]
46
+ end
47
+ if message.nil?
48
+ 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/)."
49
+ else
50
+ UI.user_error! "Symbol upload error (Response Code: #{response.code}). #{message}"
51
+ end
52
+ end
53
+ return
54
+ elsif params[:os] != "ios"
55
+ UI.user_error! "Unsopported value os=#{params[:os]}"
23
56
  end
24
57
 
58
+ # iOS workflow
59
+ dtxDssClientPath = Helper::DynatraceHelper.get_dss_client(params)
25
60
 
26
61
  dsym_paths = []
27
- symbolFilesKey = "symbolsfile" #default to iOS
62
+ symbolFilesKey = "symbolsfile" # default to iOS
28
63
 
29
- if (params[:os] == "ios")
30
- begin
31
- if (params[:versionStr])
32
- version = params[:versionStr]
33
- else
34
- version = 'latest'
64
+ if !OS.mac?
65
+ UI.user_error! "A macOS machine is required to process iOS symbols."
66
+ end
67
+
68
+ if params[:downloadDsyms] == true
69
+ UI.message "Downloading dSYMs from App Store Connect"
70
+ startTime = Time.now
71
+
72
+ UI.message "Checking AppFile for possible username/AppleID"
73
+ username = CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
74
+ if username
75
+ UI.message "Using #{username} from your AppFile"
76
+ else
77
+ username = params[:username]
78
+ UI.message "Didn't find a username in AppFile, using passed username parameter: #{params[:username]}"
79
+ end
80
+
81
+ # it takes a couple of minutes until the new build is available through the API
82
+ # -> retry until available
83
+ while params[:waitForDsymProcessing] and # wait is active
84
+ !lane_context[SharedValues::DSYM_PATHS] and # has dsym path
85
+ (Time.now - startTime) < params[:waitForDsymProcessingTimeout] # is in time
86
+
87
+ Actions::DownloadDsymsAction.run(wait_for_dsym_processing: params[:waitForDsymProcessing],
88
+ wait_timeout: (params[:waitForDsymProcessingTimeout] - (Time.now - startTime)).round(0), # remaining timeout
89
+ app_identifier: bundleId,
90
+ username: username,
91
+ version: params[:version],
92
+ build_number: params[:versionStr],
93
+ platform: :ios) # should be optional (Hint: it's not)
94
+
95
+ if !lane_context[SharedValues::DSYM_PATHS] and (Time.now - startTime) < params[:waitForDsymProcessingTimeout]
96
+ 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)."
97
+ sleep(60)
35
98
  end
99
+ end
36
100
 
37
- if (params[:downloadDsyms] == true)
38
- UI.message("Checking AppFile for possible username/AppleID")
39
- username = CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
40
- UI.message("Using #{username} from your AppFile")
41
-
42
- if !(username)
43
- UI.message "Username: #{params[:username]}"
44
- end
45
-
46
- UI.message("Downloading Dsyms from AppStore Connect")
47
- Actions::DownloadDsymsAction.run( wait_for_dsym_processing: true,
48
- wait_timeout: 1800,
49
- app_identifier: bundleId,
50
- username: username,
51
- version: version,
52
- build_number: :versionStr,
53
- )
54
- dsym_paths += Actions.lane_context[SharedValues::DSYM_PATHS] if Actions.lane_context[SharedValues::DSYM_PATHS]
55
-
56
- if dsym_paths.count > 0
57
- UI.message("Downloaded the Dsyms from AppStore Connect. Paths #{dsym_paths}")
58
-
59
- else
60
- raise 'No dsyms found error'
61
- end
62
- end
63
-
64
- rescue
65
- UI.error("Couldn't download Dsyms. Checking if we have a local path")
66
- dsym_paths << params[:symbolsfile] if params[:symbolsfile]
67
- end #end the begin-rescue block
68
-
69
- else #android
70
- dsym_paths << params[:symbolsfile] if params[:symbolsfile]
71
- symbolFilesKey = "file"
72
- end
101
+ if (Time.now - startTime) > params[:waitForDsymProcessingTimeout]
102
+ UI.user_error!("Timeout during dSYM download. Try increasing :waitForDsymProcessingTimeout.")
103
+ end
73
104
 
74
- #check if we have dsyms to proceed with
75
- if (dsym_paths.count == 0)
76
- UI.message "Symbol file path: #{params[:symbolsfile]}" #Ask the user for the symbolFiles path
77
- dsym_paths = params[:symbolsfile]
78
- symbolFilesCommandSnippet = "#{symbolFilesKey}=\"#{dsym_paths}\""
79
- else
80
- symbolFilesCommandSnippet = "#{symbolFilesKey}=\"#{dsym_paths[0]}\""
81
- end
105
+ dsym_paths += Actions.lane_context[SharedValues::DSYM_PATHS] if Actions.lane_context[SharedValues::DSYM_PATHS]
106
+
107
+ if dsym_paths.count > 0
108
+ UI.message "Downloaded the dSYMs from App Store Connect. Paths: #{dsym_paths}"
109
+ else
110
+ raise 'No dSYM paths found!'
111
+ end
112
+ else
113
+ UI.important "dSYM download disabled, using local path (#{params[:symbolsfile]})"
114
+ dsym_paths << params[:symbolsfile] if params[:symbolsfile]
115
+ end
82
116
 
83
- #Start constructing the command that will trigger the DTXDssClient
117
+ # check if we have dSYMs to proceed with
118
+ if dsym_paths.count == 0
119
+ UI.message "Symbol file path: #{params[:symbolsfile]}"
120
+ dsym_paths = params[:symbolsfile]
121
+ symbolFilesCommandSnippet = "#{symbolFilesKey}=\"#{dsym_paths}\""
122
+ else
123
+ symbolFilesCommandSnippet = "#{symbolFilesKey}=\"#{dsym_paths[0]}\""
124
+ end
125
+
126
+ # start constructing the command that will trigger the DTXDssClient
84
127
  command = []
85
- command << "#{params[:dtxDssClientPath]}"
128
+ command << "#{dtxDssClientPath}"
86
129
  command << "-#{params[:action]}" #"-upload"
87
130
  command << "appid=\"#{params[:appId]}\""
88
131
  command << "apitoken=\"#{params[:apitoken]}\""
89
132
  command << "os=#{params[:os]}"
90
133
  command << "bundleId=\"#{bundleId}\""
91
- if params[:os] == "ios"
92
- command << "versionStr=\"#{version}\""
93
- else
94
- command << "versionStr=\"#{params[:versionStr]}\""
95
- end
134
+ command << "versionStr=\"#{params[:versionStr]}\""
96
135
  command << "version=\"#{params[:version]}\""
97
136
  command << symbolFilesCommandSnippet
98
- command << "server=\"#{params[:server]}\""
137
+ command << "server=\"#{Helper::DynatraceHelper.without_trailing_slash(params[:server])}\""
99
138
  command << "DTXLogLevel=ALL -verbose" if params[:debugMode] == true
100
- command << "forced=1" #So that we do not waste time with errors if the file already exists
139
+ command << "forced=1" # if the file already exists
101
140
 
102
141
  # Create the full shell command to trigger the DTXDssClient
103
142
  shell_command = command.join(' ')
104
143
 
144
+ UI.message "dSYM path: #{dsym_paths[0]}"
145
+ UI.message "#{shell_command}"
105
146
 
106
- UI.message("Dsym paths: #{dsym_paths[0]}")
107
- UI.message("#{shell_command}")
108
-
109
- # Execute the shell command
110
- sh "#{shell_command}"
147
+ sh("#{shell_command}", error_callback: ->(result) {
148
+ # ShAction doesn't return any reference to the return value -> parse it from the output
149
+ result_groups = result.match /(?:ERROR: Execution failed, rc=)(-?\d*)(?:\sreason=)(.*)/
150
+ if result_groups and result_groups.length() >= 2
151
+ if result_groups[1] == "413"
152
+ UI.user_error!("DTXDssClient: #{result_groups[2]} See https://www.dynatrace.com/support/help/shortlink/mobile-symbolication#manage-the-uploaded-symbol-files for more information.")
153
+ else
154
+ UI.user_error!("DTXDssClient: #{result_groups[2]}")
155
+ end
156
+ else
157
+ UI.user_error!("DTXDssClient finished with errors.")
158
+ end
159
+ })
111
160
 
112
- UI.message("Cleaning build artifacts")
161
+ UI.message "Cleaning build artifacts"
113
162
  Fastlane::Actions::CleanBuildArtifactsAction.run(exclude_pattern: nil)
114
-
115
- end #end the run functions
163
+ end
116
164
 
117
165
  def self.description
118
- "This action processes and uploads your symbol files to Dynatrace"
166
+ "This action processes and uploads your symbol files to Dynatrace."
119
167
  end
120
168
 
121
169
  def self.details
122
- "This action allows you to process and upload symbol files to Dynatrace. You can also use it to first download your latest dSym files from AppStore Connect if you use Bitcode"
170
+ "This action allows you to process and upload symbol files to Dynatrace. If you use Bitcode you can also use it to download the latest dSYM files from App Store Connect."
123
171
  end
124
172
 
125
173
  def self.available_options
126
- # Define all options your action supports.
127
174
  [
128
175
  FastlaneCore::ConfigItem.new(key: :action,
129
176
  env_name: "FL_UPLOAD_TO_DYNATRACE_ACTION",
130
- description: "The action you need to perform. For example upload/decode",
177
+ description: "(iOS only) Action to be performed by DTXDssClient (\"upload\" or \"decode\")",
131
178
  default_value: "upload",
132
179
  is_string: true,
133
180
  verify_block: proc do |value|
134
- UI.user_error!("Action needs to either be upload or decode") unless (value and value == "upload" or value == "decode")
135
- # UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value)
181
+ UI.user_error!("Action needs to either be \"upload\" or \"decode\"") unless (value and value == "upload" or value == "decode")
136
182
  end),
137
183
 
138
184
  FastlaneCore::ConfigItem.new(key: :downloadDsyms,
139
- env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS", # The name of the environment variable
185
+ env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS",
140
186
  default_value: false,
141
187
  is_string: false,
142
- description: "Boolean variable that enables downloading the Dsyms from AppStore Connect (iOS only)", # a short description of this parameter
143
- ),
188
+ description: "(iOS only) Download the dSYMs from App Store Connect"),
189
+
190
+ FastlaneCore::ConfigItem.new(key: :waitForDsymProcessing,
191
+ env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS_WAIT_PROCESSING",
192
+ default_value: true,
193
+ is_string: false,
194
+ description: "(iOS only) Wait for dSYM processing to be finished"),
144
195
 
145
- FastlaneCore::ConfigItem.new(key: :dsym_waiting_timeout,
146
- env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS_WAIT_TIMEOUT", # The name of the environment variable
147
- default_value: 900,
196
+ FastlaneCore::ConfigItem.new(key: :waitForDsymProcessingTimeout,
197
+ env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS_WAIT_TIMEOUT",
198
+ default_value: 1800,
148
199
  is_string: false,
149
- description: "The timeout in milliseconds to wait for processing of dSYMs", # a short description of this parameter
150
- ),
200
+ description: "(iOS only) Timeout in seconds to wait for the dSYMs be downloadable"),
151
201
 
152
202
  FastlaneCore::ConfigItem.new(key: :username,
153
- env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS_USERNAME", # The name of the environment variable
154
- description: "The username or the AppleID to use to download the Dsyms", # a short description of this parameter
155
- ),
156
-
157
- FastlaneCore::ConfigItem.new(key: :os,
158
- env_name: "FL_UPLOAD_TO_DYNATRACE_OS", # The name of the environment variable
159
- description: "The OperatingSystem of the symbol files. Either \"ios\" or \"android\"",
160
- sensitive: false,
161
- optional: false,
162
- verify_block: proc do |value|
163
- UI.user_error!("Please specify the OperatingSystem of the symbol files. Possible values are \"ios\" or \"android\"") unless (value and not value.empty? and (value == "ios" || value =="android"))
164
- end),
203
+ env_name: "FL_UPLOAD_TO_DYNATRACE_DOWNLOAD_DSYMS_USERNAME",
204
+ description: "(iOS only) The username/AppleID to use to download the dSYMs"),
205
+
206
+ FastlaneCore::ConfigItem.new(key: :os,
207
+ env_name: "FL_UPLOAD_TO_DYNATRACE_OS",
208
+ description: "The type of the symbol files, either \"ios\" or \"android\"",
209
+ sensitive: false,
210
+ optional: false,
211
+ verify_block: proc do |value|
212
+ UI.user_error!("Please specify the type of the symbol files. Possible values are \"ios\" or \"android\".") unless (value and not value.empty? and (value == "ios" || value =="android"))
213
+ end),
165
214
 
166
215
  FastlaneCore::ConfigItem.new(key: :apitoken,
167
- env_name: "FL_UPLOAD_TO_DYNATRACE_apitoken", # The name of the environment variable
168
- description: "The Dynatrace API token", # a short description of this parameter
216
+ env_name: "FL_UPLOAD_TO_DYNATRACE_apitoken",
217
+ description: "Dynatrace API token with mobile symbolication permissions",
169
218
  verify_block: proc do |value|
170
- UI.user_error!("No API token for UploadToDynatraceAction given, pass using `apitoken: 'token'`") unless (value and not value.empty?)
171
- # UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value)
219
+ UI.user_error!("No Dynatrade API token for specified, pass using `apitoken: 'token'`") unless (value and not value.empty?)
172
220
  end),
173
221
 
174
222
  FastlaneCore::ConfigItem.new(key: :dtxDssClientPath,
175
223
  env_name: "FL_UPLOAD_TO_DYNATRACE_DTXDssClientPath",
176
- description: "The path to your DTXDssClient",
177
- default_value: "./DTXDssClient",
224
+ description: "(DEPRECATED) The path to your DTXDssClient. The DTXDssClient is downloaded and updated automatically, unless this key is set",
225
+ optional: true),
226
+
227
+ FastlaneCore::ConfigItem.new(key: :appId,
228
+ env_name: "FL_UPLOAD_TO_DYNATRACE_APP_ID",
229
+ description: "The app ID you get from your Dynatrace environment",
178
230
  verify_block: proc do |value|
179
- UI.user_error!("We need the path to the DTXDssClient in your iOS agent folder. For example . Pass using `dtxDssClientPath: 'path'`") unless (value and not value.empty?)
180
- # is_string: true # true: verifies the input is a string, false: every kind of value
181
- # default_value: false) # the default value if the user didn't provide one
182
- end),
183
-
184
- FastlaneCore::ConfigItem.new(key: :appId,
185
- env_name: "FL_UPLOAD_TO_DYNATRACE_APP_ID",
186
- description: "The app ID you get from your Dynatrace WebUI",
187
- verify_block: proc do |value|
188
- UI.user_error!("Please provide the appID for your app. Pass using `appId: 'appId'`") unless (value and not value.empty?)
189
- # is_string: true # true: verifies the input is a string, false: every kind of value
190
- # default_value: false) # the default value if the user didn't provide one
191
- end),
231
+ UI.user_error!("Please provide the appID for your application. Pass using `appId: 'appId'`") unless (value and not value.empty?)
232
+ end),
192
233
 
193
234
  FastlaneCore::ConfigItem.new(key: :bundleId,
194
235
  env_name: "FL_UPLOAD_TO_DYNATRACE_BUNDLE_ID",
195
- description: "The CFBundlebundleId (iOS) / package (Android) of the Application. Usually in reverse com notation. Ex. com.your_company.your_app",
236
+ description: "The CFBundlebundleId (iOS) / package (Android) of the application",
196
237
  verify_block: proc do |value|
197
238
  UI.user_error!("Please provide the BundleID for your app. Pass using `bundleId: 'bundleId'`") unless (value and not value.empty?)
198
- # is_string: true # true: verifies the input is a string, false: every kind of value
199
- # default_value: false) # the default value if the user didn't provide one
200
- end),
239
+ end),
201
240
 
202
241
  FastlaneCore::ConfigItem.new(key: :versionStr,
203
242
  env_name: "FL_UPLOAD_TO_DYNATRACE_VERSION_STRING",
204
243
  description: "The CFBundleShortVersionString (iOS) / versionName (Android)",
205
244
  verify_block: proc do |value|
206
245
  UI.user_error!("Please provide the CFBundleShortVersionString for your app. Pass using `versionStr: 'versionStr'`") unless (value and not value.empty?)
207
- # is_string: true # true: verifies the input is a string, false: every kind of value
208
- # default_value: false) # the default value if the user didn't provide one
209
- end),
210
-
211
- FastlaneCore::ConfigItem.new(key: :version,
212
- env_name: "FL_UPLOAD_TO_DYNATRACE_VERSION",
213
- description: "The CFBundleVersion (iOS) / versionCode (Android)",
214
- verify_block: proc do |value|
215
- UI.user_error!("Please provide the version for your app. Pass using `version: 'version'`") unless (value and not value.empty?)
216
- # is_string: true # true: verifies the input is a string, false: every kind of value
217
- # default_value: false) # the default value if the user didn't provide one
218
- end),
246
+ end),
247
+
248
+ FastlaneCore::ConfigItem.new(key: :version,
249
+ env_name: "FL_UPLOAD_TO_DYNATRACE_VERSION",
250
+ description: "The CFBundleVersion (iOS) / versionCode (Android). Is also used for the dSYM download",
251
+ verify_block: proc do |value|
252
+ UI.user_error!("Please provide the version for your app. Pass using `version: 'version'`") unless (value and not value.empty?)
253
+ end),
219
254
 
220
255
  FastlaneCore::ConfigItem.new(key: :symbolsfile,
221
256
  env_name: "FL_UPLOAD_TO_DYNATRACE_SYM_FILE_PATH",
222
- description: "The filename/path of the XCode iOS archive or iOS dSYM containing the symbol mappings",
257
+ description: "Path to the dSYM file to be processed. Is only used when downloadDsyms is not set",
258
+ verify_block: proc do |value|
259
+ UI.user_error!("Please provide a value for the symbol files. Pass using `symbolsfile: 'symbolsfile'`") unless (value and not value.empty?)
260
+ end),
261
+
262
+ FastlaneCore::ConfigItem.new(key: :server,
263
+ env_name: "FL_UPLOAD_TO_DYNATRACE_SERVER_URL",
264
+ description: "The API endpoint for the Dynatrace environment (e.g. https://environmentID.live.dynatrace.com or https://dynatrace-managed.com/e/environmentID)",
223
265
  verify_block: proc do |value|
224
- UI.user_error!("Please provide a value for the symbolFiles. Pass using `symbolsfile: 'symbolsfile'`") unless (value and not value.empty?)
225
- # is_string: true # true: verifies the input is a string, false: every kind of value
226
- # default_value: false) # the default value if the user didn't provide one
227
- end),
228
-
229
- FastlaneCore::ConfigItem.new(key: :server,
230
- env_name: "FL_UPLOAD_TO_DYNATRACE_SERVER_URL",
231
- description: "The API endpoint for the Dynatrace environment. For example https://<environmentID.live.dynatrace.com/api/config/v1",
232
- verify_block: proc do |value|
233
- UI.user_error!("Please provide your environment API endpoint. Pass using `server: 'server'`") unless (value and not value.empty?)
234
- # is_string: true # true: verifies the input is a string, false: every kind of value
235
- # default_value: false) # the default value if the user didn't provide one
236
- end),
266
+ UI.user_error!("Please provide your environment API endpoint. Pass using `server: 'server'`") unless (value and not value.empty?)
267
+ end),
237
268
 
238
269
  FastlaneCore::ConfigItem.new(key: :debugMode,
239
270
  env_name: "FL_UPLOAD_TO_DYNATRACE_DEBUG_MODE",
240
- description: "Debug logging enabled",
271
+ description: "Enable debug logging",
272
+ default_value: false,
241
273
  is_string: false,
242
- optional: true
243
- )
274
+ optional: true)
244
275
  ]
245
276
  end
246
277
 
247
- def self.return_value
248
- # If your method provides a return value, you can describe here what it does
249
- end
250
-
251
278
  def self.authors
252
- # So no one will ever forget your contribution to fastlane :) You are awesome btw!
253
- ["MANassar/@MohamedANassar"]
279
+ ["MANassar/@MohamedANassar", "cynicer"]
254
280
  end
255
281
 
256
282
  def self.is_supported?(platform)
@@ -1,15 +1,188 @@
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")
5
9
 
6
10
  module Helper
7
11
  class DynatraceHelper
8
- # class methods that you define here become available in your action
9
- # as `Helper::DynatraceHelper.your_method`
10
- #
11
- def self.show_message
12
- UI.message("Hello from the dynatrace plugin helper!")
12
+ def self.get_dss_client(params)
13
+ dynatraceDir = "dynatrace"
14
+ dtxDssClientBin = "DTXDssClient"
15
+ versionFilePath = "#{dynatraceDir}/version"
16
+ dtxDssClientPath = "#{dynatraceDir}/#{dtxDssClientBin}"
17
+
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
22
+
23
+ # get latest version info
24
+ clientUri = URI("#{self.without_trailing_slash(params[:server])}/api/config/v1/symfiles/dtxdss-download?Api-Token=#{params[:apitoken]}")
25
+ response = Net::HTTP.get_response(clientUri)
26
+
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
73
+ end
74
+
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
90
+ end
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
111
+ end
112
+ end
113
+ return dtxDssClientPath
114
+ end
115
+
116
+ def self.without_trailing_slash(server)
117
+ if server[-1] == '/'
118
+ return server[0..-2]
119
+ else
120
+ return server
121
+ end
122
+ end
123
+
124
+ def self.get_host_name(params)
125
+ uri = URI.split(params[:server])
126
+
127
+ unless uri[2].nil?
128
+ return uri[2]
129
+ end
130
+
131
+ # no procotol prefix -> host name is with path
132
+ if uri[5][-1] == '/'
133
+ return uri[5][0..-2] # remove trailing /
134
+ else
135
+ return uri[5]
136
+ end
137
+ end
138
+
139
+ def self.put_android_symbols(params, bundleId)
140
+ path = "/api/config/v1/symfiles/#{params[:appId]}/#{bundleId}/ANDROID/#{params[:version]}/#{params[:versionStr]}"
141
+
142
+ # if path points to dynatrace managed, we need to prepend the path component from the server (/e/{your-environment-id})
143
+ if params[:server].include? '/e/'
144
+ uri = URI.split(params[:server])
145
+ if uri[5].nil?
146
+ UI.user_error! "The path component of your managed Dynatrace URL is empty. Does the server URL follow the correct pattern (https://{your-domain}/e/{your-environment-id})?"
147
+ end
148
+ path = self.without_trailing_slash(uri[5]) + path
149
+ end
150
+
151
+
152
+ req = Net::HTTP::Put.new(path, initheader = { 'Content-Type' => 'text/plain',
153
+ 'Authorization' => "Api-Token #{params[:apitoken]}"} )
154
+
155
+ req.body = IO.read(params[:symbolsfile])
156
+ http = Net::HTTP.new(self.get_host_name(params), 443)
157
+ http.use_ssl = true
158
+ http.request(req)
159
+ end
160
+
161
+ private
162
+ def self.check_fallback_or_raise(fallback_client, error)
163
+ 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/."
164
+ UI.important error
165
+ if File.exists?(fallback_client) and File.size(fallback_client) > 0
166
+ UI.important "Using cached client: #{fallback_client}"
167
+ else
168
+ UI.important "No cached fallback found."
169
+ raise error
170
+ end
171
+ end
172
+
173
+ # assumes the token parameter is appended last (there is only one parameter anyway)
174
+ def self.to_redacted_api_token_string(url)
175
+ urlStr = url.to_s
176
+ str = "Api-Token="
177
+ idx = urlStr.index(str)
178
+ token_len = urlStr.length - idx - str.length
179
+ urlStr[idx + str.length..idx + str.length + token_len] = "-" * token_len
180
+ return urlStr
181
+ end
182
+
183
+ # for test mocking
184
+ def self.save_to_tempfile(url)
185
+ open(url)
13
186
  end
14
187
  end
15
188
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Dynatrace
3
- VERSION = "0.1.5"
3
+ VERSION = "1.0.3"
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: 0.1.5
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
- - Mohamed Nassar
8
- autorequire:
7
+ - Dynatrace LLC
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-04 00:00:00.000000000 Z
11
+ date: 2021-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -136,8 +136,8 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: 2.142.0
139
- description:
140
- email: mohamed.nassar@dynatrace.com
139
+ description:
140
+ email: mobile.agent@dynatrace.com
141
141
  executables: []
142
142
  extensions: []
143
143
  extra_rdoc_files: []
@@ -152,7 +152,7 @@ homepage: https://github.com/Dynatrace/fastlane-plugin-dynatrace
152
152
  licenses:
153
153
  - MIT
154
154
  metadata: {}
155
- post_install_message:
155
+ post_install_message:
156
156
  rdoc_options: []
157
157
  require_paths:
158
158
  - lib
@@ -167,8 +167,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
167
  - !ruby/object:Gem::Version
168
168
  version: '0'
169
169
  requirements: []
170
- rubygems_version: 3.0.1
171
- signing_key:
170
+ rubygems_version: 3.1.4
171
+ signing_key:
172
172
  specification_version: 4
173
173
  summary: This action processes and uploads your symbol files to Dynatrace
174
174
  test_files: []