Class: Fastlane::Actions::UploadToApkgoAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb

Overview

upload_to_apkgo publishes an APK to Chinese app stores through the apkgo cloud Open API. Stores must be pre-configured (credentials + app bindings) in the apkgo cloud dashboard; this action targets them by name.

Constant Summary collapse

DEFAULT_HOST =
"https://apkgo.baici.tech".freeze

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



225
226
227
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 225

def self.authors
  ["KevinGong2013"]
end

.available_optionsObject



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
176
177
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
203
204
205
206
207
208
209
210
211
212
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 137

def self.available_options
  [
    FastlaneCore::ConfigItem.new(key: :api_key,
                                 env_name: "APKGO_API_KEY",
                                 description: "apkgo cloud Open API key (apkgo_…), needs the `upload` permission",
                                 sensitive: true,
                                 verify_block: proc do |value|
                                   UI.user_error!("apkgo: api_key 不能为空") if value.to_s.empty?
                                   UI.user_error!("apkgo: api_key 应以 apkgo_ 开头") unless value.start_with?("apkgo_")
                                 end),
    FastlaneCore::ConfigItem.new(key: :apk,
                                 env_name: "APKGO_APK",
                                 description: "Path to the APK to upload",
                                 default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
                                 default_value_dynamic: true,
                                 verify_block: proc do |value|
                                   UI.user_error!("apkgo: 找不到 APK 文件: #{value}") unless File.exist?(value.to_s)
                                 end),
    FastlaneCore::ConfigItem.new(key: :host,
                                 env_name: "APKGO_HOST",
                                 description: "apkgo cloud base URL",
                                 default_value: DEFAULT_HOST),
    FastlaneCore::ConfigItem.new(key: :package_name,
                                 env_name: "APKGO_PACKAGE_NAME",
                                 description: "App package name (required unless app_id is set)",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :app_id,
                                 env_name: "APKGO_APP_ID",
                                 description: "apkgo cloud app UUID (alternative to package_name)",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :app_name,
                                 env_name: "APKGO_APP_NAME",
                                 description: "Display name when auto-creating the app",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :version_code,
                                 env_name: "APKGO_VERSION_CODE",
                                 description: "versionCode (informational; the worker re-parses the APK)",
                                 optional: true,
                                 type: Integer),
    FastlaneCore::ConfigItem.new(key: :version_name,
                                 env_name: "APKGO_VERSION_NAME",
                                 description: "versionName (informational)",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :release_notes,
                                 env_name: "APKGO_RELEASE_NOTES",
                                 description: "Release notes / 更新日志 passed to each store",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :release_time,
                                 env_name: "APKGO_RELEASE_TIME",
                                 description: "Scheduled release time (定时发布), RFC3339 in the future e.g. 2026-06-20T10:00:00+08:00",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :stores,
                                 env_name: "APKGO_STORES",
                                 description: "Target store names, e.g. [\"huawei\",\"xiaomi\"]. Empty = all bound stores",
                                 optional: true,
                                 type: Array),
    FastlaneCore::ConfigItem.new(key: :wait,
                                 env_name: "APKGO_WAIT",
                                 description: "Poll until the job finishes and fail the lane if any store fails",
                                 optional: true,
                                 default_value: true,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :poll_interval,
                                 env_name: "APKGO_POLL_INTERVAL",
                                 description: "Seconds between status polls",
                                 optional: true,
                                 default_value: 5,
                                 type: Integer),
    FastlaneCore::ConfigItem.new(key: :timeout,
                                 env_name: "APKGO_TIMEOUT",
                                 description: "Max seconds to wait for the job to finish",
                                 optional: true,
                                 default_value: 1800,
                                 type: Integer)
  ]
end

.build_body(params, ticket, apk) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 43

def self.build_body(params, ticket, apk)
  body = {
    object_key: ticket["object_key"],
    sha256: Helper::ApkgoHelper.sha256(apk)
  }
  body[:app_id] = params[:app_id] if params[:app_id]
  body[:package_name] = params[:package_name] if params[:package_name]
  body[:app_name] = params[:app_name] if params[:app_name]
  body[:version_code] = params[:version_code] if params[:version_code]
  body[:version_name] = params[:version_name] if params[:version_name]
  body[:release_notes] = params[:release_notes] if params[:release_notes]
  body[:release_time] = params[:release_time] if params[:release_time]
  body[:target_stores] = params[:stores] if params[:stores] && !params[:stores].empty?
  body
end

.categoryObject



246
247
248
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 246

def self.category
  :production
end

.descriptionObject



125
126
127
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 125

def self.description
  "Publish an APK to Chinese app stores via the apkgo cloud Open API"
end

.detailsObject



129
130
131
132
133
134
135
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 129

def self.details
  "Uploads an APK to apkgo cloud, which fans it out to the Android app " \
    "stores you have configured (Huawei, Xiaomi, OPPO, vivo, Honor, " \
    "Tencent, Samsung, Google Play, pgyer, fir …). Store credentials and " \
    "app bindings are managed in the apkgo cloud dashboard; this action " \
    "targets them by store name. See https://github.com/KevinGong2013/fastlane-plugin-apkgo"
end

.example_codeObject



233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 233

def self.example_code
  [
    'upload_to_apkgo(
      api_key: ENV["APKGO_API_KEY"],
      apk: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
      package_name: "com.example.myapp",
      version_name: "1.2.3",
      release_notes: "Bug fixes and improvements",
      stores: ["huawei", "xiaomi", "oppo"]
    )'
  ]
end

.finish(job) ⇒ Object

finish reports each store result and fails the lane if any store failed.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 95

def self.finish(job)
  results = job["results"] || []
  failed = []
  results.each do |r|
    if r["success"]
      review = r["review_state"] ? " (审核: #{r['review_state']})" : ""
      UI.success("#{r['store_name']}#{review}")
    else
      failed << r
      UI.error("#{r['store_name']}: #{r['error']}")
    end
  end

  if job["status"] != "completed" || !failed.empty?
    names = failed.map { |r| r["store_name"] }.join(", ")
    UI.user_error!("apkgo: 任务 #{job['status']},失败商店: #{names.empty? ? job['status'] : names}")
  end

  UI.success("apkgo: 全部商店上传成功 🎉")
  job
end

.is_supported?(platform) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 229

def self.is_supported?(platform)
  platform == :android
end

.log_progress(job) ⇒ Object



85
86
87
88
89
90
91
92
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 85

def self.log_progress(job)
  (job["progress"] || {}).each do |store, p|
    next unless p["bytes_total"].to_i.positive?

    pct = (100.0 * p["bytes_done"].to_i / p["bytes_total"].to_i).round
    UI.message("  #{store}: #{p['phase']} #{pct}%")
  end
end

.outputObject



214
215
216
217
218
219
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 214

def self.output
  [
    ["APKGO_JOB_ID", "The created upload job id"],
    ["APKGO_JOB", "The full job object (with per-store results)"]
  ]
end

.poll(client, job_id, params) ⇒ Object

poll waits for the job to reach a terminal state, surfacing per-store outcomes. Raises if any store failed so the lane fails loudly in CI.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 61

def self.poll(client, job_id, params)
  interval = params[:poll_interval]
  deadline = Time.now + params[:timeout]
  last_status = nil

  loop do
    job = client.get_job(job_id)
    status = job["status"]
    if status != last_status
      UI.message("apkgo: 任务状态 #{status}")
      last_status = status
    end
    log_progress(job)

    if Helper::ApkgoClient::TERMINAL_STATUSES.include?(status)
      Actions.lane_context[SharedValues::APKGO_JOB] = job
      return finish(job)
    end

    UI.user_error!("apkgo: 轮询超时(#{params[:timeout]}s),任务 #{job_id} 仍未完成。可稍后用 dashboard 查看。") if Time.now > deadline
    sleep(interval)
  end
end

.return_valueObject



221
222
223
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 221

def self.return_value
  "The final job hash, including per-store results when `wait` is true"
end

.run(params) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 17

def self.run(params)
  apk = params[:apk]
  UI.user_error!("apkgo: 找不到 APK 文件: #{apk}") unless apk && File.exist?(apk)

  client = Helper::ApkgoClient.new(host: params[:host], api_key: params[:api_key])

  UI.message("apkgo: 申请上传凭证 …")
  ticket = client.request_ticket(File.basename(apk))

  UI.message("apkgo: 上传 APK 到对象存储 (provider=#{ticket['provider']}) …")
  client.upload_to_storage(ticket, apk)

  body = build_body(params, ticket, apk)
  UI.message("apkgo: 创建上传任务 …")
  job = client.create_job(body)
  job_id = job["id"]

  Actions.lane_context[SharedValues::APKGO_JOB_ID] = job_id
  Actions.lane_context[SharedValues::APKGO_JOB] = job
  UI.success("apkgo: 任务已创建 #{job_id} → 目标商店 #{target_names(job)}")

  return job unless params[:wait]

  poll(client, job_id, params)
end

.target_names(job) ⇒ Object



117
118
119
# File 'lib/fastlane/plugin/apkgo/actions/upload_to_apkgo_action.rb', line 117

def self.target_names(job)
  (job["target_stores"] || []).map { |t| t["store_name"] }.join(", ")
end