fastlane-plugin-rustore_connect 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 101ef7fb018be635bc16dc3b8fe5babbac0a3317337b519cd4d686007f08bc92
4
+ data.tar.gz: 658ca30d1e25afc890fbb848d6604ce127bcf6cafa7dda0c3388741d410df7a8
5
+ SHA512:
6
+ metadata.gz: e3094f839faf30dcf8288cb54c2673204e447441f5b2e151ebe569ed444cd4f668fcc1ba7447a5e923b0fedc81a104053a48e54599e14bc2ed5993085215b403
7
+ data.tar.gz: 763bcd8362dbc47f8cc0cd1b1ae9fd2d7cd54a587b2a02f8a2439729e84dc9f78b1d7457eea7381b3b5cf01552f72d8b9c8886126e3a1268af6b4dbd8f0f11b8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Mikhail Matsera <mmatsera@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # rustore_connect plugin
2
+
3
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-rustore_connect)
4
+
5
+ ## Getting Started
6
+ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-rustore_connect`, add it to your project by running:
7
+ ```bash
8
+ fastlane add_plugin huawei_appgallery_connect
9
+ ```
10
+
11
+ ## About Fastlane Rustore Connect Plugin
12
+ **Fastlane plugin for publishing Android applications to RuStore.**
13
+ This plugin provides seamless integration with RuStore, allowing you to easily upload and manage your Android application releases directly from your CI/CD pipeline.
14
+
15
+ ## Features
16
+
17
+ * Create new draft versions or reuse existing drafts
18
+ * Upload APK or AAB files
19
+ * Submit applications for review automatically
20
+ * Supports partial and delayed publication
21
+ * Handles draft strategies: DELETE, REUSE, FAIL
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ rustore_connect(
27
+ key_id: "YOUR_KEY_ID",
28
+ private_key: "YOUR_PRIVATE_KEY",
29
+ package_name: "com.example.app",
30
+ app_name: "My App",
31
+ app_type: "GAMES",
32
+ categories: "health, news",
33
+ age_legal: "6+",
34
+ short_description: "Brief app description",
35
+ full_description: "Full app description",
36
+ whats_new: "What's new in this version",
37
+ moder_info: "Comment for moderator",
38
+ price_value: "8799",
39
+ seo_tag_ids: "tag1,tag2",
40
+ publish_type: "INSTANTLY",
41
+ publish_date_time: "2025-09-10T12:00:00+03:00",
42
+ partial_value: "50",
43
+ services_type: "Unknown",
44
+ is_main_apk: true,
45
+ apk_path: "./app-release.apk",
46
+ is_aab: false,
47
+ draft_strategy: "FAIL",
48
+ submit_for_review: true
49
+ )
50
+ ```
51
+
52
+ ## Available Options
53
+
54
+ | Option | Description | Type | Default | Required |
55
+ | ------------------- | --------------------------------------------------- | ------- | ----------- | -------- |
56
+ | `key_id` | Key ID for RuStore API | String | — | ✅ |
57
+ | `private_key` | Private key for RuStore API | String | — | ✅ |
58
+ | `package_name` | App package name (example: `com.example.app`) | String | — | ✅ |
59
+ | `app_name` | Application name | String | — | ❌ |
60
+ | `app_type` | Type of application (example: `GAMES`) | String | — | ❌ |
61
+ | `categories` | Version categories (example: `"health, news"`) | String | — | ❌ |
62
+ | `age_legal` | Age rating (example: `6+`) | String | — | ❌ |
63
+ | `short_description` | Brief description (max 80 chars) | String | — | ❌ |
64
+ | `full_description` | Full description (max 4000 chars) | String | — | ❌ |
65
+ | `whats_new` | What's new | String | — | ❌ |
66
+ | `moder_info` | Comment for moderator (max 180 chars) | String | — | ❌ |
67
+ | `price_value` | App price in kopecks (example: `8799`) | String | — | ❌ |
68
+ | `seo_tag_ids` | SEO tags | String | — | ❌ |
69
+ | `publish_type` | Publication type: `MANUAL`, `INSTANTLY`, `DELAYED` | String | `INSTANTLY` | ❌ |
70
+ | `publish_date_time` | Scheduled publication date (for delayed type) | String | — | ❌ |
71
+ | `partial_value` | Percentage for partial release | String | — | ❌ |
72
+ | `services_type` | Service type: `HMS` or `Unknown` | String | `Unknown` | ❌ |
73
+ | `is_main_apk` | Is main APK | Boolean | `false` | ❌ |
74
+ | `apk_path` | Path to APK or AAB | String | — | ✅ |
75
+ | `is_aab` | Upload AAB instead of APK | Boolean | `false` | ❌ |
76
+ | `draft_strategy` | Strategy if draft exists: `DELETE`, `REUSE`, `FAIL` | String | `FAIL` | ❌ |
77
+ | `submit_for_review` | Should submit for review automatically | Boolean | `true` | ❌ |
78
+
79
+ ## Draft Strategies
80
+
81
+ * `DELETE` – Delete existing draft and create a new one
82
+ * `REUSE` – Reuse the existing draft
83
+ * `FAIL` – Fail if a draft already exists
84
+
85
+ ## Run tests for this plugin
86
+
87
+ To run both the tests, and code style validation, run
88
+
89
+ ```
90
+ rake
91
+ ```
92
+
93
+ To automatically fix many of the styling issues, use
94
+ ```
95
+ rubocop -a
96
+ ```
97
+
98
+ ## Issues and Feedback
99
+ For any other issues and feedback about this plugin, please submit it to this repository.
100
+
101
+ ## Troubleshooting
102
+ If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
103
+
104
+ ## Using _fastlane_ Plugins
105
+ For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
106
+
107
+ ## About _fastlane_
108
+ _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
@@ -0,0 +1,283 @@
1
+ require 'fastlane/action'
2
+ require_relative '../helper/rustore_connect_helper'
3
+ require_relative '../lib/rustore_connect_draft_strategy'
4
+
5
+ module Fastlane
6
+ module Actions
7
+ class RustoreConnectAction < Action
8
+ def self.run(params)
9
+ token = Helper::RustoreConnectHelper.get_token(params[:key_id], params[:private_key])
10
+
11
+ if token.nil?
12
+ UI.message("Cannot retrieve token, please check your Key ID and private key")
13
+ else
14
+ version_id = Helper::RustoreConnectHelper.get_app_versions(
15
+ token,
16
+ params[:package_name],
17
+ version_statuses: 'DRAFT'
18
+ )
19
+
20
+ UI.message("Debug: version_id before strategy check: #{version_id}")
21
+ UI.message("Debug: strategy check: #{params[:draft_strategy]}")
22
+
23
+ if version_id.nil?
24
+ # No draft exists → create a new one
25
+ UI.message("No existing draft found. Creating a new draft...")
26
+ version_id = Helper::RustoreConnectHelper.create_draft(
27
+ token,
28
+ params[:package_name],
29
+ params[:app_name],
30
+ params[:app_type],
31
+ params[:categories],
32
+ params[:age_legal],
33
+ params[:short_description],
34
+ params[:full_description],
35
+ params[:whats_new],
36
+ params[:moder_info],
37
+ params[:price_value],
38
+ params[:seo_tag_ids],
39
+ params[:publish_type],
40
+ params[:publish_date_time],
41
+ params[:partial_value]
42
+ )
43
+
44
+ else
45
+ # Draft exists → handle based on draft_strategy
46
+ case params[:draft_strategy]
47
+ when RustoreConnectDraftStrategy::DELETE
48
+ UI.message("Draft exists (ID=#{version_id}). Deleting and creating a new one...")
49
+
50
+ Helper::RustoreConnectHelper.delete_draft(
51
+ token,
52
+ params[:package_name],
53
+ version_id
54
+ )
55
+
56
+ version_id = Helper::RustoreConnectHelper.create_draft(
57
+ token,
58
+ params[:package_name],
59
+ params[:app_name],
60
+ params[:app_type],
61
+ params[:categories],
62
+ params[:age_legal],
63
+ params[:short_description],
64
+ params[:full_description],
65
+ params[:whats_new],
66
+ params[:moder_info],
67
+ params[:price_value],
68
+ params[:seo_tag_ids],
69
+ params[:publish_type],
70
+ params[:publish_date_time],
71
+ params[:partial_value]
72
+ )
73
+
74
+ when RustoreConnectDraftStrategy::REUSE
75
+ UI.message("Draft exists (ID=#{version_id}). Reusing existing draft.")
76
+
77
+ when RustoreConnectDraftStrategy::FAIL
78
+ UI.user_error!("Draft already exists (ID=#{version_id}) and draft_strategy is set to 'FAIL'")
79
+
80
+ else
81
+ UI.user_error!("Unknown draft_strategy: #{params[:draft_strategy]}. Allowed values: delete_and_create, reuse_existing, fail_if_exists")
82
+ end
83
+ end
84
+
85
+ UI.message("Using version_id=#{version_id} for upload...")
86
+
87
+ # Upload APK or AAB file
88
+ Helper::RustoreConnectHelper.upload(token, params[:package_name], version_id, params[:is_aab], params[:apk_path], params[:services_type], params[:is_main_apk])
89
+
90
+ if params[:submit_for_review] === true
91
+ Helper::RustoreConnectHelper::commit_version(token, params[:package_name], version_id)
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ def self.description
98
+ "Fastlane plugin for publishing Android applications to RuStore."
99
+ end
100
+
101
+ def self.authors
102
+ ["Mikhail Matsera"]
103
+ end
104
+
105
+ def self.return_value
106
+ # If your method provides a return value, you can describe here what it does
107
+ end
108
+
109
+ def self.details
110
+ # Optional:
111
+ "This Fastlane plugin provides seamless integration with RuStore, allowing you to easily upload and manage your Android application releases directly from your CI/CD pipeline."
112
+ end
113
+
114
+ def self.available_options
115
+ [
116
+ FastlaneCore::ConfigItem.new(key: :key_id,
117
+ env_name: "RUSTORE_CONNECT_KEY_ID",
118
+ description: "Key id",
119
+ optional: false,
120
+ type: String,
121
+ ),
122
+
123
+ FastlaneCore::ConfigItem.new(key: :private_key,
124
+ env_name: "RUSTORE_CONNECT_PRIVATE_KEY",
125
+ description: "Private key",
126
+ optional: false,
127
+ type: String,
128
+ ),
129
+
130
+ FastlaneCore::ConfigItem.new(key: :package_name,
131
+ env_name: "RUSTORE_CONNECT_PACKAGE_NAME",
132
+ description: "Наименование пакета приложения (example: `com.example.example`)",
133
+ optional: false,
134
+ type: String,
135
+ ),
136
+
137
+ FastlaneCore::ConfigItem.new(key: :app_name,
138
+ env_name: "RUSTORE_CONNECT_APP_NAME",
139
+ description: "App name (example `My App`)",
140
+ optional: true,
141
+ type: String,
142
+ ),
143
+
144
+ FastlaneCore::ConfigItem.new(key: :app_type,
145
+ env_name: "RUSTORE_CONNECT_APP_TYPE",
146
+ description: "Тип приложения (example `GAMES`)",
147
+ optional: true,
148
+ type: String,
149
+ ),
150
+
151
+ FastlaneCore::ConfigItem.new(key: :categories,
152
+ env_name: "RUSTORE_CONNECT_CATEGORIES",
153
+ description: "Категории версии. (example `\"health\", \"news\"`)",
154
+ optional: true,
155
+ type: String,
156
+ ),
157
+
158
+ FastlaneCore::ConfigItem.new(key: :age_legal,
159
+ env_name: "RUSTORE_CONNECT_AGE_LEGAL",
160
+ description: "Возрастная категория. (example `6+`)",
161
+ optional: true,
162
+ type: String,
163
+ ),
164
+
165
+ FastlaneCore::ConfigItem.new(key: :short_description,
166
+ env_name: "RUSTORE_CONNECT_SHORT_DESCRIPTION",
167
+ description: "Brief app release description. Maximum length: 80 characters",
168
+ optional: true,
169
+ type: String,
170
+ ),
171
+
172
+ FastlaneCore::ConfigItem.new(key: :full_description,
173
+ env_name: "RUSTORE_CONNECT_FULL_DESCRIPTION",
174
+ description: "Full description. Maximum length: 4000 characters",
175
+ optional: true,
176
+ type: String,
177
+ ),
178
+
179
+ FastlaneCore::ConfigItem.new(key: :whats_new,
180
+ env_name: "RUSTORE_CONNECT_WHATS_NEW",
181
+ description: "What's New",
182
+ optional: true,
183
+ type: String,
184
+ ),
185
+
186
+ FastlaneCore::ConfigItem.new(key: :moder_info,
187
+ env_name: "RUSTORE_CONNECT_MODER_INFO",
188
+ description: "Developer comment for moderator. Maximum length: 180 characters",
189
+ optional: true,
190
+ type: String,
191
+ ),
192
+
193
+ FastlaneCore::ConfigItem.new(key: :price_value,
194
+ env_name: "RUSTORE_CONNECT_PRICE_VALUE",
195
+ description: "App price in minimum currency units (in kopecks), for example, `87.99 rubles.` = 8799. Value should be >0",
196
+ optional: true,
197
+ type: String,
198
+ ),
199
+
200
+ FastlaneCore::ConfigItem.new(key: :seo_tag_ids,
201
+ env_name: "RUSTORE_CONNECT_SEO_TAG_IDS",
202
+ description: "Package name, for example `com.example.example`",
203
+ optional: true,
204
+ type: String,
205
+ ),
206
+
207
+ FastlaneCore::ConfigItem.new(key: :publish_type,
208
+ env_name: "RUSTORE_CONNECT_PUBLISH_TYPE",
209
+ description: "Publication type. Publication type: MANUAL — manual publication; INSTANTLY — automatic publication immediately after review; DELAYED — delayed publication. Note: if this parameter is not specified, then it is taken asINSTANTLY by default",
210
+ optional: true,
211
+ default_value: "INSTANTLY",
212
+ type: String,
213
+ ),
214
+
215
+ FastlaneCore::ConfigItem.new(key: :publish_date_time,
216
+ env_name: "RUSTORE_CONNECT_PUBLISH_DATE_TIME",
217
+ description: "Date and time for delayed publication: format: yyyy-MM-dd'T'HH:mm:ssXXX. The specified date must be no earlier than 24 hours and no later than 60 days from the planned submission date. The delayed publication date can be changed. Note: if publishType is MANUAL или INSTANTLY, this parameter can be anything and will not be taken into account",
218
+ optional: true,
219
+ type: String,
220
+ ),
221
+
222
+ FastlaneCore::ConfigItem.new(key: :partial_value,
223
+ env_name: "RUSTORE_CONNECT_PARTIAL_VALUE",
224
+ description: "Percentage for partial publication",
225
+ optional: true,
226
+ type: String,
227
+ ),
228
+
229
+ FastlaneCore::ConfigItem.new(key: :services_type,
230
+ env_name: "RUSTORE_CONNECT_SERVICES_TYPE",
231
+ description: "Type of service used by the app. Possible options: • `HMS` — for APK files with Huawei Mobile Services; • `Unknown` is set by default if the field is empty",
232
+ optional: true,
233
+ default_value: "Unknown",
234
+ type: String,
235
+ ),
236
+
237
+ FastlaneCore::ConfigItem.new(key: :is_main_apk,
238
+ env_name: "RUSTORE_CONNECT_IS_MAIN_APK",
239
+ description: "Attribute that is assigned to the main apk file. Values: • `true` — main APK file; • `false` — by default",
240
+ optional: true,
241
+ default_value: false,
242
+ type: Boolean,
243
+ ),
244
+
245
+ FastlaneCore::ConfigItem.new(key: :apk_path,
246
+ env_name: "RUSTORE_CONNECT_APK_PATH",
247
+ description: "Path to APK file for upload",
248
+ optional: false,
249
+ type: String,
250
+ ),
251
+
252
+ FastlaneCore::ConfigItem.new(key: :is_aab,
253
+ env_name: "RUSTORE_CONNECT_IS_AAB",
254
+ description: "Specify this to be true if you're uploading aab instead of apk",
255
+ optional: true,
256
+ type: Boolean,
257
+ ),
258
+
259
+ FastlaneCore::ConfigItem.new(key: :draft_strategy,
260
+ env_name: "RUSTORE_CONNECT_DRAFT_STRATEGY",
261
+ description: "Strategy if draft existing. DELETE, REUSE, FAIL",
262
+ optional: true,
263
+ type: String,
264
+ default_value: RustoreConnectDraftStrategy::FAIL,
265
+ ),
266
+
267
+ FastlaneCore::ConfigItem.new(key: :submit_for_review,
268
+ env_name: "RUSTORE_CONNECT_SUBMIT_FOR_REVIEW",
269
+ description: "Should submit the app for review. The default value is true. If set false will only upload the app, and you can submit for review from the console",
270
+ optional: true,
271
+ type: Boolean,
272
+ default_value: true,
273
+ )
274
+ ]
275
+ end
276
+
277
+ def self.is_supported?(platform)
278
+ [:android].include?(platform)
279
+ true
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,257 @@
1
+ require 'fastlane_core/ui/ui'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'net/http/post/multipart'
6
+
7
+ module Fastlane
8
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
9
+
10
+ module Helper
11
+ class RustoreConnectHelper
12
+ def self.rsa_sign(timestamp, key_id, private_key)
13
+ key = OpenSSL::PKey::RSA.new("-----BEGIN RSA PRIVATE KEY-----\n#{private_key}\n-----END RSA PRIVATE KEY-----")
14
+ signature = key.sign(OpenSSL::Digest.new('SHA512'), key_id + timestamp)
15
+ Base64.encode64(signature)
16
+ end
17
+
18
+ def self.get_token(key_id, private_key)
19
+ UI.important("Fetching app access token")
20
+
21
+ uri = URI.parse('https://public-api.rustore.ru/public/auth')
22
+ http = Net::HTTP.new(uri.host, uri.port)
23
+ http.use_ssl = true
24
+ request = Net::HTTP::Post.new(uri.path)
25
+
26
+ request["Content-Type"] = "application/json"
27
+
28
+ timestamp = DateTime.now.iso8601(3)
29
+ signature = rsa_sign(timestamp, key_id, private_key)
30
+
31
+ request.body = { keyId: key_id, timestamp: timestamp, signature: signature }.to_json
32
+ response = http.request(request)
33
+
34
+ UI.message("Debug: response #{response.body}")
35
+
36
+ # Если не 200 — возвращаем nil
37
+ return nil unless response.is_a?(Net::HTTPSuccess)
38
+
39
+ result_json = JSON.parse(response.body) rescue nil
40
+ return nil unless result_json && result_json["body"]
41
+
42
+ return result_json["body"]["jwe"]
43
+ end
44
+
45
+ def self.get_app_versions(token, package_name, ids: nil, version_statuses: nil, filter_testing_type: nil, page: nil, size: nil)
46
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version")
47
+ http = Net::HTTP.new(uri.host, uri.port)
48
+ http.use_ssl = true
49
+
50
+ uri.query = URI.encode_www_form(
51
+ {
52
+ ids: ids,
53
+ versionStatuses: version_statuses,
54
+ filterTestingType: filter_testing_type,
55
+ page: page,
56
+ size: size
57
+ }.compact
58
+ )
59
+
60
+ request = Net::HTTP::Get.new(uri)
61
+
62
+ request["Content-Type"] = "application/json"
63
+ request["Public-Token"] = token
64
+
65
+ response = http.request(request)
66
+
67
+ result_json = JSON.parse(response.body)
68
+
69
+ UI.message("Debug: response #{response.body}")
70
+ if result_json["code"] == 'OK'
71
+ body = result_json["body"]
72
+ content = body["content"]
73
+
74
+ # Check that content is an array
75
+ unless content.is_a?(Array)
76
+ raise "Invalid RuStore response format: 'content' array is missing"
77
+ end
78
+
79
+ # If there are no elements — return nil
80
+ return nil if content.empty?
81
+
82
+ # If there are more than one element — raise an error
83
+ if content.size > 1
84
+ raise "Expected a single element in 'content', but found: #{content.size}"
85
+ end
86
+
87
+ # Return the id of the first element
88
+ return content.first["versionId"]
89
+ else
90
+ raise "Couldn't get draftId from RuStore: #{result_json['message'] || 'Unknown error'}"
91
+ end
92
+
93
+ end
94
+
95
+ def self.create_draft(
96
+ token,
97
+ package_name,
98
+ app_name,
99
+ app_type,
100
+ categories,
101
+ age_legal,
102
+ short_description,
103
+ full_description,
104
+ whats_new,
105
+ moder_info,
106
+ price_value,
107
+ seo_tag_ids,
108
+ publish_type,
109
+ publish_date_time,
110
+ partial_value
111
+ )
112
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version")
113
+
114
+ http = Net::HTTP.new(uri.host, uri.port)
115
+ http.use_ssl = true
116
+ request = Net::HTTP::Post.new(uri)
117
+
118
+ request["Content-Type"] = "application/json"
119
+ request["Public-Token"] = token
120
+
121
+ request.body = {
122
+ appName: app_name,
123
+ appType: app_type,
124
+ categories: categories,
125
+ ageLegal: age_legal,
126
+ shortDescription: short_description,
127
+ fullDescription: full_description,
128
+ whatsNew: whats_new,
129
+ moderInfo: moder_info,
130
+ priceValue: price_value,
131
+ seoTagIds: seo_tag_ids,
132
+ publishType: publish_type,
133
+ publishDateTime: publish_date_time,
134
+ partialValue: partial_value
135
+ }.to_json
136
+
137
+ response = http.request(request)
138
+
139
+ result_json = JSON.parse(response.body)
140
+
141
+ UI.message("Debug: response #{response.body}")
142
+
143
+ if result_json["code"] == "OK"
144
+ version_id = result_json["body"]
145
+ if version_id
146
+ return version_id
147
+ else
148
+ raise "Missing versionId in response body"
149
+ end
150
+ else
151
+ message = result_json["message"] || "Couldn't create draft on RuStore"
152
+ raise message
153
+ end
154
+ end
155
+
156
+ def self.delete_draft(
157
+ token,
158
+ package_name,
159
+ version_id
160
+ )
161
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version/#{version_id}")
162
+
163
+ http = Net::HTTP.new(uri.host, uri.port)
164
+ http.use_ssl = true
165
+ request = Net::HTTP::Delete.new(uri)
166
+
167
+ request["Content-Type"] = "application/json"
168
+ request["Public-Token"] = token
169
+
170
+ response = http.request(request)
171
+ result_json = JSON.parse(response.body)
172
+
173
+ UI.message("Debug: response #{response.body}")
174
+
175
+ if result_json["code"] == "OK"
176
+ UI.message("Draft version #{version_id} deleted successfully.")
177
+ return true
178
+ else
179
+ message = result_json["message"] || "Failed to delete draft version #{version_id}"
180
+ raise message
181
+ end
182
+ end
183
+
184
+ def self.commit_version(token, package_name, version_id)
185
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version/#{version_id}/commit")
186
+
187
+ http = Net::HTTP.new(uri.host, uri.port)
188
+ http.use_ssl = true
189
+
190
+ request = Net::HTTP::Post.new(uri)
191
+ request["Content-Type"] = "application/json"
192
+ request["Public-Token"] = token
193
+
194
+ response = http.request(request)
195
+ result_json = JSON.parse(response.body)
196
+
197
+ UI.message("Debug: response #{response.body}") if ENV['DEBUG']
198
+
199
+ if result_json["code"] == "OK"
200
+ UI.message("Draft version #{version_id} committed successfully.")
201
+ return true
202
+ else
203
+ message = result_json["message"] || "Failed to commit draft version #{version_id}"
204
+ raise message
205
+ end
206
+ end
207
+
208
+ def self.upload(token, package_name, version_id, is_aab, file_path, services_type = nil, is_main_apk = nil)
209
+ file_path = File.expand_path(file_path, Dir.pwd)
210
+ raise "File not found: #{file_path}" unless File.exist?(file_path)
211
+
212
+ file_name = File.basename(file_path)
213
+
214
+ if is_aab
215
+ # Загрузка AAB
216
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version/#{version_id}/aab")
217
+ payload = { "file" => UploadIO.new(file_path, "application/vnd.android.package-archive", file_name) }
218
+ else
219
+ # Загрузка APK
220
+ raise ArgumentError, "is_main_apk is required for APK upload" if is_main_apk.nil?
221
+
222
+ services_type ||= "Unknown"
223
+ uri = URI.parse("https://public-api.rustore.ru/public/v1/application/#{package_name}/version/#{version_id}/apk")
224
+ uri.query = URI.encode_www_form({ isMainApk: is_main_apk, servicesType: services_type })
225
+ payload = { "file" => UploadIO.new(file_path, "application/vnd.android.package-archive", file_name) }
226
+ end
227
+
228
+ request = Net::HTTP::Post::Multipart.new(uri, payload)
229
+ request["Public-Token"] = token
230
+
231
+ http = Net::HTTP.new(uri.host, uri.port)
232
+ http.use_ssl = true
233
+ http.open_timeout = 60 # время на открытие соединения
234
+ http.read_timeout = 600 # время на чтение ответа (подойдет для больших файлов)
235
+ http.write_timeout = 600 # время на отправку файла
236
+
237
+ response = http.request(request)
238
+ result_json = JSON.parse(response.body)
239
+
240
+ UI.message("Debug: response #{response.body}")
241
+
242
+ if result_json["code"] == "OK"
243
+ UI.message("#{file_name} uploaded successfully.")
244
+ true
245
+ else
246
+ message = result_json["message"] || "Failed to upload file"
247
+ if message.include?("The code of this version must be larger")
248
+ raise "Build with this version code was already uploaded earlier"
249
+ else
250
+ raise message
251
+ end
252
+ end
253
+ end
254
+
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,15 @@
1
+ module RustoreConnectDraftStrategy
2
+ DELETE = 'DELETE'
3
+ REUSE = 'REUSE'
4
+ FAIL = 'FAIL'
5
+
6
+ ALL = [
7
+ DELETE,
8
+ REUSE,
9
+ FAIL
10
+ ].freeze
11
+
12
+ def self.valid?(value)
13
+ ALL.include?(value)
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module RustoreConnect
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/rustore_connect/version'
2
+
3
+ module Fastlane
4
+ module RustoreConnect
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::RustoreConnect.all_classes.each do |current|
15
+ require current
16
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-rustore_connect
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mikhail Matsera
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-09-03 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: mmatsera@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - README.md
21
+ - lib/fastlane/plugin/rustore_connect.rb
22
+ - lib/fastlane/plugin/rustore_connect/actions/rustore_connect_action.rb
23
+ - lib/fastlane/plugin/rustore_connect/helper/rustore_connect_helper.rb
24
+ - lib/fastlane/plugin/rustore_connect/lib/rustore_connect_draft_strategy.rb
25
+ - lib/fastlane/plugin/rustore_connect/version.rb
26
+ homepage:
27
+ licenses:
28
+ - MIT
29
+ metadata:
30
+ rubygems_mfa_required: 'true'
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.6'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.4.10
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Fastlane plugin for publishing Android applications to RuStore.
50
+ test_files: []