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.
@@ -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
- def initialize(aab_path:, service_account_json:, package_name:)
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
- @service_account_json = service_account_json
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
- assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction) if track
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
- # Parse and validate service account JSON
152
- begin
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 = @auth
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
- # Build release
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: user_fraction ? 'inProgress' : 'completed'
251
+ status: effective_status
228
252
  )
229
253
 
230
- # Add release notes if provided
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
- # Add user fraction for staged rollouts
241
- release.user_fraction = user_fraction if user_fraction
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
- def commit_edit(edit_id)
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
- raise unless error_text.include?('changesNotSentForReview')
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
- # App has managed review enabled, commit without the flag
265
- @service.commit_edit(@package_name, edit_id)
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
- # iTMSTransporter looks for .p8 files in these locations:
87
- # - ./private_keys/AuthKey_<KEY_ID>.p8
88
- # - ~/private_keys/AuthKey_<KEY_ID>.p8
89
- # - ~/.private_keys/AuthKey_<KEY_ID>.p8
90
- # - ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
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) # Secure permissions
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mysigner
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
5
5
  end
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.3
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