mysigner 0.1.3 → 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/.gitignore +4 -0
- data/.rubocop_todo.yml +23 -9
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/certificate_.cer +0 -0
- data/exe/mysigner +17 -1
- data/iOS_App_Store_Profile.mobileprovision +1 -0
- data/iOS_Distribution_Certificate.cer +1 -0
- data/lib/mysigner/build/android_executor.rb +37 -11
- data/lib/mysigner/cleanup/private_keys_purger.rb +41 -0
- data/lib/mysigner/cli/auth_commands.rb +42 -18
- data/lib/mysigner/cli/build_commands.rb +307 -117
- data/lib/mysigner/cli/concerns/helpers.rb +32 -0
- data/lib/mysigner/cli/diagnostic_commands.rb +8 -1
- data/lib/mysigner/cli/resource_commands.rb +304 -114
- data/lib/mysigner/cli.rb +8 -0
- data/lib/mysigner/config.rb +68 -4
- data/lib/mysigner/export/exporter.rb +6 -1
- data/lib/mysigner/signing/gradle_signing_injector.rb +67 -0
- data/lib/mysigner/signing/keystore_manager.rb +50 -25
- data/lib/mysigner/upload/app_store_automation.rb +46 -1
- data/lib/mysigner/upload/asc_rest_uploader.rb +119 -0
- data/lib/mysigner/upload/play_store_uploader.rb +77 -40
- data/lib/mysigner/upload/uploader.rb +41 -12
- data/lib/mysigner/version.rb +1 -1
- data/profile_.mobileprovision +0 -0
- metadata +8 -2
- data/.DS_Store +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
require 'stringio'
|
|
5
4
|
|
|
6
5
|
module Mysigner
|
|
7
6
|
module Upload
|
|
@@ -24,11 +23,17 @@ module Mysigner
|
|
|
24
23
|
VALID_TRACKS = %w[internal alpha beta production].freeze
|
|
25
24
|
SCOPE = 'https://www.googleapis.com/auth/androidpublisher'
|
|
26
25
|
|
|
27
|
-
|
|
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:)
|
|
28
31
|
@aab_path = File.expand_path(aab_path)
|
|
29
|
-
@
|
|
32
|
+
@access_token = access_token
|
|
30
33
|
@package_name = package_name
|
|
31
34
|
|
|
35
|
+
raise CredentialsError, 'access_token is required' if @access_token.nil? || @access_token.to_s.empty?
|
|
36
|
+
|
|
32
37
|
validate_aab!
|
|
33
38
|
setup_google_client!
|
|
34
39
|
end
|
|
@@ -37,8 +42,17 @@ module Mysigner
|
|
|
37
42
|
# @param track [String] Track to assign: internal, alpha, beta, production
|
|
38
43
|
# @param release_notes [Hash] Localized release notes { 'en-US' => 'What\'s new...' }
|
|
39
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
|
|
40
52
|
# @return [Hash] Upload result with version_code and track info
|
|
41
|
-
def upload!(track: 'internal', release_notes: nil, user_fraction: nil
|
|
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)
|
|
42
56
|
@current_track = track # Store for error messages
|
|
43
57
|
say_uploading(track)
|
|
44
58
|
|
|
@@ -55,10 +69,20 @@ module Mysigner
|
|
|
55
69
|
say_upload_success(version_code)
|
|
56
70
|
|
|
57
71
|
# 3. Assign to track with release
|
|
58
|
-
|
|
72
|
+
if 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
|
+
)
|
|
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
|
|
|
@@ -145,25 +169,12 @@ module Mysigner
|
|
|
145
169
|
end
|
|
146
170
|
|
|
147
171
|
def setup_google_client!
|
|
148
|
-
require 'googleauth'
|
|
149
172
|
require 'google/apis/androidpublisher_v3'
|
|
150
173
|
|
|
151
|
-
#
|
|
152
|
-
|
|
153
|
-
@credentials_data = JSON.parse(@service_account_json)
|
|
154
|
-
rescue JSON::ParserError => e
|
|
155
|
-
raise CredentialsError, "Invalid service account JSON: #{e.message}"
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Build authorization
|
|
159
|
-
@auth = Google::Auth::ServiceAccountCredentials.make_creds(
|
|
160
|
-
json_key_io: StringIO.new(@service_account_json),
|
|
161
|
-
scope: SCOPE
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
# Create service
|
|
174
|
+
# Create service with the bare bearer token. google-api-ruby-client
|
|
175
|
+
# sends a string authorization as `Authorization: Bearer <string>`.
|
|
165
176
|
@service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
|
|
166
|
-
@service.authorization = @
|
|
177
|
+
@service.authorization = @access_token
|
|
167
178
|
@service.client_options.open_timeout_sec = 30
|
|
168
179
|
@service.client_options.read_timeout_sec = 300 # Large file uploads need time
|
|
169
180
|
@service.request_options.retries = 3
|
|
@@ -216,18 +227,32 @@ module Mysigner
|
|
|
216
227
|
end
|
|
217
228
|
end
|
|
218
229
|
|
|
219
|
-
def assign_to_track(edit_id, track, version_code, release_notes: nil, user_fraction: nil
|
|
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)
|
|
220
233
|
raise TrackError, "Invalid track '#{track}'. Valid tracks: #{VALID_TRACKS.join(', ')}" unless VALID_TRACKS.include?(track)
|
|
221
234
|
|
|
222
235
|
puts "🚂 Assigning to #{track} track..."
|
|
223
236
|
|
|
224
|
-
#
|
|
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
|
+
|
|
225
249
|
release = Google::Apis::AndroidpublisherV3::TrackRelease.new(
|
|
226
250
|
version_codes: [version_code.to_s],
|
|
227
|
-
status:
|
|
251
|
+
status: effective_status
|
|
228
252
|
)
|
|
229
253
|
|
|
230
|
-
|
|
254
|
+
release.name = release_name if release_name
|
|
255
|
+
|
|
231
256
|
if release_notes&.any?
|
|
232
257
|
release.release_notes = release_notes.map do |lang, text|
|
|
233
258
|
Google::Apis::AndroidpublisherV3::LocalizedText.new(
|
|
@@ -237,10 +262,16 @@ module Mysigner
|
|
|
237
262
|
end
|
|
238
263
|
end
|
|
239
264
|
|
|
240
|
-
|
|
241
|
-
release.
|
|
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
|
+
)
|
|
273
|
+
end
|
|
242
274
|
|
|
243
|
-
# Build track update
|
|
244
275
|
track_obj = Google::Apis::AndroidpublisherV3::Track.new(
|
|
245
276
|
track: track,
|
|
246
277
|
releases: [release]
|
|
@@ -249,20 +280,26 @@ module Mysigner
|
|
|
249
280
|
@service.update_edit_track(@package_name, edit_id, track, track_obj)
|
|
250
281
|
end
|
|
251
282
|
|
|
252
|
-
|
|
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)
|
|
253
289
|
puts '💾 Committing changes...'
|
|
254
|
-
begin
|
|
255
|
-
# Try with changesNotSentForReview first (for apps without managed review)
|
|
256
|
-
@service.commit_edit(@package_name, edit_id, changes_not_sent_for_review: true)
|
|
257
|
-
rescue Google::Apis::ClientError => e
|
|
258
|
-
error_text = e.message.to_s
|
|
259
|
-
# Also check body if present
|
|
260
|
-
error_text += " #{e.body}" if e.respond_to?(:body) && e.body
|
|
261
290
|
|
|
262
|
-
|
|
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')
|
|
263
298
|
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
@service.commit_edit(@package_name, edit_id)
|
|
300
|
+
end
|
|
301
|
+
else
|
|
302
|
+
@service.commit_edit(@package_name, edit_id, changes_not_sent_for_review: !!changes_not_sent_for_review)
|
|
266
303
|
end
|
|
267
304
|
end
|
|
268
305
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'English'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require 'json'
|
|
6
|
+
require 'tmpdir'
|
|
6
7
|
|
|
7
8
|
module Mysigner
|
|
8
9
|
module Upload
|
|
@@ -17,6 +18,30 @@ module Mysigner
|
|
|
17
18
|
'/usr/local/itms/bin/iTMSTransporter' # Custom installation
|
|
18
19
|
].freeze
|
|
19
20
|
|
|
21
|
+
# Parses CFBundleVersion + CFBundleShortVersionString from an .ipa
|
|
22
|
+
# (reads Info.plist from the Payload/*.app/ inside the zip).
|
|
23
|
+
# Used by the new ASC REST upload flow in build_commands.rb.
|
|
24
|
+
def self.extract_ipa_info(ipa_path)
|
|
25
|
+
require 'open3'
|
|
26
|
+
result = { cf_bundle_version: nil, cf_bundle_short_version_string: nil, bundle_id: nil }
|
|
27
|
+
Dir.mktmpdir('mysigner-ipa-inspect-') do |tmp|
|
|
28
|
+
plist_path = File.join(tmp, 'Info.plist')
|
|
29
|
+
zip_out, status = Open3.capture2e('unzip', '-p', ipa_path, 'Payload/*.app/Info.plist')
|
|
30
|
+
return result unless status.success? && !zip_out.empty?
|
|
31
|
+
|
|
32
|
+
File.binwrite(plist_path, zip_out)
|
|
33
|
+
xml_out, xml_status = Open3.capture2e('plutil', '-convert', 'xml1', '-o', '-', plist_path)
|
|
34
|
+
return result unless xml_status.success?
|
|
35
|
+
|
|
36
|
+
result[:cf_bundle_version] = xml_out[%r{<key>CFBundleVersion</key>\s*<string>([^<]+)</string>}, 1]
|
|
37
|
+
result[:cf_bundle_short_version_string] = xml_out[%r{<key>CFBundleShortVersionString</key>\s*<string>([^<]+)</string>}, 1]
|
|
38
|
+
result[:bundle_id] = xml_out[%r{<key>CFBundleIdentifier</key>\s*<string>([^<]+)</string>}, 1]
|
|
39
|
+
end
|
|
40
|
+
result
|
|
41
|
+
rescue StandardError
|
|
42
|
+
{ cf_bundle_version: nil, cf_bundle_short_version_string: nil, bundle_id: nil }
|
|
43
|
+
end
|
|
44
|
+
|
|
20
45
|
def initialize(ipa_path, api_key:, api_issuer:, private_key:)
|
|
21
46
|
@ipa_path = File.expand_path(ipa_path)
|
|
22
47
|
@api_key = api_key
|
|
@@ -56,6 +81,8 @@ module Mysigner
|
|
|
56
81
|
}
|
|
57
82
|
rescue StandardError => e
|
|
58
83
|
raise UploadError, "Upload failed: #{e.message}"
|
|
84
|
+
ensure
|
|
85
|
+
cleanup_private_key!
|
|
59
86
|
end
|
|
60
87
|
end
|
|
61
88
|
|
|
@@ -83,20 +110,22 @@ module Mysigner
|
|
|
83
110
|
end
|
|
84
111
|
|
|
85
112
|
def setup_private_key!
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@private_keys_dir = File.expand_path('~/.private_keys')
|
|
93
|
-
FileUtils.mkdir_p(@private_keys_dir)
|
|
94
|
-
|
|
113
|
+
# Phase 0: write the .p8 to a per-run tempdir (0600) and point altool
|
|
114
|
+
# there via API_PRIVATE_KEYS_DIR. #cleanup_private_key! in the upload!
|
|
115
|
+
# ensure block removes the tempdir, so the key never persists across
|
|
116
|
+
# invocations. Replaces the old ~/.private_keys/ persistent write.
|
|
117
|
+
@private_keys_dir = Dir.mktmpdir('mysigner-p8-')
|
|
95
118
|
@private_key_path = File.join(@private_keys_dir, "AuthKey_#{@api_key}.p8")
|
|
96
|
-
|
|
97
|
-
# Write the private key to disk
|
|
98
119
|
File.write(@private_key_path, @private_key)
|
|
99
|
-
File.chmod(0o600, @private_key_path)
|
|
120
|
+
File.chmod(0o600, @private_key_path)
|
|
121
|
+
ENV['API_PRIVATE_KEYS_DIR'] = @private_keys_dir
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def cleanup_private_key!
|
|
125
|
+
FileUtils.rm_rf(@private_keys_dir) if @private_keys_dir && Dir.exist?(@private_keys_dir)
|
|
126
|
+
ENV.delete('API_PRIVATE_KEYS_DIR')
|
|
127
|
+
rescue StandardError
|
|
128
|
+
# Best-effort cleanup; never raise from ensure.
|
|
100
129
|
end
|
|
101
130
|
|
|
102
131
|
def validate_ipa
|
data/lib/mysigner/version.rb
CHANGED
|
File without changes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mysigner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jurgen Leka
|
|
@@ -216,7 +216,6 @@ executables:
|
|
|
216
216
|
extensions: []
|
|
217
217
|
extra_rdoc_files: []
|
|
218
218
|
files:
|
|
219
|
-
- ".DS_Store"
|
|
220
219
|
- ".githooks/pre-commit"
|
|
221
220
|
- ".githooks/pre-push"
|
|
222
221
|
- ".github/workflows/ci.yml"
|
|
@@ -236,7 +235,10 @@ files:
|
|
|
236
235
|
- Rakefile
|
|
237
236
|
- bin/console
|
|
238
237
|
- bin/setup
|
|
238
|
+
- certificate_.cer
|
|
239
239
|
- exe/mysigner
|
|
240
|
+
- iOS_App_Store_Profile.mobileprovision
|
|
241
|
+
- iOS_Distribution_Certificate.cer
|
|
240
242
|
- lib/mysigner.rb
|
|
241
243
|
- lib/mysigner/build/android_executor.rb
|
|
242
244
|
- lib/mysigner/build/android_parser.rb
|
|
@@ -245,6 +247,7 @@ files:
|
|
|
245
247
|
- lib/mysigner/build/error_analyzer.rb
|
|
246
248
|
- lib/mysigner/build/executor.rb
|
|
247
249
|
- lib/mysigner/build/parser.rb
|
|
250
|
+
- lib/mysigner/cleanup/private_keys_purger.rb
|
|
248
251
|
- lib/mysigner/cli.rb
|
|
249
252
|
- lib/mysigner/cli/auth_commands.rb
|
|
250
253
|
- lib/mysigner/cli/build_commands.rb
|
|
@@ -259,15 +262,18 @@ files:
|
|
|
259
262
|
- lib/mysigner/config.rb
|
|
260
263
|
- lib/mysigner/export/exporter.rb
|
|
261
264
|
- lib/mysigner/signing/certificate_checker.rb
|
|
265
|
+
- lib/mysigner/signing/gradle_signing_injector.rb
|
|
262
266
|
- lib/mysigner/signing/keystore_manager.rb
|
|
263
267
|
- lib/mysigner/signing/validator.rb
|
|
264
268
|
- lib/mysigner/signing/wizard.rb
|
|
265
269
|
- lib/mysigner/upload/app_store_automation.rb
|
|
266
270
|
- lib/mysigner/upload/app_store_submission.rb
|
|
271
|
+
- lib/mysigner/upload/asc_rest_uploader.rb
|
|
267
272
|
- lib/mysigner/upload/play_store_uploader.rb
|
|
268
273
|
- lib/mysigner/upload/uploader.rb
|
|
269
274
|
- lib/mysigner/version.rb
|
|
270
275
|
- mysigner.gemspec
|
|
276
|
+
- profile_.mobileprovision
|
|
271
277
|
- test_manual.rb
|
|
272
278
|
homepage: https://mysigner.dev
|
|
273
279
|
licenses:
|
data/.DS_Store
DELETED
|
Binary file
|