mysigner 0.1.2 ā 0.1.4
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 +4 -4
- data/.githooks/pre-commit +15 -0
- data/.githooks/pre-push +21 -0
- data/.github/workflows/ci.yml +29 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +126 -0
- data/CHANGELOG.md +96 -0
- data/Gemfile +5 -3
- data/Gemfile.lock +38 -8
- data/README.md +14 -16
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/setup +3 -0
- data/certificate_.cer +0 -0
- data/exe/mysigner +19 -2
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +83 -63
- data/lib/mysigner/build/android_parser.rb +33 -40
- data/lib/mysigner/build/configurator.rb +17 -16
- data/lib/mysigner/build/detector.rb +39 -50
- data/lib/mysigner/build/error_analyzer.rb +70 -68
- data/lib/mysigner/build/executor.rb +30 -37
- data/lib/mysigner/build/parser.rb +18 -18
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +771 -764
- data/lib/mysigner/cli/build_commands.rb +962 -796
- data/lib/mysigner/cli/concerns/actionable_suggestions.rb +208 -154
- data/lib/mysigner/cli/concerns/api_helpers.rb +46 -54
- data/lib/mysigner/cli/concerns/error_handlers.rb +247 -237
- data/lib/mysigner/cli/concerns/helpers.rb +44 -1
- data/lib/mysigner/cli/diagnostic_commands.rb +667 -636
- data/lib/mysigner/cli/resource_commands.rb +1153 -985
- data/lib/mysigner/cli/validate_commands.rb +25 -25
- data/lib/mysigner/cli.rb +11 -1
- data/lib/mysigner/client.rb +27 -19
- data/lib/mysigner/config.rb +161 -60
- data/lib/mysigner/export/exporter.rb +38 -37
- data/lib/mysigner/signing/certificate_checker.rb +18 -23
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +81 -61
- data/lib/mysigner/signing/validator.rb +38 -40
- data/lib/mysigner/signing/wizard.rb +329 -342
- data/lib/mysigner/upload/app_store_automation.rb +96 -49
- data/lib/mysigner/upload/app_store_submission.rb +87 -92
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +164 -144
- data/lib/mysigner/upload/uploader.rb +136 -115
- data/lib/mysigner/version.rb +3 -1
- data/lib/mysigner.rb +13 -11
- data/mysigner.gemspec +36 -33
- data/profile_.mobileprovision +0 -0
- data/test_manual.rb +37 -36
- metadata +44 -17
- data/.DS_Store +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Mysigner
|
|
8
|
+
module Upload
|
|
9
|
+
class AscRestUploader
|
|
10
|
+
# Raised when Apple rejects the /v1/buildUploads POST with a 409
|
|
11
|
+
# (duplicate CFBundleVersion). Callers should translate this into a
|
|
12
|
+
# "bump your build number" hint rather than a generic "Unexpected error".
|
|
13
|
+
class BuildVersionConflictError < StandardError; end
|
|
14
|
+
|
|
15
|
+
TERMINAL_APPLE_STATES = %w[COMPLETE FAILED INVALIDATED].freeze
|
|
16
|
+
POLL_INTERVAL = 10
|
|
17
|
+
POLL_TIMEOUT = 600
|
|
18
|
+
CHUNK_RETRIES = 2
|
|
19
|
+
|
|
20
|
+
def initialize(client:, organization_id:, ipa_path:, apple_app_id:,
|
|
21
|
+
cf_bundle_version:, cf_bundle_short_version_string:,
|
|
22
|
+
platform: 'IOS', poll_interval: POLL_INTERVAL, poll_timeout: POLL_TIMEOUT)
|
|
23
|
+
@client = client
|
|
24
|
+
@org_id = organization_id
|
|
25
|
+
@ipa_path = ipa_path
|
|
26
|
+
@apple_app_id = apple_app_id
|
|
27
|
+
@cf_bundle_version = cf_bundle_version
|
|
28
|
+
@cf_bundle_short_version_string = cf_bundle_short_version_string
|
|
29
|
+
@platform = platform
|
|
30
|
+
@poll_interval = poll_interval
|
|
31
|
+
@poll_timeout = poll_timeout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
begin
|
|
36
|
+
resp = @client.post(
|
|
37
|
+
"/api/v1/organizations/#{@org_id}/builds/asc_upload",
|
|
38
|
+
body: {
|
|
39
|
+
apple_app_id: @apple_app_id,
|
|
40
|
+
cf_bundle_version: @cf_bundle_version,
|
|
41
|
+
cf_bundle_short_version_string: @cf_bundle_short_version_string,
|
|
42
|
+
platform: @platform,
|
|
43
|
+
file_name: File.basename(@ipa_path),
|
|
44
|
+
file_size: File.size(@ipa_path)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
# Apple returns 409 from /v1/buildUploads when a build with the
|
|
49
|
+
# same CFBundleVersion already exists for this app. Surface a
|
|
50
|
+
# useful message instead of letting the caller print the raw
|
|
51
|
+
# "ASC /v1/buildUploads returned 409" string.
|
|
52
|
+
if e.message =~ /\b(409|buildUploads returned 409|duplicate)/i
|
|
53
|
+
raise BuildVersionConflictError,
|
|
54
|
+
"Apple refused the upload: build #{@cf_bundle_version} already exists for this app (CFBundleVersion must be unique). " \
|
|
55
|
+
'Bump CFBundleVersion in your Xcode project and re-archive, then retry.'
|
|
56
|
+
end
|
|
57
|
+
raise
|
|
58
|
+
end
|
|
59
|
+
data = resp[:data]
|
|
60
|
+
build_upload_id = data['build_upload_id']
|
|
61
|
+
ops = data['upload_operations']
|
|
62
|
+
|
|
63
|
+
File.open(@ipa_path, 'rb') do |f|
|
|
64
|
+
ops.each { |op| put_chunk_with_retry(f, op) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
md5 = Digest::MD5.file(@ipa_path).hexdigest
|
|
68
|
+
sha = Digest::SHA256.file(@ipa_path).hexdigest
|
|
69
|
+
|
|
70
|
+
@client.patch(
|
|
71
|
+
"/api/v1/organizations/#{@org_id}/builds/asc_upload/#{build_upload_id}",
|
|
72
|
+
body: { uploaded: true, source_file_checksums: { md5: md5, sha256: sha } }
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
final = poll_until_terminal(build_upload_id)
|
|
76
|
+
{ build_upload_id: build_upload_id, final_state: final }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def put_chunk_with_retry(file, operation)
|
|
82
|
+
# Defense-in-depth: Apple's signed URLs are always https. If the
|
|
83
|
+
# server ever returns an http:// URL, refuse to PUT the chunk ā that
|
|
84
|
+
# would leak the .ipa bytes (and auth headers) in clear text.
|
|
85
|
+
scheme = URI.parse(operation['url'].to_s).scheme
|
|
86
|
+
raise "refusing non-https upload URL (scheme=#{scheme.inspect})" unless scheme == 'https'
|
|
87
|
+
|
|
88
|
+
file.seek(operation['offset'])
|
|
89
|
+
bytes = file.read(operation['length'])
|
|
90
|
+
attempts = 0
|
|
91
|
+
begin
|
|
92
|
+
conn = Faraday.new { |f| f.adapter Faraday.default_adapter }
|
|
93
|
+
resp = conn.public_send(operation['method'].downcase) do |req|
|
|
94
|
+
req.url operation['url']
|
|
95
|
+
(operation['requestHeaders'] || []).each { |h| req.headers[h['name']] = h['value'] }
|
|
96
|
+
req.body = bytes
|
|
97
|
+
end
|
|
98
|
+
raise "chunk PUT failed #{resp.status}" unless resp.status.between?(200, 299)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
attempts += 1
|
|
101
|
+
retry if attempts <= CHUNK_RETRIES
|
|
102
|
+
raise
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def poll_until_terminal(build_upload_id)
|
|
107
|
+
deadline = Time.now + @poll_timeout
|
|
108
|
+
loop do
|
|
109
|
+
resp = @client.get("/api/v1/organizations/#{@org_id}/builds/asc_upload/#{build_upload_id}")
|
|
110
|
+
state = resp[:data]['apple_state']
|
|
111
|
+
return state if TERMINAL_APPLE_STATES.include?(state)
|
|
112
|
+
return 'TIMEOUT' if Time.now > deadline
|
|
113
|
+
|
|
114
|
+
sleep @poll_interval
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'json'
|
|
2
|
-
require 'stringio'
|
|
3
4
|
|
|
4
5
|
module Mysigner
|
|
5
6
|
module Upload
|
|
@@ -7,12 +8,12 @@ module Mysigner
|
|
|
7
8
|
class UploadError < Mysigner::Error; end
|
|
8
9
|
class CredentialsError < UploadError; end
|
|
9
10
|
class TrackError < UploadError; end
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
# Special error for when AAB uploaded but track assignment failed
|
|
12
13
|
# This carries the version_code so it can be saved to prevent conflicts
|
|
13
14
|
class PartialUploadError < UploadError
|
|
14
15
|
attr_reader :version_code
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
def initialize(message, version_code:)
|
|
17
18
|
super(message)
|
|
18
19
|
@version_code = version_code
|
|
@@ -20,13 +21,19 @@ module Mysigner
|
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
VALID_TRACKS = %w[internal alpha beta production].freeze
|
|
23
|
-
SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
|
|
24
|
+
SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
# Phase 0: accepts a short-lived OAuth2 access_token (minted server-side
|
|
27
|
+
# from the customer's service-account JSON). The JSON no longer leaves
|
|
28
|
+
# the server. google-api-ruby-client accepts a bare string for
|
|
29
|
+
# authorization= and sends it as `Authorization: Bearer <token>`.
|
|
30
|
+
def initialize(aab_path:, access_token:, package_name:)
|
|
26
31
|
@aab_path = File.expand_path(aab_path)
|
|
27
|
-
@
|
|
32
|
+
@access_token = access_token
|
|
28
33
|
@package_name = package_name
|
|
29
34
|
|
|
35
|
+
raise CredentialsError, 'access_token is required' if @access_token.nil? || @access_token.to_s.empty?
|
|
36
|
+
|
|
30
37
|
validate_aab!
|
|
31
38
|
setup_google_client!
|
|
32
39
|
end
|
|
@@ -35,13 +42,22 @@ module Mysigner
|
|
|
35
42
|
# @param track [String] Track to assign: internal, alpha, beta, production
|
|
36
43
|
# @param release_notes [Hash] Localized release notes { 'en-US' => 'What\'s new...' }
|
|
37
44
|
# @param user_fraction [Float] Rollout percentage (0.0-1.0) for staged rollouts
|
|
45
|
+
# @param status [String] Explicit release status: draft | inProgress | completed.
|
|
46
|
+
# Overrides the user_fraction-derived default. `draft` is useful for
|
|
47
|
+
# "upload, don't release yet" flows that iOS-MANUAL users expect.
|
|
48
|
+
# @param in_app_update_priority [Integer] 0ā5 priority hint for in-app update flows
|
|
49
|
+
# @param release_name [String] Optional release name (defaults to AAB versionName)
|
|
50
|
+
# @param country_targeting [Hash] { countries: ['US','CA'], include_rest_of_world: false }
|
|
51
|
+
# @param changes_not_sent_for_review [Boolean] Skip submitting changes to Play review on commit
|
|
38
52
|
# @return [Hash] Upload result with version_code and track info
|
|
39
|
-
def upload!(track: 'internal', release_notes: nil, user_fraction: nil
|
|
40
|
-
|
|
53
|
+
def upload!(track: 'internal', release_notes: nil, user_fraction: nil,
|
|
54
|
+
status: nil, in_app_update_priority: nil, release_name: nil,
|
|
55
|
+
country_targeting: nil, changes_not_sent_for_review: nil)
|
|
56
|
+
@current_track = track # Store for error messages
|
|
41
57
|
say_uploading(track)
|
|
42
58
|
|
|
43
59
|
version_code = nil
|
|
44
|
-
|
|
60
|
+
|
|
45
61
|
begin
|
|
46
62
|
# 1. Create an edit
|
|
47
63
|
edit = create_edit
|
|
@@ -54,11 +70,19 @@ module Mysigner
|
|
|
54
70
|
|
|
55
71
|
# 3. Assign to track with release
|
|
56
72
|
if track
|
|
57
|
-
assign_to_track(
|
|
73
|
+
assign_to_track(
|
|
74
|
+
edit.id, track, version_code,
|
|
75
|
+
release_notes: release_notes,
|
|
76
|
+
user_fraction: user_fraction,
|
|
77
|
+
status: status,
|
|
78
|
+
in_app_update_priority: in_app_update_priority,
|
|
79
|
+
release_name: release_name,
|
|
80
|
+
country_targeting: country_targeting
|
|
81
|
+
)
|
|
58
82
|
end
|
|
59
83
|
|
|
60
84
|
# 4. Commit the edit
|
|
61
|
-
commit_edit(edit.id)
|
|
85
|
+
commit_edit(edit.id, changes_not_sent_for_review: changes_not_sent_for_review)
|
|
62
86
|
|
|
63
87
|
say_success(track, version_code)
|
|
64
88
|
|
|
@@ -71,20 +95,16 @@ module Mysigner
|
|
|
71
95
|
rescue Google::Apis::ClientError => e
|
|
72
96
|
error_message = parse_google_error(e)
|
|
73
97
|
# If AAB was uploaded, raise PartialUploadError so CLI can save the version
|
|
74
|
-
if version_code
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
raise UploadError, "Google Play API error: #{error_message}"
|
|
78
|
-
end
|
|
98
|
+
raise PartialUploadError.new("Google Play API error: #{error_message}", version_code: version_code) if version_code
|
|
99
|
+
|
|
100
|
+
raise UploadError, "Google Play API error: #{error_message}"
|
|
79
101
|
rescue PartialUploadError
|
|
80
102
|
# Re-raise as-is
|
|
81
103
|
raise
|
|
82
|
-
rescue => e
|
|
83
|
-
if version_code
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
raise UploadError, "Upload failed: #{e.message}"
|
|
87
|
-
end
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
raise PartialUploadError.new("Upload failed: #{e.message}", version_code: version_code) if version_code
|
|
106
|
+
|
|
107
|
+
raise UploadError, "Upload failed: #{e.message}"
|
|
88
108
|
end
|
|
89
109
|
end
|
|
90
110
|
|
|
@@ -110,14 +130,14 @@ module Mysigner
|
|
|
110
130
|
rescue Google::Apis::ClientError => e
|
|
111
131
|
error_message = parse_google_error(e)
|
|
112
132
|
raise UploadError, "Google Play API error: #{error_message}"
|
|
113
|
-
rescue => e
|
|
133
|
+
rescue StandardError => e
|
|
114
134
|
raise UploadError, "Upload failed: #{e.message}"
|
|
115
135
|
end
|
|
116
136
|
end
|
|
117
137
|
|
|
118
138
|
# Assign an existing version code to a track
|
|
119
139
|
def assign_existing_to_track!(version_code, track:, release_notes: nil, user_fraction: nil)
|
|
120
|
-
@current_track = track
|
|
140
|
+
@current_track = track # Store for error messages
|
|
121
141
|
begin
|
|
122
142
|
edit = create_edit
|
|
123
143
|
assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction)
|
|
@@ -138,55 +158,36 @@ module Mysigner
|
|
|
138
158
|
private
|
|
139
159
|
|
|
140
160
|
def validate_aab!
|
|
141
|
-
unless File.exist?(@aab_path)
|
|
142
|
-
raise UploadError, "AAB file not found: #{@aab_path}"
|
|
143
|
-
end
|
|
161
|
+
raise UploadError, "AAB file not found: #{@aab_path}" unless File.exist?(@aab_path)
|
|
144
162
|
|
|
145
|
-
unless @aab_path.end_with?('.aab')
|
|
146
|
-
raise UploadError, "Invalid file type: #{@aab_path} (must be .aab)"
|
|
147
|
-
end
|
|
163
|
+
raise UploadError, "Invalid file type: #{@aab_path} (must be .aab)" unless @aab_path.end_with?('.aab')
|
|
148
164
|
|
|
149
165
|
file_size = File.size(@aab_path)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
return unless file_size < 10_000
|
|
167
|
+
|
|
168
|
+
raise UploadError, "AAB file seems too small: #{file_size} bytes (possible corruption)"
|
|
153
169
|
end
|
|
154
170
|
|
|
155
171
|
def setup_google_client!
|
|
156
|
-
require 'googleauth'
|
|
157
172
|
require 'google/apis/androidpublisher_v3'
|
|
158
173
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
@credentials_data = JSON.parse(@service_account_json)
|
|
162
|
-
rescue JSON::ParserError => e
|
|
163
|
-
raise CredentialsError, "Invalid service account JSON: #{e.message}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Build authorization
|
|
167
|
-
@auth = Google::Auth::ServiceAccountCredentials.make_creds(
|
|
168
|
-
json_key_io: StringIO.new(@service_account_json),
|
|
169
|
-
scope: SCOPE
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Create service
|
|
174
|
+
# Create service with the bare bearer token. google-api-ruby-client
|
|
175
|
+
# sends a string authorization as `Authorization: Bearer <string>`.
|
|
173
176
|
@service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
|
|
174
|
-
@service.authorization = @
|
|
177
|
+
@service.authorization = @access_token
|
|
175
178
|
@service.client_options.open_timeout_sec = 30
|
|
176
|
-
@service.client_options.read_timeout_sec = 300
|
|
179
|
+
@service.client_options.read_timeout_sec = 300 # Large file uploads need time
|
|
177
180
|
@service.request_options.retries = 3
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
raise CredentialsError, "Google API client not installed. Run: gem install google-api-client"
|
|
181
|
+
rescue LoadError
|
|
182
|
+
raise CredentialsError, 'Google API client not installed. Run: gem install google-api-client'
|
|
181
183
|
end
|
|
182
184
|
|
|
183
185
|
def create_edit
|
|
184
186
|
edit = Google::Apis::AndroidpublisherV3::AppEdit.new
|
|
185
187
|
@service.insert_edit(@package_name, edit)
|
|
186
188
|
rescue Google::Apis::ClientError => e
|
|
187
|
-
if e.message.include?(
|
|
188
|
-
|
|
189
|
-
end
|
|
189
|
+
raise UploadError, first_upload_error_message if e.message.include?('Package not found') || e.status_code == 404
|
|
190
|
+
|
|
190
191
|
raise
|
|
191
192
|
end
|
|
192
193
|
|
|
@@ -209,39 +210,50 @@ module Mysigner
|
|
|
209
210
|
|
|
210
211
|
def upload_bundle(edit_id)
|
|
211
212
|
puts "š¦ Uploading AAB (#{format_bytes(File.size(@aab_path))})..."
|
|
212
|
-
puts
|
|
213
|
+
puts ''
|
|
213
214
|
|
|
214
215
|
begin
|
|
215
|
-
|
|
216
|
+
@service.upload_edit_bundle(
|
|
216
217
|
@package_name,
|
|
217
218
|
edit_id,
|
|
218
219
|
upload_source: @aab_path,
|
|
219
220
|
content_type: 'application/octet-stream'
|
|
220
221
|
)
|
|
221
|
-
result
|
|
222
222
|
rescue Google::Apis::ClientError => e
|
|
223
223
|
error_msg = parse_google_error(e)
|
|
224
224
|
raise UploadError, "Bundle upload failed: #{error_msg}"
|
|
225
|
-
rescue => e
|
|
225
|
+
rescue StandardError => e
|
|
226
226
|
raise UploadError, "Bundle upload failed: #{e.message}"
|
|
227
227
|
end
|
|
228
228
|
end
|
|
229
229
|
|
|
230
|
-
def assign_to_track(edit_id, track, version_code, release_notes: nil, user_fraction: nil
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
230
|
+
def assign_to_track(edit_id, track, version_code, release_notes: nil, user_fraction: nil,
|
|
231
|
+
status: nil, in_app_update_priority: nil, release_name: nil,
|
|
232
|
+
country_targeting: nil)
|
|
233
|
+
raise TrackError, "Invalid track '#{track}'. Valid tracks: #{VALID_TRACKS.join(', ')}" unless VALID_TRACKS.include?(track)
|
|
234
234
|
|
|
235
235
|
puts "š Assigning to #{track} track..."
|
|
236
236
|
|
|
237
|
-
#
|
|
237
|
+
# Status precedence: explicit `status:` arg > user_fraction-derived >
|
|
238
|
+
# completed. Google Play rejects `userFraction` outside (0,1) and also
|
|
239
|
+
# rejects it when status != inProgress, so we clear it when the
|
|
240
|
+
# caller-provided status is non-rollout.
|
|
241
|
+
effective_status = if status && %w[draft inProgress halted completed].include?(status)
|
|
242
|
+
status
|
|
243
|
+
elsif user_fraction
|
|
244
|
+
'inProgress'
|
|
245
|
+
else
|
|
246
|
+
'completed'
|
|
247
|
+
end
|
|
248
|
+
|
|
238
249
|
release = Google::Apis::AndroidpublisherV3::TrackRelease.new(
|
|
239
250
|
version_codes: [version_code.to_s],
|
|
240
|
-
status:
|
|
251
|
+
status: effective_status
|
|
241
252
|
)
|
|
242
253
|
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
release.name = release_name if release_name
|
|
255
|
+
|
|
256
|
+
if release_notes&.any?
|
|
245
257
|
release.release_notes = release_notes.map do |lang, text|
|
|
246
258
|
Google::Apis::AndroidpublisherV3::LocalizedText.new(
|
|
247
259
|
language: lang,
|
|
@@ -250,12 +262,16 @@ module Mysigner
|
|
|
250
262
|
end
|
|
251
263
|
end
|
|
252
264
|
|
|
253
|
-
|
|
254
|
-
if
|
|
255
|
-
|
|
265
|
+
release.user_fraction = user_fraction if user_fraction && effective_status == 'inProgress'
|
|
266
|
+
release.in_app_update_priority = in_app_update_priority if in_app_update_priority
|
|
267
|
+
|
|
268
|
+
if country_targeting.is_a?(Hash) && country_targeting[:countries].is_a?(Array) && country_targeting[:countries].any?
|
|
269
|
+
release.country_targeting = Google::Apis::AndroidpublisherV3::CountryTargeting.new(
|
|
270
|
+
countries: country_targeting[:countries],
|
|
271
|
+
include_rest_of_world: country_targeting.fetch(:include_rest_of_world, false)
|
|
272
|
+
)
|
|
256
273
|
end
|
|
257
274
|
|
|
258
|
-
# Build track update
|
|
259
275
|
track_obj = Google::Apis::AndroidpublisherV3::Track.new(
|
|
260
276
|
track: track,
|
|
261
277
|
releases: [release]
|
|
@@ -264,104 +280,108 @@ module Mysigner
|
|
|
264
280
|
@service.update_edit_track(@package_name, edit_id, track, track_obj)
|
|
265
281
|
end
|
|
266
282
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
# Commit the edit. When `changes_not_sent_for_review` is nil (the
|
|
284
|
+
# historical default) we opt-in (true) and fall back to false if Google
|
|
285
|
+
# rejects ā some apps have Play-managed review that forbids the flag.
|
|
286
|
+
# When the caller passes an explicit boolean (from cli_defaults), we
|
|
287
|
+
# pass it through without the fallback retry.
|
|
288
|
+
def commit_edit(edit_id, changes_not_sent_for_review: nil)
|
|
289
|
+
puts 'š¾ Committing changes...'
|
|
290
|
+
|
|
291
|
+
if changes_not_sent_for_review.nil?
|
|
292
|
+
begin
|
|
293
|
+
@service.commit_edit(@package_name, edit_id, changes_not_sent_for_review: true)
|
|
294
|
+
rescue Google::Apis::ClientError => e
|
|
295
|
+
error_text = e.message.to_s
|
|
296
|
+
error_text += " #{e.body}" if e.respond_to?(:body) && e.body
|
|
297
|
+
raise unless error_text.include?('changesNotSentForReview')
|
|
298
|
+
|
|
281
299
|
@service.commit_edit(@package_name, edit_id)
|
|
282
|
-
else
|
|
283
|
-
raise
|
|
284
300
|
end
|
|
301
|
+
else
|
|
302
|
+
@service.commit_edit(@package_name, edit_id, changes_not_sent_for_review: !!changes_not_sent_for_review)
|
|
285
303
|
end
|
|
286
304
|
end
|
|
287
305
|
|
|
288
306
|
def parse_google_error(error)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
message = body&.dig('error', 'message') || error.message
|
|
296
|
-
details = body&.dig('error', 'errors')&.map { |e| e['message'] }&.join('; ')
|
|
297
|
-
full_message = details ? "#{message} (#{details})" : message
|
|
298
|
-
|
|
299
|
-
# Provide helpful context for common errors
|
|
300
|
-
case full_message.to_s.downcase
|
|
301
|
-
when /package.*not found/i, /app not found/i
|
|
302
|
-
"#{full_message}\n\nš” Make sure the package name '#{@package_name}' matches your app in Google Play Console."
|
|
303
|
-
when /not authorized/i, /permission denied/i, /forbidden/i
|
|
304
|
-
"#{full_message}\n\nš” Check that your service account has Editor or Admin access to the app in Google Play Console."
|
|
305
|
-
when /version.*code.*already/i, /already.*used/i
|
|
306
|
-
"#{full_message}\n\nš” Version code already exists. Increment versionCode in android/app/build.gradle and rebuild."
|
|
307
|
-
when /precondition.*check.*failed/i, /precondition.*failed/i
|
|
308
|
-
track_name = @current_track || "this track"
|
|
309
|
-
"#{full_message}\n\n" \
|
|
310
|
-
"š” Google Play Console requires setup before publishing to #{track_name}:\n\n" \
|
|
311
|
-
" For PRODUCTION track:\n" \
|
|
312
|
-
" ⢠Complete store listing (description, screenshots, etc.)\n" \
|
|
313
|
-
" ⢠Set content rating\n" \
|
|
314
|
-
" ⢠Configure pricing & distribution\n\n" \
|
|
315
|
-
" For BETA/ALPHA tracks:\n" \
|
|
316
|
-
" ⢠Create a closed/open testing track in Play Console\n" \
|
|
317
|
-
" ⢠Add at least one tester email\n\n" \
|
|
318
|
-
" For INTERNAL track:\n" \
|
|
319
|
-
" ⢠Add internal testers in Play Console\n\n" \
|
|
320
|
-
" ā
Your AAB was uploaded successfully!\n" \
|
|
321
|
-
" ā Go to Play Console to complete track setup, then use:\n" \
|
|
322
|
-
" mysigner submit #{track_name} --platform android --version-code VERSION"
|
|
323
|
-
when /invalid request/i
|
|
324
|
-
"#{full_message}\n\nš” Common causes:\n" \
|
|
325
|
-
" ⢠Version code not found on Google Play (must upload first)\n" \
|
|
326
|
-
" ⢠App not created in Google Play Console\n" \
|
|
327
|
-
" ⢠Service account missing permissions"
|
|
328
|
-
when /signing/i, /signature/i
|
|
329
|
-
"#{full_message}\n\nš” The AAB may not be signed with the correct key. Check your keystore matches what's registered in Play Console."
|
|
330
|
-
else
|
|
331
|
-
full_message
|
|
307
|
+
body = nil
|
|
308
|
+
if error.respond_to?(:body) && error.body
|
|
309
|
+
body = begin
|
|
310
|
+
JSON.parse(error.body)
|
|
311
|
+
rescue StandardError
|
|
312
|
+
nil
|
|
332
313
|
end
|
|
333
|
-
rescue => parse_error
|
|
334
|
-
"#{error.message} (parsing error: #{parse_error.message})"
|
|
335
314
|
end
|
|
315
|
+
|
|
316
|
+
message = body&.dig('error', 'message') || error.message
|
|
317
|
+
details = body&.dig('error', 'errors')&.map { |e| e['message'] }&.join('; ')
|
|
318
|
+
full_message = details ? "#{message} (#{details})" : message
|
|
319
|
+
|
|
320
|
+
# Provide helpful context for common errors
|
|
321
|
+
case full_message.to_s.downcase
|
|
322
|
+
when /package.*not found/i, /app not found/i
|
|
323
|
+
"#{full_message}\n\nš” Make sure the package name '#{@package_name}' matches your app in Google Play Console."
|
|
324
|
+
when /not authorized/i, /permission denied/i, /forbidden/i
|
|
325
|
+
"#{full_message}\n\nš” Check that your service account has Editor or Admin access to the app in Google Play Console."
|
|
326
|
+
when /version.*code.*already/i, /already.*used/i
|
|
327
|
+
"#{full_message}\n\nš” Version code already exists. Increment versionCode in android/app/build.gradle and rebuild."
|
|
328
|
+
when /precondition.*check.*failed/i, /precondition.*failed/i
|
|
329
|
+
track_name = @current_track || 'this track'
|
|
330
|
+
"#{full_message}\n\n" \
|
|
331
|
+
"š” Google Play Console requires setup before publishing to #{track_name}:\n\n " \
|
|
332
|
+
"For PRODUCTION track:\n " \
|
|
333
|
+
"⢠Complete store listing (description, screenshots, etc.)\n " \
|
|
334
|
+
"⢠Set content rating\n " \
|
|
335
|
+
"⢠Configure pricing & distribution\n\n " \
|
|
336
|
+
"For BETA/ALPHA tracks:\n " \
|
|
337
|
+
"⢠Create a closed/open testing track in Play Console\n " \
|
|
338
|
+
"⢠Add at least one tester email\n\n " \
|
|
339
|
+
"For INTERNAL track:\n " \
|
|
340
|
+
"⢠Add internal testers in Play Console\n\n " \
|
|
341
|
+
"ā
Your AAB was uploaded successfully!\n " \
|
|
342
|
+
"ā Go to Play Console to complete track setup, then use:\n " \
|
|
343
|
+
"mysigner submit #{track_name} --platform android --version-code VERSION"
|
|
344
|
+
when /invalid request/i
|
|
345
|
+
"#{full_message}\n\nš” Common causes:\n " \
|
|
346
|
+
"⢠Version code not found on Google Play (must upload first)\n " \
|
|
347
|
+
"⢠App not created in Google Play Console\n " \
|
|
348
|
+
'⢠Service account missing permissions'
|
|
349
|
+
when /signing/i, /signature/i
|
|
350
|
+
"#{full_message}\n\nš” The AAB may not be signed with the correct key. Check your keystore matches what's registered in Play Console."
|
|
351
|
+
else
|
|
352
|
+
full_message
|
|
353
|
+
end
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
"#{error.message} (parsing error: #{e.message})"
|
|
336
356
|
end
|
|
337
357
|
|
|
338
358
|
def say_uploading(track)
|
|
339
|
-
puts "āļø Uploading to Google Play#{
|
|
340
|
-
puts
|
|
359
|
+
puts "āļø Uploading to Google Play#{" (#{track} track)" if track}..."
|
|
360
|
+
puts ''
|
|
341
361
|
puts "AAB: #{File.basename(@aab_path)}"
|
|
342
362
|
puts "Size: #{format_bytes(File.size(@aab_path))}"
|
|
343
363
|
puts "Package: #{@package_name}"
|
|
344
364
|
puts "Track: #{track || 'none (upload only)'}"
|
|
345
|
-
puts
|
|
365
|
+
puts ''
|
|
346
366
|
end
|
|
347
367
|
|
|
348
368
|
def say_upload_success(version_code)
|
|
349
369
|
puts "ā Bundle uploaded successfully (version code: #{version_code})"
|
|
350
|
-
puts
|
|
370
|
+
puts ''
|
|
351
371
|
end
|
|
352
372
|
|
|
353
373
|
def say_success(track, version_code)
|
|
354
|
-
puts
|
|
355
|
-
puts
|
|
356
|
-
puts
|
|
357
|
-
puts
|
|
358
|
-
puts
|
|
374
|
+
puts ''
|
|
375
|
+
puts '=' * 80
|
|
376
|
+
puts 'ā Upload complete!'
|
|
377
|
+
puts '=' * 80
|
|
378
|
+
puts ''
|
|
359
379
|
puts "š Your app is now on Google Play (#{track} track)"
|
|
360
|
-
puts
|
|
380
|
+
puts ''
|
|
361
381
|
puts "Version Code: #{version_code}"
|
|
362
382
|
puts "Package: #{@package_name}"
|
|
363
383
|
puts "Track: #{track}"
|
|
364
|
-
puts
|
|
384
|
+
puts ''
|
|
365
385
|
end
|
|
366
386
|
|
|
367
387
|
def format_bytes(bytes)
|