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.
- checksums.yaml +5 -5
- data/BugseeAgent +3962 -0
- data/README.md +102 -2
- data/lib/fastlane/plugin/bugsee/actions/upload_artifact_to_bugsee.rb +230 -0
- data/lib/fastlane/plugin/bugsee/actions/upload_mapping_to_bugsee.rb +260 -0
- data/lib/fastlane/plugin/bugsee/actions/upload_symbols_to_bugsee.rb +104 -19
- data/lib/fastlane/plugin/bugsee/helper/bugsee_handshake.rb +228 -0
- data/lib/fastlane/plugin/bugsee/helper/bugsee_uuid.rb +101 -0
- data/lib/fastlane/plugin/bugsee/version.rb +1 -1
- metadata +8 -8
|
@@ -1,11 +1,46 @@
|
|
|
1
|
+
require 'fastlane/plugin/bugsee/helper/bugsee_handshake'
|
|
2
|
+
|
|
1
3
|
module Fastlane
|
|
2
4
|
module Actions
|
|
3
5
|
|
|
6
|
+
# Upload iOS dSYM(s) to Bugsee, optionally collecting the
|
|
7
|
+
# project's CocoaPods/SPM/Carthage dependency graph and the
|
|
8
|
+
# latest Xcode xcactivitylog-derived build timings in the
|
|
9
|
+
# same lane action.
|
|
10
|
+
#
|
|
11
|
+
# ## Cross-producer handshake
|
|
12
|
+
#
|
|
13
|
+
# When the Bugsee iOS SDK's `tools.bundle/BugseeAgent` build
|
|
14
|
+
# phase is wired into the Xcode project, it runs alongside
|
|
15
|
+
# fastlane and may already have handled some of these
|
|
16
|
+
# actions for this build. Before invoking BugseeAgent, this
|
|
17
|
+
# action reads the handshake manifest (see
|
|
18
|
+
# `Fastlane::Bugsee::Handshake`) and skips per-action work
|
|
19
|
+
# that's already been done.
|
|
20
|
+
#
|
|
21
|
+
# The handshake is checked for THREE independent actions:
|
|
22
|
+
#
|
|
23
|
+
# - `dsym_upload` — the primary purpose of this action.
|
|
24
|
+
# - `deps_collection` — Podfile/Package.resolved/Cartfile
|
|
25
|
+
# parsing + upload.
|
|
26
|
+
# - `timings` — xcactivitylog parsing + upload.
|
|
27
|
+
#
|
|
28
|
+
# When ALL three are handled by another producer for this
|
|
29
|
+
# build, the action skips invoking BugseeAgent entirely. When
|
|
30
|
+
# SOME are handled, the relevant `--no-*` flags are passed to
|
|
31
|
+
# BugseeAgent so only the missing work runs.
|
|
32
|
+
#
|
|
33
|
+
# The `:force` ConfigItem skips the handshake check entirely.
|
|
34
|
+
# Useful for re-uploads / debugging.
|
|
4
35
|
class UploadSymbolsToBugseeAction < Action
|
|
36
|
+
|
|
37
|
+
BUGSEE_AGENT_PATH = File.expand_path(
|
|
38
|
+
File.join(File.dirname(__FILE__), '..', '..', '..', '..', '..', 'BugseeAgent'))
|
|
39
|
+
|
|
5
40
|
def self.run(params)
|
|
6
41
|
app_token = params[:app_token]
|
|
7
42
|
host = params[:host] || "https://api.bugsee.com"
|
|
8
|
-
agent_path = params[:agent_path] ||
|
|
43
|
+
agent_path = params[:agent_path] || BUGSEE_AGENT_PATH
|
|
9
44
|
build_dir = params[:build_dir] || "./"
|
|
10
45
|
|
|
11
46
|
UI.user_error!("Please provide an app token using app_token:") unless app_token
|
|
@@ -26,26 +61,70 @@ module Fastlane
|
|
|
26
61
|
|
|
27
62
|
dsym_paths.each do |path|
|
|
28
63
|
print(path)
|
|
29
|
-
UI.user_error!("dSYM does not exist at path: #{path}") unless File.
|
|
64
|
+
UI.user_error!("dSYM does not exist at path: #{path}") unless File.exist?(path)
|
|
30
65
|
end
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
# ──────────────────────────────────────────────────────
|
|
68
|
+
# Cross-producer handshake
|
|
69
|
+
# ──────────────────────────────────────────────────────
|
|
70
|
+
skip_dsym = false
|
|
71
|
+
skip_deps = false
|
|
72
|
+
skip_timings = false
|
|
73
|
+
unless params[:force]
|
|
74
|
+
manifest = Fastlane::Bugsee::Handshake.find_manifest(
|
|
75
|
+
search_root: Dir.pwd,
|
|
76
|
+
version_name: version,
|
|
77
|
+
version_code: build,
|
|
78
|
+
)
|
|
79
|
+
skip_dsym = Fastlane::Bugsee::Handshake.handled_by_other?(manifest, 'dsym_upload')
|
|
80
|
+
skip_deps = Fastlane::Bugsee::Handshake.handled_by_other?(manifest, 'deps_collection')
|
|
81
|
+
skip_timings = Fastlane::Bugsee::Handshake.handled_by_other?(manifest, 'timings')
|
|
82
|
+
|
|
83
|
+
[['dsym_upload', skip_dsym],
|
|
84
|
+
['deps_collection', skip_deps],
|
|
85
|
+
['timings', skip_timings]].each do |action_name, handled|
|
|
86
|
+
next unless handled
|
|
87
|
+
UI.important(Fastlane::Bugsee::Handshake.skip_message(manifest, action_name))
|
|
47
88
|
end
|
|
48
89
|
end
|
|
90
|
+
|
|
91
|
+
# Strip dSYM paths when the handshake says they're already
|
|
92
|
+
# handled — sending them along would make BugseeAgent
|
|
93
|
+
# re-upload them since it interprets positional args as
|
|
94
|
+
# dSYMs to push.
|
|
95
|
+
effective_dsyms = skip_dsym ? [] : dsym_paths
|
|
96
|
+
|
|
97
|
+
# When there's nothing for BugseeAgent to do at all,
|
|
98
|
+
# short-circuit before constructing the command.
|
|
99
|
+
if effective_dsyms.empty? && skip_deps && skip_timings
|
|
100
|
+
UI.message("Bugsee: nothing to do — every action this lane would have run is already handled by another Bugsee producer for this build")
|
|
101
|
+
return
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# If we have NO dSYMs (either none provided OR all
|
|
105
|
+
# handled) AND deps/timings have work to do, the BugseeAgent
|
|
106
|
+
# invocation has to be non-dSYM. The agent's main() runs
|
|
107
|
+
# the deps + timings pipeline unconditionally when
|
|
108
|
+
# collect_deps is enabled and is told it's not dealing
|
|
109
|
+
# with dSYMs — passing zero positional args + the
|
|
110
|
+
# "external" -x flag is the existing shape for that.
|
|
111
|
+
command = []
|
|
112
|
+
command << agent_path.shellescape
|
|
113
|
+
command << "-e #{host}"
|
|
114
|
+
command << "-v #{version}" if version
|
|
115
|
+
command << "-b #{build}" if build
|
|
116
|
+
command << "-d #{build_dir}"
|
|
117
|
+
command << "-m #{symbol_maps}" if symbol_maps
|
|
118
|
+
command << "--no-deps" if skip_deps
|
|
119
|
+
command << "--no-timings" if skip_timings
|
|
120
|
+
command << "-x -l #{app_token}"
|
|
121
|
+
command += effective_dsyms
|
|
122
|
+
|
|
123
|
+
begin
|
|
124
|
+
Actions.sh(command.join(" "), log: false)
|
|
125
|
+
rescue => ex
|
|
126
|
+
UI.error ex.to_s # it fails, however we don't want to fail everything just for this
|
|
127
|
+
end
|
|
49
128
|
end
|
|
50
129
|
|
|
51
130
|
|
|
@@ -55,7 +134,7 @@ module Fastlane
|
|
|
55
134
|
#####################################################
|
|
56
135
|
|
|
57
136
|
def self.description
|
|
58
|
-
|
|
137
|
+
"Upload iOS dSYM(s) to Bugsee for crash symbolication. Also collects dependencies + build timings when not already handled by the Bugsee iOS SDK's tools.bundle/BugseeAgent build phase."
|
|
59
138
|
end
|
|
60
139
|
|
|
61
140
|
def self.details
|
|
@@ -107,7 +186,13 @@ module Fastlane
|
|
|
107
186
|
FastlaneCore::ConfigItem.new(key: :build_dir,
|
|
108
187
|
env_name: "TARGET_BUILD_DIR",
|
|
109
188
|
description: "Target build directory",
|
|
110
|
-
optional: true)
|
|
189
|
+
optional: true),
|
|
190
|
+
FastlaneCore::ConfigItem.new(key: :force,
|
|
191
|
+
env_name: "BUGSEE_FORCE",
|
|
192
|
+
description: "Skip the cross-producer handshake check and run every action this lane was configured for, even if the Bugsee iOS SDK BugseeAgent build phase already handled some of them",
|
|
193
|
+
is_string: false,
|
|
194
|
+
default_value: false,
|
|
195
|
+
optional: true)
|
|
111
196
|
]
|
|
112
197
|
end
|
|
113
198
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module Bugsee
|
|
7
|
+
# Cross-producer build-action handshake.
|
|
8
|
+
#
|
|
9
|
+
# When more than one Bugsee producer can run on a build —
|
|
10
|
+
# typically the Bugsee Android Gradle plugin OR the Bugsee iOS
|
|
11
|
+
# SDK's `tools.bundle/BugseeAgent` build phase running ALONGSIDE
|
|
12
|
+
# this fastlane plugin — we want only ONE producer to handle
|
|
13
|
+
# each action (mapping upload, dSYM upload, deps collection,
|
|
14
|
+
# build timings, etc.). Otherwise the same payload reaches the
|
|
15
|
+
# server twice; the server dedupes by hash so it's not
|
|
16
|
+
# *broken*, but the lane log gets noisy and the customer pays
|
|
17
|
+
# for redundant bandwidth.
|
|
18
|
+
#
|
|
19
|
+
# Each producer that runs writes a small JSON manifest
|
|
20
|
+
# declaring which actions it handled for THIS build. The
|
|
21
|
+
# fastlane plugin reads the most recent matching manifest
|
|
22
|
+
# before each action and skips work the other producer already
|
|
23
|
+
# did.
|
|
24
|
+
#
|
|
25
|
+
# ## Manifest locations
|
|
26
|
+
#
|
|
27
|
+
# - **Bugsee Android Gradle plugin** writes
|
|
28
|
+
# `<project>/**/build/intermediates/bugsee/<variant>/build-actions.json`
|
|
29
|
+
# from its existing variant tasks (same parent dir as
|
|
30
|
+
# `build-id.txt`).
|
|
31
|
+
# - **Bugsee iOS SDK `tools.bundle/BugseeAgent`** writes
|
|
32
|
+
# `<srcroot>/build/bugsee/build-actions.json` from its
|
|
33
|
+
# post-build Xcode build phase.
|
|
34
|
+
#
|
|
35
|
+
# Both formats share the JSON shape below.
|
|
36
|
+
#
|
|
37
|
+
# ## Manifest schema (version 1)
|
|
38
|
+
#
|
|
39
|
+
# {
|
|
40
|
+
# "schema_version": 1,
|
|
41
|
+
# "producer": "bugsee-android-gradle-plugin"
|
|
42
|
+
# | "bugsee-ios-sdk-tools-bundle",
|
|
43
|
+
# "producer_version": "1.2.3",
|
|
44
|
+
# "build_id": "<BUILD_UUID>",
|
|
45
|
+
# "produced_at_ms": 1700000000000,
|
|
46
|
+
# "version_name": "1.2.3",
|
|
47
|
+
# "version_code": "42",
|
|
48
|
+
# "actions": {
|
|
49
|
+
# "mapping_upload": true | false,
|
|
50
|
+
# "dsym_upload": true | false,
|
|
51
|
+
# "deps_collection": true | false,
|
|
52
|
+
# "timings": true | false,
|
|
53
|
+
# "size_analysis": true | false,
|
|
54
|
+
# "artifact_upload": true | false
|
|
55
|
+
# }
|
|
56
|
+
# }
|
|
57
|
+
#
|
|
58
|
+
# A `true` value means the named producer ran that action for
|
|
59
|
+
# this build (whether it succeeded or failed — retrying isn't
|
|
60
|
+
# fastlane's concern). A `false` or absent value means the
|
|
61
|
+
# producer was either not configured to handle it or skipped
|
|
62
|
+
# for this variant. Unknown keys are reserved for forward
|
|
63
|
+
# compatibility; readers must ignore them.
|
|
64
|
+
#
|
|
65
|
+
# ## Staleness window
|
|
66
|
+
#
|
|
67
|
+
# A manifest is considered valid only when its `produced_at_ms`
|
|
68
|
+
# is within {STALENESS_WINDOW_S} seconds of `Time.now`. A stale
|
|
69
|
+
# manifest from last week's build is treated as absent; fastlane
|
|
70
|
+
# does the work itself.
|
|
71
|
+
#
|
|
72
|
+
# ## Build-identity match
|
|
73
|
+
#
|
|
74
|
+
# If the action passes explicit `:version` / `:build`
|
|
75
|
+
# ConfigItems, those are checked against the manifest's
|
|
76
|
+
# `version_name` / `version_code`. A mismatch indicates the
|
|
77
|
+
# manifest is from a different build (developer's previous
|
|
78
|
+
# archive, CI's previous run, etc.) and is treated as absent.
|
|
79
|
+
# When the action doesn't pass them, build-identity matching
|
|
80
|
+
# is skipped and the manifest is honored as long as it's not
|
|
81
|
+
# stale.
|
|
82
|
+
#
|
|
83
|
+
# ## Override
|
|
84
|
+
#
|
|
85
|
+
# Every action that uses this helper takes a `:force` ConfigItem
|
|
86
|
+
# that short-circuits the handshake check. Useful for one-off
|
|
87
|
+
# re-uploads, debugging, or cases where the user wants fastlane
|
|
88
|
+
# to do the work regardless of what the other producer claims.
|
|
89
|
+
module Handshake
|
|
90
|
+
# Seconds — manifests older than this are treated as absent.
|
|
91
|
+
# A normal CI run is rare to last more than an hour; a local
|
|
92
|
+
# dev rerun the next morning genuinely is a different build.
|
|
93
|
+
STALENESS_WINDOW_S = 60 * 60
|
|
94
|
+
|
|
95
|
+
# The canonical action keys the manifest's `actions` block
|
|
96
|
+
# uses. Producers SHOULD emit only these — unknown keys are
|
|
97
|
+
# ignored by readers but may be reserved for future features.
|
|
98
|
+
ACTION_KEYS = %w[
|
|
99
|
+
mapping_upload
|
|
100
|
+
dsym_upload
|
|
101
|
+
deps_collection
|
|
102
|
+
timings
|
|
103
|
+
size_analysis
|
|
104
|
+
artifact_upload
|
|
105
|
+
].freeze
|
|
106
|
+
|
|
107
|
+
# Filesystem glob patterns for locating manifests. The list
|
|
108
|
+
# is ordered so that a project laid out for both platforms
|
|
109
|
+
# at once (e.g. a cross-platform fastlane lane) picks the
|
|
110
|
+
# most-likely-relevant manifest first.
|
|
111
|
+
MANIFEST_GLOBS = [
|
|
112
|
+
# Android Gradle plugin output. The `**/` covers multi-
|
|
113
|
+
# module projects where the `app/` module nests below a
|
|
114
|
+
# top-level checkout. The intermediates dir is per-variant
|
|
115
|
+
# so a project shipping multiple variants will produce
|
|
116
|
+
# multiple manifests; the staleness + identity check
|
|
117
|
+
# narrows to the right one.
|
|
118
|
+
"**/build/intermediates/bugsee/*/build-actions.json",
|
|
119
|
+
# iOS SDK BugseeAgent output. Single file per project — no
|
|
120
|
+
# variant slot since iOS targets multiplex through one
|
|
121
|
+
# build phase.
|
|
122
|
+
"**/build/bugsee/build-actions.json",
|
|
123
|
+
].freeze
|
|
124
|
+
|
|
125
|
+
# Locate the manifest most likely to describe THIS build.
|
|
126
|
+
#
|
|
127
|
+
# @param search_root [String, nil] directory to search under.
|
|
128
|
+
# Defaults to current working directory (most fastlane
|
|
129
|
+
# lanes cd to the project root before invoking actions).
|
|
130
|
+
# @param version_name [String, nil] expected android:versionName
|
|
131
|
+
# / iOS CFBundleShortVersionString. When given, the manifest
|
|
132
|
+
# must match — used to reject stale manifests from a
|
|
133
|
+
# previous build at a different version.
|
|
134
|
+
# @param version_code [String, Integer, nil] expected
|
|
135
|
+
# android:versionCode / iOS CFBundleVersion. Same role.
|
|
136
|
+
# @param now [Time] for tests — production callers omit it.
|
|
137
|
+
# @return [Hash, nil] the parsed manifest, or `nil` when no
|
|
138
|
+
# valid manifest was found.
|
|
139
|
+
def self.find_manifest(search_root: Dir.pwd,
|
|
140
|
+
version_name: nil,
|
|
141
|
+
version_code: nil,
|
|
142
|
+
now: Time.now)
|
|
143
|
+
candidates = MANIFEST_GLOBS.flat_map do |glob|
|
|
144
|
+
Dir.glob(File.join(search_root, glob))
|
|
145
|
+
end
|
|
146
|
+
# Sort newest-first by file mtime; then validity-check.
|
|
147
|
+
# First valid wins.
|
|
148
|
+
candidates
|
|
149
|
+
.sort_by { |p| -File.mtime(p).to_f }
|
|
150
|
+
.each do |path|
|
|
151
|
+
parsed = safe_parse(path)
|
|
152
|
+
next unless parsed
|
|
153
|
+
next unless valid_for_build?(
|
|
154
|
+
parsed,
|
|
155
|
+
version_name: version_name,
|
|
156
|
+
version_code: version_code,
|
|
157
|
+
now: now,
|
|
158
|
+
)
|
|
159
|
+
return parsed
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Per-action skip check.
|
|
165
|
+
#
|
|
166
|
+
# @param manifest [Hash, nil] the manifest returned by
|
|
167
|
+
# {find_manifest}, or `nil` when none was found.
|
|
168
|
+
# @param action [String] one of {ACTION_KEYS}.
|
|
169
|
+
# @return [Boolean] true when ANOTHER producer ran this
|
|
170
|
+
# action and fastlane should skip; false otherwise
|
|
171
|
+
# (manifest absent, action not listed, action listed false).
|
|
172
|
+
def self.handled_by_other?(manifest, action)
|
|
173
|
+
return false if manifest.nil?
|
|
174
|
+
actions = manifest['actions']
|
|
175
|
+
return false unless actions.is_a?(Hash)
|
|
176
|
+
actions[action] == true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Convenience: when {handled_by_other?} is true, build a
|
|
180
|
+
# log line the action can surface so the user understands
|
|
181
|
+
# why fastlane is skipping. Returns nil otherwise.
|
|
182
|
+
def self.skip_message(manifest, action)
|
|
183
|
+
return nil unless handled_by_other?(manifest, action)
|
|
184
|
+
producer = manifest['producer'] || 'another Bugsee producer'
|
|
185
|
+
version = manifest['producer_version']
|
|
186
|
+
version_str = version ? " (#{version})" : ''
|
|
187
|
+
"Bugsee: skipping #{action} — already handled by #{producer}#{version_str} for this build"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @api private
|
|
191
|
+
# Parse + structurally validate a manifest file. Returns the
|
|
192
|
+
# parsed Hash on success, nil on any error. Errors are
|
|
193
|
+
# SILENT — a malformed manifest must not break the fastlane
|
|
194
|
+
# lane.
|
|
195
|
+
def self.safe_parse(path)
|
|
196
|
+
return nil unless File.file?(path)
|
|
197
|
+
raw = File.read(path)
|
|
198
|
+
return nil if raw.nil? || raw.empty?
|
|
199
|
+
parsed = JSON.parse(raw)
|
|
200
|
+
return nil unless parsed.is_a?(Hash)
|
|
201
|
+
return nil unless parsed['schema_version'] == 1
|
|
202
|
+
parsed
|
|
203
|
+
rescue JSON::ParserError, Errno::ENOENT, Errno::EACCES
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @api private
|
|
208
|
+
# Apply the staleness window + build-identity match.
|
|
209
|
+
def self.valid_for_build?(manifest, version_name:, version_code:, now:)
|
|
210
|
+
produced_at_ms = manifest['produced_at_ms']
|
|
211
|
+
return false unless produced_at_ms.is_a?(Numeric)
|
|
212
|
+
age_s = now.to_f - (produced_at_ms.to_f / 1000.0)
|
|
213
|
+
# Negative age (manifest from the future — clock skew or
|
|
214
|
+
# tampered file) is rejected. Excess positive age too.
|
|
215
|
+
return false if age_s < 0
|
|
216
|
+
return false if age_s > STALENESS_WINDOW_S
|
|
217
|
+
|
|
218
|
+
if version_name && !version_name.to_s.empty?
|
|
219
|
+
return false unless manifest['version_name'].to_s == version_name.to_s
|
|
220
|
+
end
|
|
221
|
+
if version_code && !version_code.to_s.empty?
|
|
222
|
+
return false unless manifest['version_code'].to_s == version_code.to_s
|
|
223
|
+
end
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module Bugsee
|
|
7
|
+
# Cross-language `UUID.nameUUIDFromBytes` equivalent.
|
|
8
|
+
#
|
|
9
|
+
# The Bugsee Android SDK (Java) computes synthesized BUILD_UUIDs
|
|
10
|
+
# via {@code java.util.UUID.nameUUIDFromBytes(bytes)}. Per RFC
|
|
11
|
+
# 4122 / the Java spec, that's MD5 over the input bytes with two
|
|
12
|
+
# bit twiddles:
|
|
13
|
+
#
|
|
14
|
+
# - byte 6, upper nibble: clear, then set to 0011 (UUID
|
|
15
|
+
# version 3 — name-based, MD5)
|
|
16
|
+
# - byte 8, upper two bits: clear, then set to 10 (variant
|
|
17
|
+
# RFC 4122)
|
|
18
|
+
#
|
|
19
|
+
# The Bugsee Rust CLI's `bugsee-cli` replicates the same
|
|
20
|
+
# primitive for ProGuard-mapping-content-derived UUIDs (the
|
|
21
|
+
# `bugsee_cli_proguard_uuid_compat` parity is already proven and
|
|
22
|
+
# locked in). This Ruby implementation closes the loop for the
|
|
23
|
+
# fastlane plugin's mapping-upload action: when no explicit
|
|
24
|
+
# `:uuid` is passed and no Gradle-plugin-emitted `build-uuid.txt`
|
|
25
|
+
# is found, the action synthesizes a UUID Ruby-side from
|
|
26
|
+
# `(app_token, version_name, version_code)` and uploads the
|
|
27
|
+
# mapping with that UUID; the SDK (7.0.0-beta13+) computes the
|
|
28
|
+
# same bytes at runtime via the same primitive and the server
|
|
29
|
+
# matches.
|
|
30
|
+
#
|
|
31
|
+
# This module has ZERO fastlane dependencies — the unit tests can
|
|
32
|
+
# require it directly without loading the whole fastlane runtime.
|
|
33
|
+
module Uuid
|
|
34
|
+
# ASCII Unit Separator (0x1F). Same byte the SDK uses to join
|
|
35
|
+
# the synthesis inputs (see
|
|
36
|
+
# `BugseeEnvironment.BUILD_ID_SYNTHESIS_SEPARATOR`). Guaranteed
|
|
37
|
+
# never to appear in any of the three inputs (app token is
|
|
38
|
+
# ASCII hex, versionName is dotted, versionCode is digits).
|
|
39
|
+
SEPARATOR = ""
|
|
40
|
+
|
|
41
|
+
# Ruby equivalent of Java's `UUID.nameUUIDFromBytes(bytes)`.
|
|
42
|
+
# Takes a String (treated as UTF-8 bytes) or a Binary String,
|
|
43
|
+
# returns the canonical 8-4-4-4-12 lowercase UUID string.
|
|
44
|
+
#
|
|
45
|
+
# NOT a cryptographic primitive. MD5 is deprecated for security
|
|
46
|
+
# uses but the Java spec mandates it for `nameUUIDFromBytes`,
|
|
47
|
+
# and we MUST stay byte-for-byte compatible with the SDK side.
|
|
48
|
+
def self.name_uuid_from_bytes(input)
|
|
49
|
+
# Force UTF-8 encoding before digest so that a future caller
|
|
50
|
+
# passing an ASCII-8BIT (binary) string doesn't accidentally
|
|
51
|
+
# round-trip through a different code path. Digest::MD5 sees
|
|
52
|
+
# bytes either way, but normalizing upstream removes one
|
|
53
|
+
# category of "why don't these match" puzzles.
|
|
54
|
+
bytes = input.dup.force_encoding(Encoding::UTF_8).bytes
|
|
55
|
+
md5 = Digest::MD5.digest(bytes.pack('C*'))
|
|
56
|
+
raw = md5.bytes
|
|
57
|
+
|
|
58
|
+
# UUID version (4 high bits of byte 6) = 3 (name-based, MD5).
|
|
59
|
+
raw[6] = (raw[6] & 0x0f) | 0x30
|
|
60
|
+
# UUID variant (2 high bits of byte 8) = RFC 4122 (binary 10).
|
|
61
|
+
raw[8] = (raw[8] & 0x3f) | 0x80
|
|
62
|
+
|
|
63
|
+
hex = raw.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
64
|
+
"#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Synthesize a BUILD_UUID matching what the Bugsee Android SDK
|
|
68
|
+
# 7.0.0-beta13+ computes at runtime when neither the asset
|
|
69
|
+
# channel nor the manifest meta-data channel populated.
|
|
70
|
+
#
|
|
71
|
+
# Formula (must stay byte-for-byte aligned with the SDK side —
|
|
72
|
+
# see {@code BugseeEnvironment.synthesizeBuildIdFromVersion}):
|
|
73
|
+
#
|
|
74
|
+
# name_uuid_from_bytes(
|
|
75
|
+
# app_token + 0x1F + version_name + 0x1F + version_code
|
|
76
|
+
# )
|
|
77
|
+
#
|
|
78
|
+
# @param app_token [String] the Bugsee application token
|
|
79
|
+
# (mandatory — disambiguates apps within
|
|
80
|
+
# the same Bugsee organisation).
|
|
81
|
+
# @param version_name [String, nil] android:versionName from
|
|
82
|
+
# the merged manifest. {@code nil}
|
|
83
|
+
# coerces to empty string (matches the
|
|
84
|
+
# SDK side's null-versionName coercion).
|
|
85
|
+
# @param version_code [Integer, String] android:versionCode.
|
|
86
|
+
# Coerced to base-10 string. The SDK uses
|
|
87
|
+
# {@code getLongVersionCode().toString()};
|
|
88
|
+
# we accept either an Integer or the
|
|
89
|
+
# pre-stringified form so the fastlane
|
|
90
|
+
# ConfigItem can take either.
|
|
91
|
+
def self.synthesize_build_uuid(app_token, version_name, version_code)
|
|
92
|
+
raise ArgumentError, "app_token is required" if app_token.nil? || app_token.to_s.empty?
|
|
93
|
+
|
|
94
|
+
version_name_str = version_name.nil? ? "" : version_name.to_s
|
|
95
|
+
version_code_str = version_code.nil? ? "0" : version_code.to_s
|
|
96
|
+
input = "#{app_token}#{SEPARATOR}#{version_name_str}#{SEPARATOR}#{version_code_str}"
|
|
97
|
+
name_uuid_from_bytes(input)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-bugsee
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Fink
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pry
|
|
@@ -94,23 +93,26 @@ dependencies:
|
|
|
94
93
|
- - ">="
|
|
95
94
|
- !ruby/object:Gem::Version
|
|
96
95
|
version: 2.2.0
|
|
97
|
-
description:
|
|
98
96
|
email: finik@bugsee.net
|
|
99
97
|
executables: []
|
|
100
98
|
extensions: []
|
|
101
99
|
extra_rdoc_files: []
|
|
102
100
|
files:
|
|
101
|
+
- BugseeAgent
|
|
103
102
|
- LICENSE
|
|
104
103
|
- README.md
|
|
105
104
|
- lib/fastlane/plugin/bugsee.rb
|
|
105
|
+
- lib/fastlane/plugin/bugsee/actions/upload_artifact_to_bugsee.rb
|
|
106
|
+
- lib/fastlane/plugin/bugsee/actions/upload_mapping_to_bugsee.rb
|
|
106
107
|
- lib/fastlane/plugin/bugsee/actions/upload_symbols_to_bugsee.rb
|
|
108
|
+
- lib/fastlane/plugin/bugsee/helper/bugsee_handshake.rb
|
|
107
109
|
- lib/fastlane/plugin/bugsee/helper/bugsee_helper.rb
|
|
110
|
+
- lib/fastlane/plugin/bugsee/helper/bugsee_uuid.rb
|
|
108
111
|
- lib/fastlane/plugin/bugsee/version.rb
|
|
109
112
|
homepage: https://github.com/bugsee/fastlane-plugin-bugsee
|
|
110
113
|
licenses:
|
|
111
114
|
- MIT
|
|
112
115
|
metadata: {}
|
|
113
|
-
post_install_message:
|
|
114
116
|
rdoc_options: []
|
|
115
117
|
require_paths:
|
|
116
118
|
- lib
|
|
@@ -125,9 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
127
|
- !ruby/object:Gem::Version
|
|
126
128
|
version: '0'
|
|
127
129
|
requirements: []
|
|
128
|
-
|
|
129
|
-
rubygems_version: 2.5.2.3
|
|
130
|
-
signing_key:
|
|
130
|
+
rubygems_version: 3.6.2
|
|
131
131
|
specification_version: 4
|
|
132
132
|
summary: Bugsee symbols uploader
|
|
133
133
|
test_files: []
|