Class: Mysigner::Upload::PlayStoreUploader

Inherits:
Object
  • Object
show all
Defined in:
lib/mysigner/upload/play_store_uploader.rb

Defined Under Namespace

Classes: CredentialsError, MissingLocalCredentialsError, PartialUploadError, TrackError, UploadError

Constant Summary collapse

VALID_TRACKS =
%w[internal alpha beta production].freeze
SCOPE =
'https://www.googleapis.com/auth/androidpublisher'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(aab_path:, package_name:, access_token: nil, local_only: false, play_creds: nil) ⇒ PlayStoreUploader

Phase 0: accepts a short-lived OAuth2 access_token (minted server-side from the customer’s service-account JSON). The JSON no longer leaves the server. google-api-ruby-client accepts a bare string for authorization= and sends it as ‘Authorization: Bearer <token>`.

mysigner-43: when ‘local_only: true`, `access_token` is optional —the uploader mints one locally from Keychain-backed SA-JSON. The SA-JSON never leaves the user’s machine, and no MySigner server credential endpoints are contacted.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/mysigner/upload/play_store_uploader.rb', line 83

def initialize(aab_path:, package_name:, access_token: nil, local_only: false, play_creds: nil)
  @aab_path = File.expand_path(aab_path)
  @access_token = access_token
  @package_name = package_name
  @local_only = local_only
  # mysigner-22 Phase 5 — pre-resolved PlayCreds Struct from the
  # CredentialResolver cascade. When nil (legacy / unit tests), we fall
  # back to the resolver with default args (Keychain only) inside
  # local_access_token — preserving existing spec invariants.
  @play_creds = play_creds

  if @local_only
    # Mint immediately so missing-credentials errors surface at
    # construction time (same DX as the server path's
    # CredentialsError) rather than mid-upload.
    @access_token = local_access_token
  elsif @access_token.nil? || @access_token.to_s.empty?
    raise CredentialsError, 'access_token is required'
  end

  validate_aab!
  setup_google_client!
end

Class Method Details

.fetch_highest_version_code(package_name:, access_token:) ⇒ Object

mysigner-22 follow-up — pre-check the user’s project versionCode against what’s already on Google Play in local-only mode, where the MySigner server’s ‘highest_version_code` lookup is bypassed. The cheapest authenticated way to ask Google “what’s already there” is to insert an edit, list all uploaded bundles (which carry their versionCode), and discard the edit. Inserting an edit is free and has no side effect when never committed.

Returns the maximum versionCode across all bundles (Integer), or nil when the app has no bundles yet (very first upload).



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/mysigner/upload/play_store_uploader.rb', line 44

def self.fetch_highest_version_code(package_name:, access_token:)
  require 'google/apis/androidpublisher_v3'

  service = Google::Apis::AndroidpublisherV3::AndroidPublisherService.new
  service.authorization = access_token

  edit = service.insert_edit(package_name, Google::Apis::AndroidpublisherV3::AppEdit.new)
  begin
    bundles_response = service.list_edit_bundles(package_name, edit.id)
    version_codes = Array(bundles_response&.bundles).map(&:version_code).compact
    return nil if version_codes.empty?

    version_codes.max
  ensure
    # Best-effort cleanup — the edit auto-expires after a week if we
    # leak one, but tidiness is cheap. Swallow errors so a transient
    # cleanup failure can't mask the real return value.
    begin
      service.delete_edit(package_name, edit.id)
    rescue StandardError
      nil
    end
  end
rescue Google::Apis::ClientError
  # We treat a lookup failure (auth issue, package-not-found) as
  # "unknown" rather than fatal — Google will still reject at upload
  # time with a useful message. This pre-check is best-effort.
  nil
end

Instance Method Details

#assign_existing_to_track!(version_code, track:, release_notes: nil, user_fraction: nil) ⇒ Object

Assign an existing version code to a track



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/mysigner/upload/play_store_uploader.rb', line 205

def assign_existing_to_track!(version_code, track:, release_notes: nil, user_fraction: nil)
  @current_track = track # Store for error messages
  begin
    edit = create_edit
    assign_to_track(edit.id, track, version_code, release_notes: release_notes, user_fraction: user_fraction)
    commit_edit(edit.id)

    {
      success: true,
      version_code: version_code,
      track: track,
      package_name: @package_name
    }
  rescue Google::Apis::ClientError => e
    error_message = parse_google_error(e)
    raise TrackError, "Failed to assign to track: #{error_message}"
  end
end

#upload!(track: 'internal', release_notes: nil, user_fraction: nil, status: nil, in_app_update_priority: nil, release_name: nil, country_targeting: nil, changes_not_sent_for_review: nil) ⇒ Hash

Upload AAB and optionally assign to a track

Parameters:

  • track (String) (defaults to: 'internal')

    Track to assign: internal, alpha, beta, production

  • release_notes (Hash) (defaults to: nil)

    Localized release notes { ‘en-US’ => ‘What's new…’ }

  • user_fraction (Float) (defaults to: nil)

    Rollout percentage (0.0-1.0) for staged rollouts

  • status (String) (defaults to: nil)

    Explicit release status: draft | inProgress | completed. Overrides the user_fraction-derived default. ‘draft` is useful for “upload, don’t release yet” flows that iOS-MANUAL users expect.

  • in_app_update_priority (Integer) (defaults to: nil)

    0–5 priority hint for in-app update flows

  • release_name (String) (defaults to: nil)

    Optional release name (defaults to AAB versionName)

  • country_targeting (Hash) (defaults to: nil)

    { countries: [‘US’,‘CA’], include_rest_of_world: false }

  • changes_not_sent_for_review (Boolean) (defaults to: nil)

    Skip submitting changes to Play review on commit

Returns:

  • (Hash)

    Upload result with version_code and track info



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/mysigner/upload/play_store_uploader.rb', line 119

def upload!(track: 'internal', release_notes: nil, user_fraction: nil,
            status: nil, in_app_update_priority: nil, release_name: nil,
            country_targeting: nil, changes_not_sent_for_review: nil)
  @current_track = track # Store for error messages
  say_uploading(track)

  version_code = nil

  begin
    # 1. Create an edit
    edit = create_edit

    # 2. Upload the AAB
    bundle = upload_bundle(edit.id)
    version_code = bundle.version_code

    say_upload_success(version_code)

    # 3. Assign to track with release
    if track
      assign_to_track(
        edit.id, track, version_code,
        release_notes: release_notes,
        user_fraction: user_fraction,
        status: status,
        in_app_update_priority: in_app_update_priority,
        release_name: release_name,
        country_targeting: country_targeting
      )
    end

    # 4. Commit the edit
    commit_edit(edit.id, changes_not_sent_for_review: changes_not_sent_for_review)

    say_success(track, version_code)

    {
      success: true,
      version_code: version_code,
      track: track,
      package_name: @package_name
    }
  rescue Google::Apis::ClientError => e
    error_message = parse_google_error(e)
    # If AAB was uploaded, raise PartialUploadError so CLI can save the version
    raise PartialUploadError.new("Google Play API error: #{error_message}", version_code: version_code) if version_code

    raise UploadError, "Google Play API error: #{error_message}"
  rescue PartialUploadError
    # Re-raise as-is
    raise
  rescue StandardError => e
    raise PartialUploadError.new("Upload failed: #{e.message}", version_code: version_code) if version_code

    raise UploadError, "Upload failed: #{e.message}"
  end
end

#upload_bundle_only!Object

Upload AAB only (without assigning to track)



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/mysigner/upload/play_store_uploader.rb', line 178

def upload_bundle_only!
  say_uploading(nil)

  begin
    edit = create_edit
    bundle = upload_bundle(edit.id)
    version_code = bundle.version_code

    say_upload_success(version_code)

    # Don't assign to track, just commit
    commit_edit(edit.id)

    {
      success: true,
      version_code: version_code,
      package_name: @package_name
    }
  rescue Google::Apis::ClientError => e
    error_message = parse_google_error(e)
    raise UploadError, "Google Play API error: #{error_message}"
  rescue StandardError => e
    raise UploadError, "Upload failed: #{e.message}"
  end
end