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 +7 -0
- data/LICENSE +21 -0
- data/README.md +108 -0
- data/lib/fastlane/plugin/rustore_connect/actions/rustore_connect_action.rb +283 -0
- data/lib/fastlane/plugin/rustore_connect/helper/rustore_connect_helper.rb +257 -0
- data/lib/fastlane/plugin/rustore_connect/lib/rustore_connect_draft_strategy.rb +15 -0
- data/lib/fastlane/plugin/rustore_connect/version.rb +5 -0
- data/lib/fastlane/plugin/rustore_connect.rb +16 -0
- metadata +50 -0
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
|
+
[](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,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: []
|