fastlane-plugin-bugsee 1.0.2 → 1.1.0

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.
data/README.md CHANGED
@@ -12,8 +12,7 @@ fastlane add_plugin bugsee
12
12
 
13
13
  ## About bugsee
14
14
 
15
- Bugsee is free crash and bug reporting with video, network and logs. Sign up for a service at [https://www.bugsee.com](https://www.bugsee.com). This plugin implements fastlane action to upload debug
16
- symbol (dSYM) files to Bugsee servers.
15
+ Bugsee is free crash and bug reporting with video, network and logs. Sign up for a service at [https://www.bugsee.com](https://www.bugsee.com). This plugin implements a fastlane action that uploads debug symbol (dSYM) files to Bugsee, and — when invoked from an Xcode build phase — also collects and registers the project's dependency graph.
17
16
 
18
17
  ## Usage
19
18
 
@@ -42,6 +41,107 @@ lane :refresh_dsyms do
42
41
  end
43
42
  ```
44
43
 
44
+ ## How symbol upload works
45
+
46
+ Starting with `1.1.0`, symbol upload shells out to the [bugsee-cli](https://github.com/bugsee/bugsee-cli) Rust binary — the same uploader the Bugsee Android Gradle plugin uses for ProGuard/R8 mappings. One mechanism, one wire format across both platforms.
47
+
48
+ On first use the CLI is downloaded from `https://download.bugsee.com/cli`, SHA-256 verified against the published sidecar, and cached at `~/.bugsee/cli/<version>/<host-triple>/`. Subsequent runs hit the cache — no per-build network round-trip past the dSYM upload itself.
49
+
50
+ Override the auto-download with environment variables:
51
+
52
+ | Variable | Purpose |
53
+ | --- | --- |
54
+ | `BUGSEE_CLI_PATH` | Path to a local `bugsee-cli` binary. Useful when developing the CLI itself or in air-gapped CI. |
55
+ | `BUGSEE_CLI_VERSION` | Pin or test a specific CLI release; defaults to the version bundled with this plugin release. |
56
+
57
+ Each `.dSYM` is uploaded as its own request, so the dashboard surfaces a per-framework symbol record. If a host architecture isn't supported by the published CLI (currently macOS / Linux / Windows on x86_64 + arm64, where each platform exists), the agent logs and skips — it does not fail the build.
58
+
59
+ ## Dependency collection
60
+
61
+ When `BugseeAgent` runs from an Xcode build phase (where `SRCROOT` / `INFOPLIST_PATH` are set), it also scans the project for dependency lockfiles and registers the resolved graph with the build:
62
+
63
+ - **CocoaPods** — `Podfile.lock` (provides direct/transitive distinction and parent edges).
64
+ - **Swift Package Manager** — `Package.resolved` (both Xcode-managed and SPM CLI v2 formats).
65
+ - **Carthage** — `Cartfile.resolved`.
66
+
67
+ The emitted blob is wire-compatible with the Bugsee Android Gradle plugin's `DependencyCollector`, so iOS and Android deps render identically in the dashboard.
68
+
69
+ No configuration is required — if a lockfile exists, it's collected.
70
+
71
+ ## Android mapping (ProGuard / R8) upload
72
+
73
+ Starting with `1.1.0`, this plugin also supports uploading Android `mapping.txt` files via a separate `upload_mapping_to_bugsee` action. The canonical path remains the [Bugsee Android Gradle plugin](https://github.com/bugsee/bugsee-android-gradle-plugin), which does the upload automatically as part of every gradle build. Reach for this fastlane action only when one of the following applies:
74
+
75
+ - **Your CI splits build (no token) from publish (production token).** The Gradle plugin builds the APK on machine A without uploading; this action uploads the mapping on machine B with the production token. The action looks for the Gradle plugin's `build-uuid.txt` (under `**/build/intermediates/bugsee/*/build-uuid.txt`) so the UUID matches what the SDK already has baked into the APK.
76
+ - **The host app is not instrumented by the Bugsee Gradle plugin** — *and* uses Bugsee Android SDK 7.0.0-beta13+. The action synthesises a UUID Ruby-side from `(app_token, version, build)`; the SDK reproduces the same UUID at runtime via its third BUILD_UUID fallback (added in 7.0.0-beta13), so symbolication still works.
77
+
78
+ If neither applies — i.e. you're using the Gradle plugin in the standard way — there's nothing to do; the Gradle plugin handles the upload as part of the existing `gradle` action.
79
+
80
+ Example invocation:
81
+
82
+ ```ruby
83
+ upload_mapping_to_bugsee(
84
+ app_token: ENV["BUGSEE_APP_TOKEN"],
85
+ mapping_path: "./app/build/outputs/mapping/release/mapping.txt",
86
+ version: "1.2.3", # android:versionName
87
+ build: "42", # android:versionCode
88
+ # Optional:
89
+ # uuid: "...", # explicit override
90
+ # build_uuid_path: "app/build/intermediates/bugsee/release/build-uuid.txt",
91
+ # icon_path: "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
92
+ )
93
+ ```
94
+
95
+ **UUID resolution chain** (most authoritative to fallback):
96
+
97
+ 1. Explicit `:uuid` if passed.
98
+ 2. `:build_uuid_path` if given and the file exists.
99
+ 3. The Bugsee Gradle plugin's `build-uuid.txt`, auto-globbed under `**/build/intermediates/bugsee/*/build-uuid.txt`.
100
+ 4. Ruby-side synthesis: `nameUUIDFromBytes(app_token + 0x1F + version + 0x1F + build)` — matches the SDK's Channel 3 runtime fallback (7.0.0-beta13+).
101
+
102
+ All four branches produce a UUID the SDK can independently reproduce, so server-side mapping lookup resolves crashes correctly regardless of which branch fired.
103
+
104
+ The action `is_supported?(:android)` only.
105
+
106
+ ## iOS artefact upload (size analysis)
107
+
108
+ This plugin also exposes an `upload_artifact_to_bugsee` action that packages a built `.app` into a byte-deterministic synthetic `.ipa` and uploads it to Bugsee for size analysis. The canonical path remains the [Bugsee iOS SDK](https://github.com/bugsee/bugsee-ios)'s `tools.bundle/BugseeAgent` build phase, which runs size analysis automatically when `BUGSEE_BUILD_INFO_ENABLED` is on (default). Reach for this fastlane action only when one of the following applies:
109
+
110
+ - **Your CI doesn't integrate the SDK's build phase.** Binary-only CI, alternative build systems (Bazel, Tuist with custom phases), or pipelines that pre-build the `.app` and only invoke fastlane for publish.
111
+ - **Your CI splits build (no token) from publish (production token).** The build machine produces the `.app` or `.xcarchive`; the publish machine runs this action with the production token.
112
+ - **You want the size-trend chart without shipping bytes.** Pass `build_info_only: true` to record `artifact_size` on the server (so the dashboard's size-trend chart works) without uploading the `.ipa` bytes themselves. Useful for firewalled CI and privacy-sensitive setups.
113
+
114
+ If the SDK's build phase already runs in the same Xcode build, the action's cross-producer handshake skips by default (the SDK writes a manifest at `build/bugsee/build-actions.json`; the action reads it and short-circuits when `artifact_upload: true`). Pass `force: true` to override.
115
+
116
+ Example invocation:
117
+
118
+ ```ruby
119
+ upload_artifact_to_bugsee(
120
+ app_token: ENV["BUGSEE_APP_TOKEN"],
121
+ xcarchive_path: ENV["XCARCHIVE_PATH"],
122
+ # OR pass the .app directly:
123
+ # app_path: "build/Products/Release-iphoneos/MyApp.app",
124
+ # Optional:
125
+ # version: "1.2.3", # CFBundleShortVersionString (else read from Info.plist)
126
+ # build: "42", # CFBundleVersion (else read from Info.plist)
127
+ # build_info_only: true, # record size, skip the .ipa bytes upload
128
+ # force: true, # override the cross-producer handshake skip
129
+ )
130
+ ```
131
+
132
+ **App path resolution:**
133
+
134
+ 1. Explicit `:app_path` if passed.
135
+ 2. `:xcarchive_path` → the single `.app` under `<archive>/Products/Applications/`. Multiple `.app` siblings raise a `user_error` so the user can disambiguate.
136
+
137
+ **Env var aliases** (all are also exposed via fastlane's `available_options`):
138
+
139
+ - `BUGSEE_APP_TOKEN`, `BUGSEE_APP_PATH`, `BUGSEE_XCARCHIVE_PATH`
140
+ - `BUGSEE_APP_VERSION`, `BUGSEE_APP_BUILD`
141
+ - `BUGSEE_BUILD_INFO_ONLY`, `BUGSEE_FORCE`
142
+
143
+ The action `is_supported?(:ios)` only.
144
+
45
145
  ## Documentation
46
146
 
47
147
  Further documentation about Bugsee crash symbolication is available at https://docs.bugsee.com
@@ -0,0 +1,230 @@
1
+ require 'fastlane/plugin/bugsee/helper/bugsee_handshake'
2
+
3
+ module Fastlane
4
+ module Actions
5
+ # Upload an iOS build artefact (`.app`) to Bugsee for size analysis.
6
+ #
7
+ # When and why to use this:
8
+ #
9
+ # The Bugsee iOS SDK's `tools.bundle/BugseeAgent` already runs
10
+ # size analysis as a post-action build phase when
11
+ # `BUGSEE_BUILD_INFO_ENABLED` is on (default). The most common
12
+ # scenario for THIS fastlane action is CI pipelines that don't
13
+ # integrate the SDK's build phase (binary-only CI, alternative
14
+ # build systems) or that explicitly split build (machine A) from
15
+ # publish (machine B, has the production token).
16
+ #
17
+ # What it does:
18
+ #
19
+ # 1. Locates the `.app` — explicit `:app_path`, OR resolved from
20
+ # `:xcarchive_path` via `Products/Applications/<App>.app`.
21
+ # 2. Shells to `BugseeAgent --upload-artifact` which:
22
+ # - Packages the `.app` into a synthetic byte-deterministic
23
+ # `.ipa` (same posture as the SDK side and the Android
24
+ # Gradle plugin so the back-end's content-hash dedup works
25
+ # across producers).
26
+ # - POSTs build registration to /v2/apps/<token>/builds with
27
+ # `request_artifact_upload: true`, getting back a presigned
28
+ # S3 PUT URL.
29
+ # - PUTs the .ipa bytes to that URL.
30
+ #
31
+ # Cross-producer handshake: if the Bugsee iOS SDK's BugseeAgent
32
+ # build phase already uploaded this build's artefact, skip — same
33
+ # gating shape as upload_symbols_to_bugsee and
34
+ # upload_mapping_to_bugsee.
35
+ class UploadArtifactToBugseeAction < Action
36
+ BUGSEE_AGENT_PATH = File.expand_path(
37
+ File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'BugseeAgent'))
38
+
39
+ def self.run(params)
40
+ app_token = params[:app_token]
41
+ host = params[:host] || "https://api.bugsee.com"
42
+ version = params[:version]
43
+ build = params[:build]
44
+ agent_path = params[:agent_path] || BUGSEE_AGENT_PATH
45
+
46
+ UI.user_error!("Please provide an app token via app_token:") unless app_token
47
+
48
+ agent_path = File.expand_path(agent_path)
49
+ UI.user_error!("BugseeAgent helper script is missing: #{agent_path}") unless File.exist?(agent_path)
50
+
51
+ # ──────────────────────────────────────────────────────
52
+ # Resolve the `.app` path. Priority:
53
+ # 1. Explicit :app_path
54
+ # 2. :xcarchive_path → Products/Applications/<single .app>
55
+ # ──────────────────────────────────────────────────────
56
+ app_path = resolve_app_path(params)
57
+ UI.user_error!(
58
+ "Please provide either :app_path or :xcarchive_path (with a single .app inside Products/Applications/)"
59
+ ) unless app_path
60
+ UI.user_error!("App path does not exist: #{app_path}") unless File.directory?(app_path)
61
+ UI.user_error!(
62
+ "App path must end in .app: #{app_path}"
63
+ ) unless app_path.end_with?('.app')
64
+
65
+ # ──────────────────────────────────────────────────────
66
+ # Cross-producer handshake
67
+ # ──────────────────────────────────────────────────────
68
+ # `artifact_upload` is the manifest action name the SDK
69
+ # BugseeAgent writes when it successfully shipped the IPA.
70
+ unless params[:force]
71
+ manifest = Fastlane::Bugsee::Handshake.find_manifest(
72
+ search_root: Dir.pwd,
73
+ version_name: version,
74
+ version_code: build,
75
+ )
76
+ if Fastlane::Bugsee::Handshake.handled_by_other?(manifest, 'artifact_upload')
77
+ UI.important(Fastlane::Bugsee::Handshake.skip_message(manifest, 'artifact_upload'))
78
+ return
79
+ end
80
+ end
81
+
82
+ UI.message("Bugsee: uploading artefact for #{File.basename(app_path)}")
83
+
84
+ # Shell command shape:
85
+ # python3 BugseeAgent -x \
86
+ # -e <host> [-v <ver>] [-b <build>] \
87
+ # --upload-artifact \
88
+ # --app-path <path> \
89
+ # <app_token>
90
+ cmd = []
91
+ cmd << agent_path.shellescape
92
+ cmd << "-x" # not run from Xcode — synchronous, no daemonize
93
+ cmd << "-e #{host.shellescape}"
94
+ cmd << "-v #{version.to_s.shellescape}" if version && !version.to_s.empty?
95
+ cmd << "-b #{build.to_s.shellescape}" if build && !build.to_s.empty?
96
+ cmd << "--upload-artifact"
97
+ cmd << "--app-path #{app_path.shellescape}"
98
+ # When build_info_only is true, skip the .ipa bytes upload —
99
+ # the registration POST still records artifact_size on the
100
+ # server so the dashboard's size-trend chart works, but the
101
+ # bytes never leave the build host. Useful for firewalled CI
102
+ # and privacy-sensitive setups.
103
+ cmd << "--build-info-only" if params[:build_info_only]
104
+ cmd << app_token.shellescape
105
+
106
+ begin
107
+ Actions.sh(cmd.join(" "), log: false)
108
+ rescue => e
109
+ # Upload failure should NOT take the lane down — size
110
+ # analysis is a release-supporting feature, same posture as
111
+ # the other Bugsee fastlane actions.
112
+ UI.error(e.to_s)
113
+ end
114
+ end
115
+
116
+ # @api private
117
+ # Resolves the `.app` path the user wants packaged. Public-ish
118
+ # so RSpec can exercise each branch in isolation.
119
+ def self.resolve_app_path(params)
120
+ explicit = params[:app_path]
121
+ return explicit if explicit && !explicit.to_s.empty?
122
+
123
+ archive_path = params[:xcarchive_path]
124
+ return nil unless archive_path && !archive_path.to_s.empty?
125
+
126
+ # An .xcarchive bundles the built `.app` at
127
+ # `<archive>/Products/Applications/<App>.app`. Usually exactly
128
+ # one `.app` is present; if multiple are present (extensions
129
+ # are bundled inside the main .app's Frameworks/ subtree, not
130
+ # a sibling at Products/Applications), we pick the only entry
131
+ # and error if ambiguous so the user can disambiguate via
132
+ # `:app_path`.
133
+ apps_dir = File.join(archive_path, 'Products', 'Applications')
134
+ return nil unless File.directory?(apps_dir)
135
+
136
+ matches = Dir.entries(apps_dir).select do |e|
137
+ e.end_with?('.app') && File.directory?(File.join(apps_dir, e))
138
+ end
139
+ if matches.length == 1
140
+ return File.join(apps_dir, matches.first)
141
+ elsif matches.length > 1
142
+ UI.user_error!(
143
+ "Multiple .app bundles found under #{apps_dir}; pass :app_path to disambiguate. Found: #{matches.inspect}"
144
+ )
145
+ end
146
+ nil
147
+ end
148
+
149
+ def self.description
150
+ "Upload an iOS build artefact (.app → synthetic .ipa) to Bugsee for size analysis."
151
+ end
152
+
153
+ def self.details
154
+ <<~DETAILS
155
+ Packages a built `.app` into a byte-deterministic synthetic `.ipa`
156
+ and uploads it to Bugsee for size analysis. The Bugsee iOS SDK's
157
+ tools.bundle/BugseeAgent build phase already does this when
158
+ BUGSEE_BUILD_INFO_ENABLED is on (default); this action is for CI
159
+ pipelines that don't integrate that build phase, or for
160
+ split-build/publish setups where build and upload run on
161
+ different machines.
162
+
163
+ App path resolution: explicit :app_path > :xcarchive_path with a
164
+ single .app inside Products/Applications/.
165
+
166
+ Cross-producer handshake: if the SDK's BugseeAgent build phase
167
+ already uploaded this build's artefact (recorded in the
168
+ build-actions.json manifest), this action skips by default. Pass
169
+ force: true to override.
170
+ DETAILS
171
+ end
172
+
173
+ def self.available_options
174
+ [
175
+ FastlaneCore::ConfigItem.new(key: :agent_path,
176
+ env_name: "BUGSEE_AGENT_PATH",
177
+ description: "The path to the BugseeAgent helper script",
178
+ optional: true,
179
+ verify_block: proc do |value|
180
+ UI.user_error!("Couldn't find BugseeAgent at path '#{value}'") unless File.exist?(value)
181
+ end),
182
+ FastlaneCore::ConfigItem.new(key: :host,
183
+ env_name: "BUGSEE_API_HOST",
184
+ description: "The path to API endpoint",
185
+ optional: true),
186
+ FastlaneCore::ConfigItem.new(key: :app_token,
187
+ env_name: "BUGSEE_APP_TOKEN",
188
+ description: "Bugsee iOS application token",
189
+ optional: false),
190
+ FastlaneCore::ConfigItem.new(key: :app_path,
191
+ env_name: "BUGSEE_APP_PATH",
192
+ description: "Path to the built `.app` directory. When unset, resolved from :xcarchive_path",
193
+ optional: true),
194
+ FastlaneCore::ConfigItem.new(key: :xcarchive_path,
195
+ env_name: "BUGSEE_XCARCHIVE_PATH",
196
+ description: "Path to the .xcarchive — used to resolve a single .app under Products/Applications/. Ignored when :app_path is set",
197
+ optional: true),
198
+ FastlaneCore::ConfigItem.new(key: :version,
199
+ env_name: "BUGSEE_APP_VERSION",
200
+ description: "CFBundleShortVersionString (e.g. \"1.2.3\"). Optional — read from Info.plist by BugseeAgent if absent",
201
+ optional: true),
202
+ FastlaneCore::ConfigItem.new(key: :build,
203
+ env_name: "BUGSEE_APP_BUILD",
204
+ description: "CFBundleVersion (e.g. \"42\"). Optional — read from Info.plist by BugseeAgent if absent",
205
+ optional: true),
206
+ FastlaneCore::ConfigItem.new(key: :build_info_only,
207
+ env_name: "BUGSEE_BUILD_INFO_ONLY",
208
+ description: "When true, register the build (records artifact_size for the size-trend chart) but skip shipping the .ipa bytes. Useful for firewalled CI and privacy-sensitive setups",
209
+ is_string: false,
210
+ default_value: false,
211
+ optional: true),
212
+ FastlaneCore::ConfigItem.new(key: :force,
213
+ env_name: "BUGSEE_FORCE",
214
+ description: "Skip the cross-producer handshake check and always upload, even if the Bugsee iOS SDK's BugseeAgent build phase already handled this build's artefact",
215
+ is_string: false,
216
+ default_value: false,
217
+ optional: true)
218
+ ]
219
+ end
220
+
221
+ def self.authors
222
+ ["bugsee"]
223
+ end
224
+
225
+ def self.is_supported?(platform)
226
+ platform == :ios
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,260 @@
1
+ require 'fastlane/plugin/bugsee/helper/bugsee_uuid'
2
+ require 'fastlane/plugin/bugsee/helper/bugsee_handshake'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ # Upload an Android ProGuard / R8 mapping.txt to Bugsee.
7
+ #
8
+ # When and why to use this:
9
+ #
10
+ # The Bugsee Android Gradle plugin already uploads mapping.txt
11
+ # as part of the standard gradle build. The most common
12
+ # scenario for this fastlane action is CI pipelines that split
13
+ # build (machine A, no token) from publish (machine B, has the
14
+ # production token) — gradle builds the APK on A; fastlane on
15
+ # B uploads the mapping with the production token. The action
16
+ # ALSO works for apps not instrumented by the Bugsee Gradle
17
+ # plugin, but only when paired with Bugsee Android SDK
18
+ # 7.0.0-beta13+ (older SDKs cannot match crashes against a
19
+ # synthesized UUID).
20
+ #
21
+ # UUID resolution chain (from most authoritative to fallback):
22
+ #
23
+ # 1. The :uuid ConfigItem when passed explicitly. Escape hatch.
24
+ # 2. The Bugsee Gradle plugin's BugseeBuildIdResolveTask
25
+ # output file (build/intermediates/bugsee/<variant>/
26
+ # build-uuid.txt). When this file is present, the Gradle
27
+ # plugin IS in the loop and the UUID it computed is the
28
+ # authoritative value that the SDK's asset / manifest
29
+ # channels carry at runtime.
30
+ # 3. Ruby-side synthesis matching the SDK's Channel 3
31
+ # fallback formula:
32
+ # UUID.nameUUIDFromBytes(
33
+ # app_token + 0x1F + version + 0x1F + build
34
+ # )
35
+ # Only useful when the host app uses SDK 7.0.0-beta13+
36
+ # AND has no Gradle plugin in the loop.
37
+ #
38
+ # All three branches produce a UUID the SDK can independently
39
+ # reproduce at runtime, so the server-side mapping lookup
40
+ # resolves crashes correctly regardless of which branch fired.
41
+ class UploadMappingToBugseeAction < Action
42
+ BUGSEE_AGENT_PATH = File.expand_path(
43
+ File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'BugseeAgent'))
44
+
45
+ # Glob pattern for the Bugsee Gradle plugin's
46
+ # BugseeBuildIdResolveTask output. Used by the UUID resolution
47
+ # chain when :build_uuid_path isn't explicit. The leading
48
+ # `**/` covers multi-module projects where the `app/` module
49
+ # nests under a top-level checkout.
50
+ BUILD_UUID_GLOB = "**/build/intermediates/bugsee/*/build-uuid.txt".freeze
51
+
52
+ def self.run(params)
53
+ app_token = params[:app_token]
54
+ mapping_path = params[:mapping_path]
55
+ host = params[:host] || "https://api.bugsee.com"
56
+ version = params[:version]
57
+ build = params[:build]
58
+ icon_path = params[:icon_path]
59
+ agent_path = params[:agent_path] || BUGSEE_AGENT_PATH
60
+
61
+ UI.user_error!("Please provide an app token via app_token:") unless app_token
62
+ UI.user_error!("Please provide a path to the Android mapping.txt via mapping_path:") unless mapping_path
63
+ UI.user_error!("Mapping file does not exist: #{mapping_path}") unless File.exist?(mapping_path)
64
+ UI.user_error!("Please provide an app version via version: (android:versionName)") if version.nil? || version.to_s.empty?
65
+ UI.user_error!("Please provide a build number via build: (android:versionCode)") if build.nil? || build.to_s.empty?
66
+
67
+ agent_path = File.expand_path(agent_path)
68
+ UI.user_error!("BugseeAgent helper script is missing: #{agent_path}") unless File.exist?(agent_path)
69
+
70
+ if icon_path && !File.exist?(icon_path)
71
+ UI.important("Bugsee: icon_path does not exist (#{icon_path}); proceeding without icon")
72
+ icon_path = nil
73
+ end
74
+
75
+ # Cross-producer handshake: if the Bugsee Android Gradle
76
+ # plugin already uploaded this build's mapping, skip — the
77
+ # server would just dedupe by hash but the lane log gets
78
+ # noisy and CI pays for redundant bandwidth. The user can
79
+ # override with `force: true` for re-uploads / debugging.
80
+ unless params[:force]
81
+ manifest = Fastlane::Bugsee::Handshake.find_manifest(
82
+ search_root: Dir.pwd,
83
+ version_name: version,
84
+ version_code: build,
85
+ )
86
+ if Fastlane::Bugsee::Handshake.handled_by_other?(manifest, 'mapping_upload')
87
+ UI.important(Fastlane::Bugsee::Handshake.skip_message(manifest, 'mapping_upload'))
88
+ return
89
+ end
90
+ end
91
+
92
+ uuid = resolve_uuid(params, app_token, version, build)
93
+ UI.message("Bugsee: uploading mapping with UUID #{uuid}")
94
+
95
+ # Shell command shape:
96
+ # python3 BugseeAgent -x \
97
+ # -e <host> -v <version> -b <build> \
98
+ # --upload-mapping \
99
+ # --mapping-path <mapping.txt> \
100
+ # --mapping-uuid <uuid> \
101
+ # [--icon <icon>] \
102
+ # [--cli-path <path> | --cli-version <ver>] \
103
+ # <app_token>
104
+ cmd = []
105
+ cmd << agent_path.shellescape
106
+ cmd << "-x" # not run from Xcode — synchronous, no daemonize
107
+ cmd << "-e #{host.shellescape}"
108
+ cmd << "-v #{version.to_s.shellescape}"
109
+ cmd << "-b #{build.to_s.shellescape}"
110
+ cmd << "--upload-mapping"
111
+ cmd << "--mapping-path #{mapping_path.shellescape}"
112
+ cmd << "--mapping-uuid #{uuid.shellescape}"
113
+ cmd << "--icon #{icon_path.shellescape}" if icon_path
114
+ cmd << "--cli-path #{params[:cli_path].shellescape}" if params[:cli_path]
115
+ cmd << "--cli-version #{params[:cli_version].shellescape}" if params[:cli_version]
116
+ cmd << app_token.shellescape
117
+
118
+ begin
119
+ Actions.sh(cmd.join(" "), log: false)
120
+ rescue => e
121
+ # Upload failure should NOT take the lane down — the
122
+ # symbols upload is a release-supporting nicety, same
123
+ # posture as upload_symbols_to_bugsee. The Ruby UI.error
124
+ # surfaces the cause in the build log.
125
+ UI.error(e.to_s)
126
+ end
127
+ end
128
+
129
+ # @api private
130
+ # Resolution chain implemented as documented in the class
131
+ # docstring above. Public-ish so the RSpec tests can exercise
132
+ # each branch in isolation.
133
+ def self.resolve_uuid(params, app_token, version, build)
134
+ explicit = params[:uuid]
135
+ return explicit if explicit && !explicit.to_s.empty?
136
+
137
+ # Branch 2: Gradle plugin's BugseeBuildIdResolveTask output.
138
+ # When :build_uuid_path is explicit, trust it. Otherwise glob
139
+ # under the current working directory (most fastlane lanes
140
+ # cd to the project root before invoking actions).
141
+ build_uuid_path = params[:build_uuid_path]
142
+ if build_uuid_path
143
+ if File.exist?(build_uuid_path)
144
+ return File.read(build_uuid_path).strip
145
+ else
146
+ UI.important("Bugsee: build_uuid_path given but not found: #{build_uuid_path}")
147
+ end
148
+ else
149
+ # Best-effort glob. Take the first match — multi-variant
150
+ # projects may have several; if the lane wants a specific
151
+ # variant, it should pass :build_uuid_path explicitly.
152
+ match = Dir.glob(BUILD_UUID_GLOB).first
153
+ return File.read(match).strip if match
154
+ end
155
+
156
+ # Branch 3: synthesize from (app_token, version, build).
157
+ # Matches the SDK's Channel 3 fallback formula byte-for-byte
158
+ # so a 7.0.0-beta13+ SDK at runtime computes the same UUID
159
+ # and the upload matches.
160
+ UI.message("Bugsee: no Gradle plugin build-uuid.txt found; " \
161
+ "synthesizing UUID from app_token+version+build. " \
162
+ "This requires Bugsee Android SDK >= 7.0.0-beta13 " \
163
+ "at runtime.")
164
+ Fastlane::Bugsee::Uuid.synthesize_build_uuid(app_token, version, build)
165
+ end
166
+
167
+ def self.description
168
+ "Upload an Android ProGuard / R8 mapping.txt to Bugsee."
169
+ end
170
+
171
+ def self.details
172
+ <<~DETAILS
173
+ Uploads an Android ProGuard / R8 mapping.txt to Bugsee for crash
174
+ symbolication. The Bugsee Android Gradle plugin already does this
175
+ as part of the standard gradle build; this action is for split
176
+ build/publish CI pipelines and for apps not instrumented by the
177
+ Gradle plugin (the latter requires Bugsee Android SDK 7.0.0-beta13+).
178
+
179
+ UUID resolution: explicit :uuid > :build_uuid_path > the Gradle
180
+ plugin's build-uuid.txt (auto-globbed) > synthesized from
181
+ (app_token, version, build) matching the SDK's runtime fallback.
182
+ DETAILS
183
+ end
184
+
185
+ def self.available_options
186
+ [
187
+ FastlaneCore::ConfigItem.new(key: :agent_path,
188
+ env_name: "BUGSEE_AGENT_PATH",
189
+ description: "The path to the BugseeAgent helper script",
190
+ optional: true,
191
+ verify_block: proc do |value|
192
+ UI.user_error!("Couldn't find BugseeAgent at path '#{value}'") unless File.exist?(value)
193
+ end),
194
+ FastlaneCore::ConfigItem.new(key: :host,
195
+ env_name: "BUGSEE_API_HOST",
196
+ description: "The path to API endpoint",
197
+ optional: true),
198
+ FastlaneCore::ConfigItem.new(key: :app_token,
199
+ env_name: "BUGSEE_APP_TOKEN",
200
+ description: "Bugsee Android application token",
201
+ optional: false),
202
+ FastlaneCore::ConfigItem.new(key: :mapping_path,
203
+ env_name: "BUGSEE_MAPPING_PATH",
204
+ description: "Path to the Android mapping.txt (R8 / ProGuard)",
205
+ optional: false),
206
+ # Fastlane's Android `gradle` action publishes no
207
+ # SharedValues equivalent of iOS's VERSION_NUMBER /
208
+ # BUILD_NUMBER — :version / :build remain explicit
209
+ # ConfigItems with no auto-default. The action validates
210
+ # both in .run and raises a UI.user_error if missing.
211
+ # Consumers typically wire these from get_version_name /
212
+ # get_version_code (community actions) or read them from
213
+ # build.gradle themselves before calling this action.
214
+ FastlaneCore::ConfigItem.new(key: :version,
215
+ env_name: "BUGSEE_APP_VERSION",
216
+ description: "android:versionName (e.g. \"1.2.3\")",
217
+ optional: true),
218
+ FastlaneCore::ConfigItem.new(key: :build,
219
+ env_name: "BUGSEE_APP_BUILD",
220
+ description: "android:versionCode (e.g. \"42\")",
221
+ optional: true),
222
+ FastlaneCore::ConfigItem.new(key: :uuid,
223
+ env_name: "BUGSEE_BUILD_UUID",
224
+ description: "Override BUILD_UUID. When unset, the action reads the Bugsee Gradle plugin's build-uuid.txt OR synthesizes via MD5",
225
+ optional: true),
226
+ FastlaneCore::ConfigItem.new(key: :build_uuid_path,
227
+ env_name: "BUGSEE_BUILD_UUID_PATH",
228
+ description: "Explicit path to the Bugsee Gradle plugin's build-uuid.txt. When unset, the action globs **/build/intermediates/bugsee/*/build-uuid.txt",
229
+ optional: true),
230
+ FastlaneCore::ConfigItem.new(key: :icon_path,
231
+ env_name: "BUGSEE_ICON_PATH",
232
+ description: "Optional launcher icon PNG to attach to the symbol record",
233
+ optional: true),
234
+ FastlaneCore::ConfigItem.new(key: :cli_path,
235
+ env_name: "BUGSEE_CLI_PATH",
236
+ description: "Path to a local bugsee-cli binary (developer override)",
237
+ optional: true),
238
+ FastlaneCore::ConfigItem.new(key: :cli_version,
239
+ env_name: "BUGSEE_CLI_VERSION",
240
+ description: "bugsee-cli version to auto-download",
241
+ optional: true),
242
+ FastlaneCore::ConfigItem.new(key: :force,
243
+ env_name: "BUGSEE_FORCE",
244
+ description: "Skip the cross-producer handshake check and always upload, even if the Bugsee Android Gradle plugin already handled this build's mapping",
245
+ is_string: false,
246
+ default_value: false,
247
+ optional: true)
248
+ ]
249
+ end
250
+
251
+ def self.authors
252
+ ["bugsee"]
253
+ end
254
+
255
+ def self.is_supported?(platform)
256
+ platform == :android
257
+ end
258
+ end
259
+ end
260
+ end