fastlane-plugin-rustore_publish 0.3.1
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 +445 -0
- data/lib/fastlane/plugin/rustore_publish/actions/rustore_upload.rb +523 -0
- data/lib/fastlane/plugin/rustore_publish/certs/russian_trusted_root_ca.crt +33 -0
- data/lib/fastlane/plugin/rustore_publish/helper/rustore_auth.rb +179 -0
- data/lib/fastlane/plugin/rustore_publish/helper/rustore_client.rb +353 -0
- data/lib/fastlane/plugin/rustore_publish/helper/rustore_endpoint.rb +33 -0
- data/lib/fastlane/plugin/rustore_publish/helper/rustore_logger.rb +108 -0
- data/lib/fastlane/plugin/rustore_publish/version.rb +5 -0
- data/lib/fastlane/plugin/rustore_publish.rb +13 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e78e12cd03800029213198db6dc3653ce95c8ea4dd6471d5e067c9ff79b562e1
|
|
4
|
+
data.tar.gz: 512f9299d5414564a7c59eeb219fbdedda933d2057941de7d042dc5d013f9183
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 60b304a1d22e6d5c7edf6ad72fea7c7095c611866c4554f06728eb661f5f53f5bbf6d2b09e70a5b71a77cd8554453d9cd423a55cc1594be8064c7368b5ac4366
|
|
7
|
+
data.tar.gz: 9be1cb11bce6018d2254478dd0a3d9b39cb35bc4c90365c828fb73a0cc8ffbdfbde4b38307fa30db399e2a97137ca5a264f6562b9ba03ab89f9a9b78cdc7a89f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey
|
|
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,445 @@
|
|
|
1
|
+
# RuStore fastlane plugin — publish Android apps to RuStore from CI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Fastlane plugin for publishing Android applications to [RuStore](https://rustore.ru) — the Russian app store — via the [RuStore Public API](https://www.rustore.ru/help/work-with-rustore-api/api-upload-publication-app). Uses the new `public-api-m.rustore.ru` domain with the Russian Trusted Root CA bundled, so nothing has to be installed on the runner.
|
|
6
|
+
|
|
7
|
+
Gem: `fastlane-plugin-rustore_publish` · Action: `rustore_upload`
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Upload **AAB** (Google/GMS, main artifact) and **APK** (Huawei/HMS, secondary) in a single version
|
|
12
|
+
- Full publish workflow: authenticate → create draft (with publication settings) → upload files → submit for moderation
|
|
13
|
+
- The pipeline always finishes immediately after submission — no waiting for moderation
|
|
14
|
+
- **GitLab CI** collapsible log sections (foldable blocks in Pipeline UI)
|
|
15
|
+
- Staged rollout support (`rollout_percentage`)
|
|
16
|
+
- Scheduled publishing (`DELAYED` + `release_date`)
|
|
17
|
+
- Auto-cleanup of existing drafts before creating a new one
|
|
18
|
+
- JWE token auto-refresh (900 s TTL)
|
|
19
|
+
- Works with the `public-api-m.rustore.ru` domain; the Russian Trusted Root CA is bundled, nothing to install on the runner
|
|
20
|
+
- CI-friendly upload logs: send time, a heartbeat while RuStore checks the file, response time, detected permissions and SDKs
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Fastlane ≥ 2.200.0
|
|
25
|
+
- Ruby ≥ 2.6
|
|
26
|
+
- At least **one active published version** must already exist in RuStore Console (API requirement)
|
|
27
|
+
- Outbound HTTPS to `public-api-m.rustore.ru:443`
|
|
28
|
+
|
|
29
|
+
### RuStore domain and certificate
|
|
30
|
+
|
|
31
|
+
RuStore moved the Public API from `public-api.rustore.ru` to `public-api-m.rustore.ru` ([announcement](https://www.rustore.ru/developer/blog/rustore-obnovlyaet-sertifikaty-i-domeny-chto-nuzhno-sdelat-razrabotchikam)). The new host is signed by the Russian Trusted Root CA, which is missing from the default OS and Ruby trust stores. The plugin ships that root (`lib/fastlane/plugin/rustore_publish/certs/russian_trusted_root_ca.crt`, from [Gosuslugi](https://www.gosuslugi.ru/crt), SHA-256 `D2:6D:2D:02:31:B7:C3:9F:92:CC:73:85:12:BA:54:10:35:19:E4:40:5D:68:B5:BD:70:3E:97:88:CA:8E:CF:31`) and trusts it in addition to the system roots, for plugin requests only.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Add the plugin to your `Pluginfile`:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Pluginfile
|
|
41
|
+
gem "fastlane-plugin-rustore_publish"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then run:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
fastlane install_plugins
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Setup: RuStore API credentials
|
|
53
|
+
|
|
54
|
+
1. Log in to [RuStore Console](https://console.rustore.ru) with a **company owner** account
|
|
55
|
+
2. Go to **Company → API RuStore**
|
|
56
|
+
3. Click **Generate a key**, give it a name, select your app and required API methods
|
|
57
|
+
4. Download the **private key** (PEM file) — save it securely, RuStore does not store it
|
|
58
|
+
|
|
59
|
+
You will get two values:
|
|
60
|
+
- **Key ID** (`key_id`) — shown in the Console after generation
|
|
61
|
+
- **Private key** — the downloaded `.pem` file
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Parameters
|
|
66
|
+
|
|
67
|
+
| Parameter | Env var | Required | Default | Description |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
| `key_id` | `RUSTORE_KEY_ID` | yes | — | API key ID from RuStore Console |
|
|
70
|
+
| `private_key_path` | `RUSTORE_PRIVATE_KEY_PATH` | one of two | — | Path to RSA private key PEM file |
|
|
71
|
+
| `private_key` | `RUSTORE_PRIVATE_KEY` | one of two | — | PEM content as string (for CI secrets) |
|
|
72
|
+
| `package_name` | `RUSTORE_PACKAGE_NAME` | yes | — | App package name, e.g. `com.example.app` |
|
|
73
|
+
| `aab_path` | `RUSTORE_AAB_PATH` | one of two | — | Path to AAB (Google/GMS — becomes main artifact) |
|
|
74
|
+
| `apk_path` | `RUSTORE_APK_PATH` | one of two | — | Path to APK (used when `aab_path` not set) |
|
|
75
|
+
| `hms_apk_path` | `RUSTORE_HMS_APK_PATH` | no | — | Path to Huawei/HMS APK (`servicesType=HMS, isMainApk=false`) |
|
|
76
|
+
| `whats_new` | `RUSTORE_WHATS_NEW` | no | — | Release notes shown to users (plain text, max 5000 chars) |
|
|
77
|
+
| `moder_info` | `RUSTORE_MODER_INFO` | no | — | Notes for the RuStore moderator, e.g. test credentials (max 180 chars) |
|
|
78
|
+
| `publish_type` | `RUSTORE_PUBLISH_TYPE` | no | `INSTANTLY` | `INSTANTLY` / `MANUAL` / `DELAYED` |
|
|
79
|
+
| `release_date` | `RUSTORE_RELEASE_DATE` | no | — | ISO 8601 datetime, only for `DELAYED` |
|
|
80
|
+
| `rollout_percentage` | `RUSTORE_ROLLOUT_PERCENTAGE` | no | — | Staged rollout: allowed values `5, 10, 25, 50, 75, 100` |
|
|
81
|
+
| `update_priority` | `RUSTORE_UPDATE_PRIORITY` | no | `0` | In-app update priority sent with the moderation request, `0`–`5` |
|
|
82
|
+
| `upload_timeout` | `RUSTORE_UPLOAD_TIMEOUT` | no | `900` | Seconds to wait for RuStore to answer an AAB/APK upload. RuStore checks the file (permissions, SDKs) before answering |
|
|
83
|
+
|
|
84
|
+
### Draft metadata (NEW — RuStore API v2)
|
|
85
|
+
|
|
86
|
+
RuStore's `create-draft-version` endpoint accepts additional fields beyond release notes. When unset, RuStore inherits each value from the previously active version. **`developer_contacts` may be required** if your RuStore Console developer contacts have not been filled in — a missing value here causes `API request failed [400]: Developer contacts are not filled in`.
|
|
87
|
+
|
|
88
|
+
| Parameter | Env var | Type | Description |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `developer_contacts` | — | Hash | `{ email:, website:, vk_community: }` — at least one sub-key required |
|
|
91
|
+
| `app_name` | `RUSTORE_APP_NAME` | String | App display name, max 50 chars |
|
|
92
|
+
| `app_type` | `RUSTORE_APP_TYPE` | String | `GAMES` or `MAIN` |
|
|
93
|
+
| `categories` | — | Array<String> | Up to 2 categories, e.g. `["news", "education"]` |
|
|
94
|
+
| `age_legal` | `RUSTORE_AGE_LEGAL` | String | One of `0+`, `6+`, `12+`, `16+`, `18+` |
|
|
95
|
+
| `short_description` | `RUSTORE_SHORT_DESCRIPTION` | String | Brief description, max 80 chars |
|
|
96
|
+
| `full_description` | `RUSTORE_FULL_DESCRIPTION` | String | Full description, max 4000 chars |
|
|
97
|
+
| `price_value` | `RUSTORE_PRICE_VALUE` | String | Price in kopecks as digits, e.g. `"8799"` for 87.99 RUB |
|
|
98
|
+
| `seo_tag_ids` | — | Array<Integer> | Up to 5 search tag IDs |
|
|
99
|
+
| `faq` | — | Array<Hash> | Up to 10 items, each `{ question: ≤120 chars, answer: ≤500 chars }` |
|
|
100
|
+
|
|
101
|
+
Hash and Array parameters have no env-var binding — env vars carry strings only. If you need env-driven configuration, parse JSON yourself in your Fastfile:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
developer_contacts: JSON.parse(ENV.fetch("RUSTORE_DEVELOPER_CONTACTS"), symbolize_names: true)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Example:**
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
rustore_upload(
|
|
111
|
+
package_name: "com.example.app",
|
|
112
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
113
|
+
private_key: ENV["RUSTORE_PRIVATE_KEY"],
|
|
114
|
+
aab_path: "app/build/outputs/bundle/release/app-release.aab",
|
|
115
|
+
|
|
116
|
+
# Draft metadata (v2)
|
|
117
|
+
developer_contacts: {
|
|
118
|
+
email: "support@example.com",
|
|
119
|
+
website: "https://example.com",
|
|
120
|
+
vk_community: "https://vk.com/example"
|
|
121
|
+
},
|
|
122
|
+
app_name: "Example App",
|
|
123
|
+
app_type: "MAIN",
|
|
124
|
+
categories: ["news", "education"],
|
|
125
|
+
age_legal: "0+",
|
|
126
|
+
short_description: "Best example app",
|
|
127
|
+
full_description: "A longer description of what the app does and why people should install it...",
|
|
128
|
+
faq: [
|
|
129
|
+
{ question: "How do I sign in?", answer: "Use your email and password." },
|
|
130
|
+
{ question: "Is it free?", answer: "Yes." }
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Publish types
|
|
138
|
+
|
|
139
|
+
| `publish_type` | Behaviour |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `INSTANTLY` | RuStore auto-publishes after moderation passes (default) |
|
|
142
|
+
| `DELAYED` | Scheduled publication via `release_date` in ISO 8601 |
|
|
143
|
+
| `MANUAL` | Moderation runs normally; publish manually from RuStore Console |
|
|
144
|
+
|
|
145
|
+
The pipeline finishes immediately after submitting for moderation for all three types.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Usage examples
|
|
150
|
+
|
|
151
|
+
### Minimal — AAB only, INSTANTLY (default)
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# fastlane/Fastfile
|
|
155
|
+
lane :deploy_rustore do
|
|
156
|
+
rustore_upload(
|
|
157
|
+
package_name: "com.example.app",
|
|
158
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
159
|
+
private_key_path: "fastlane/rustore_private_key.pem",
|
|
160
|
+
aab_path: "app/build/outputs/bundle/release/app-release.aab"
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### AAB (GMS) + APK (HMS), staged rollout
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
lane :deploy_rustore do
|
|
169
|
+
rustore_upload(
|
|
170
|
+
package_name: "com.example.app",
|
|
171
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
172
|
+
private_key: ENV["RUSTORE_PRIVATE_KEY"],
|
|
173
|
+
|
|
174
|
+
# Primary build — Google/GMS (AAB is always isMainApk)
|
|
175
|
+
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
|
|
176
|
+
|
|
177
|
+
# Secondary build — Huawei/HMS (servicesType=HMS, isMainApk=false)
|
|
178
|
+
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
|
|
179
|
+
|
|
180
|
+
publish_type: "INSTANTLY",
|
|
181
|
+
rollout_percentage: 25 # release to 25% of users first
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Scheduled release
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
lane :deploy_rustore_scheduled do
|
|
190
|
+
rustore_upload(
|
|
191
|
+
package_name: "com.example.app",
|
|
192
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
193
|
+
private_key: ENV["RUSTORE_PRIVATE_KEY"],
|
|
194
|
+
aab_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
|
|
195
|
+
publish_type: "DELAYED",
|
|
196
|
+
release_date: "2025-03-01T10:00:00+03:00" # Moscow time
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Combined with Gradle build
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
lane :build_and_deploy do
|
|
205
|
+
gradle(task: "bundle", flavor: "gms", build_type: "Release")
|
|
206
|
+
gradle(task: "assemble", flavor: "hms", build_type: "Release")
|
|
207
|
+
|
|
208
|
+
rustore_upload(
|
|
209
|
+
package_name: "com.example.app",
|
|
210
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
211
|
+
private_key: ENV["RUSTORE_PRIVATE_KEY"],
|
|
212
|
+
aab_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
|
|
213
|
+
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
|
|
214
|
+
publish_type: "INSTANTLY"
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## CI/CD integration
|
|
222
|
+
|
|
223
|
+
### GitLab CI
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
# .gitlab-ci.yml
|
|
227
|
+
|
|
228
|
+
variables:
|
|
229
|
+
RUSTORE_KEY_ID: $RUSTORE_KEY_ID # set in GitLab CI/CD → Variables
|
|
230
|
+
RUSTORE_PRIVATE_KEY: $RUSTORE_PRIVATE_KEY # set as "File" type variable — path to PEM
|
|
231
|
+
|
|
232
|
+
stages:
|
|
233
|
+
- build
|
|
234
|
+
- deploy
|
|
235
|
+
|
|
236
|
+
build:
|
|
237
|
+
stage: build
|
|
238
|
+
script:
|
|
239
|
+
- bundle exec fastlane build_release
|
|
240
|
+
artifacts:
|
|
241
|
+
paths:
|
|
242
|
+
- app/build/outputs/bundle/gmsRelease/
|
|
243
|
+
- app/build/outputs/apk/hmsRelease/
|
|
244
|
+
expire_in: 1 hour
|
|
245
|
+
|
|
246
|
+
deploy_rustore:
|
|
247
|
+
stage: deploy
|
|
248
|
+
needs: [build]
|
|
249
|
+
script:
|
|
250
|
+
- bundle exec fastlane deploy_rustore
|
|
251
|
+
environment:
|
|
252
|
+
name: production
|
|
253
|
+
rules:
|
|
254
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # run only on version tags
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
# fastlane/Fastfile
|
|
259
|
+
lane :deploy_rustore do
|
|
260
|
+
rustore_upload(
|
|
261
|
+
package_name: "com.example.app",
|
|
262
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
263
|
+
private_key_path: ENV["RUSTORE_PRIVATE_KEY"], # "File" variable = path to PEM
|
|
264
|
+
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
|
|
265
|
+
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
|
|
266
|
+
publish_type: "INSTANTLY"
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Storing the private key in GitLab:**
|
|
272
|
+
|
|
273
|
+
- Go to **Settings → CI/CD → Variables**
|
|
274
|
+
- Add `RUSTORE_KEY_ID` as a regular masked variable
|
|
275
|
+
- Add `RUSTORE_PRIVATE_KEY` as **type: File** — GitLab writes the PEM to a temp file and exports the path. Use `private_key_path: ENV["RUSTORE_PRIVATE_KEY"]`
|
|
276
|
+
- Alternatively, paste the PEM content as a masked variable and use `private_key: ENV["RUSTORE_PRIVATE_KEY"]`
|
|
277
|
+
|
|
278
|
+
**GitLab CI log output** (steps appear as collapsible sections):
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
▶ [RuStore] Step 1/5: Authentication ← click to expand
|
|
282
|
+
▶ [RuStore] Step 2/5: Draft Management
|
|
283
|
+
▶ [RuStore] Step 3/5: Uploading AAB (GMS/main)
|
|
284
|
+
▶ [RuStore] Step 4/5: Uploading APK (HMS/secondary)
|
|
285
|
+
▶ [RuStore] Step 5/5: Submitting for moderation
|
|
286
|
+
[RuStore] ✓ All done! com.example.app versionId=12345 submitted to RuStore.
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Upload step output.** RuStore answers an upload only after it has checked the file, so the step logs when the file was sent and keeps a heartbeat until the answer arrives:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
[RuStore] Sending app-release.aab (32.5 MB)
|
|
293
|
+
[RuStore] app-release.aab sent in 1.2s, waiting for RuStore to check it
|
|
294
|
+
[RuStore] Waiting for RuStore response: 60s / 900s
|
|
295
|
+
[RuStore] RuStore responded in 102s
|
|
296
|
+
[RuStore] Permissions detected by RuStore: android.permission.CAMERA, android.permission.ACCESS_FINE_LOCATION
|
|
297
|
+
[RuStore] SDKs detected by RuStore: analytics: com.google.firebase.analytics
|
|
298
|
+
[RuStore] ✓ AAB uploaded successfully
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### GitHub Actions
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
# .github/workflows/deploy-rustore.yml
|
|
307
|
+
name: Deploy to RuStore
|
|
308
|
+
|
|
309
|
+
on:
|
|
310
|
+
push:
|
|
311
|
+
tags:
|
|
312
|
+
- 'v*'
|
|
313
|
+
|
|
314
|
+
jobs:
|
|
315
|
+
deploy:
|
|
316
|
+
runs-on: ubuntu-latest
|
|
317
|
+
steps:
|
|
318
|
+
- uses: actions/checkout@v4
|
|
319
|
+
|
|
320
|
+
- uses: actions/setup-java@v4
|
|
321
|
+
with:
|
|
322
|
+
java-version: '17'
|
|
323
|
+
distribution: 'temurin'
|
|
324
|
+
|
|
325
|
+
- uses: ruby/setup-ruby@v1
|
|
326
|
+
with:
|
|
327
|
+
ruby-version: '3.2'
|
|
328
|
+
bundler-cache: true
|
|
329
|
+
|
|
330
|
+
- name: Build release
|
|
331
|
+
run: bundle exec fastlane build_release
|
|
332
|
+
|
|
333
|
+
- name: Deploy to RuStore
|
|
334
|
+
env:
|
|
335
|
+
RUSTORE_KEY_ID: ${{ secrets.RUSTORE_KEY_ID }}
|
|
336
|
+
RUSTORE_PRIVATE_KEY: ${{ secrets.RUSTORE_PRIVATE_KEY }}
|
|
337
|
+
run: bundle exec fastlane deploy_rustore
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
# fastlane/Fastfile
|
|
342
|
+
lane :deploy_rustore do
|
|
343
|
+
rustore_upload(
|
|
344
|
+
package_name: "com.example.app",
|
|
345
|
+
key_id: ENV["RUSTORE_KEY_ID"],
|
|
346
|
+
private_key: ENV["RUSTORE_PRIVATE_KEY"], # PEM content from GitHub secret
|
|
347
|
+
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
|
|
348
|
+
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
|
|
349
|
+
publish_type: "INSTANTLY"
|
|
350
|
+
)
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Storing secrets in GitHub:**
|
|
355
|
+
|
|
356
|
+
- Go to **Settings → Secrets and variables → Actions**
|
|
357
|
+
- Add `RUSTORE_KEY_ID` — the key ID string
|
|
358
|
+
- Add `RUSTORE_PRIVATE_KEY` — paste the full PEM content (including `-----BEGIN/END PRIVATE KEY-----` lines)
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### Bitrise
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
# bitrise.yml (relevant step)
|
|
366
|
+
- fastlane@3:
|
|
367
|
+
inputs:
|
|
368
|
+
- lane: deploy_rustore
|
|
369
|
+
envs:
|
|
370
|
+
- RUSTORE_KEY_ID: $RUSTORE_KEY_ID
|
|
371
|
+
- RUSTORE_PRIVATE_KEY: $RUSTORE_PRIVATE_KEY
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Add `RUSTORE_KEY_ID` and `RUSTORE_PRIVATE_KEY` as **Secret Environment Variables** in Bitrise **Secrets** tab.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Multi-file versions
|
|
379
|
+
|
|
380
|
+
RuStore allows up to **1 AAB + 8 APK** (or 10 APK) files per version. The plugin supports the most common scenario:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
Version
|
|
384
|
+
├── app-gms-release.aab ← AAB, servicesType=Unknown, isMainApk=true (implicit)
|
|
385
|
+
└── app-hms-release.apk ← APK, servicesType=HMS, isMainApk=false
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
RuStore automatically serves the appropriate file to each user based on their device.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Debugging
|
|
393
|
+
|
|
394
|
+
Enable verbose HTTP logging:
|
|
395
|
+
|
|
396
|
+
```sh
|
|
397
|
+
RUSTORE_DEBUG=1 bundle exec fastlane deploy_rustore
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Run Fastlane with full verbose output:
|
|
401
|
+
|
|
402
|
+
```sh
|
|
403
|
+
bundle exec fastlane deploy_rustore --verbose
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Error reference
|
|
409
|
+
|
|
410
|
+
| Error | Cause | Fix |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `Authentication failed: ...` | Wrong `key_id` or corrupted key | Check key in RuStore Console → API RuStore |
|
|
413
|
+
| `Failed to load RSA private key` | Invalid PEM format | Ensure the key includes `-----BEGIN/END PRIVATE KEY-----` |
|
|
414
|
+
| `API request failed [404]` | Package not found | Verify `package_name`; at least 1 active version must exist in RuStore Console |
|
|
415
|
+
| `API request failed [403]` | Key lacks permissions | Ensure the key has access to the required API methods for your app in Console |
|
|
416
|
+
| `version_code must be greater than current active` | Old build uploaded | Increment `versionCode` in `build.gradle` |
|
|
417
|
+
| `RuStore did not respond to the upload of ... within 900s` | RuStore is still checking the file | Rerun the lane (the draft is replaced); if it repeats, raise `upload_timeout` |
|
|
418
|
+
| `API request failed [504] ... non-JSON response` | RuStore gateway timeout | Failure on the RuStore side; rerun the lane |
|
|
419
|
+
| `TLS handshake with public-api-m.rustore.ru failed` | Certificate chain not trusted, often a TLS-inspecting proxy | Check the proxy and `SSL_CERT_FILE` on the runner |
|
|
420
|
+
| `Could not connect to public-api-m.rustore.ru` | DNS, proxy or firewall | Check network access from the runner to port 443 |
|
|
421
|
+
| `APK/AAB contains new sensitive permissions` | The build declares new sensitive permissions | Upload the file in RuStore Console and fill in the permission declaration |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Development
|
|
426
|
+
|
|
427
|
+
```sh
|
|
428
|
+
git clone https://github.com/gorban-dev/rustore-fastlane-plugin
|
|
429
|
+
cd fastlane-plugin-rustore_publish
|
|
430
|
+
bundle install
|
|
431
|
+
bundle exec rspec # run tests
|
|
432
|
+
bundle exec rspec --format documentation # verbose output
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Releasing
|
|
436
|
+
|
|
437
|
+
1. Bump `VERSION` in `lib/fastlane/plugin/rustore_publish/version.rb`, run `bundle install` to update `Gemfile.lock`, add a `CHANGELOG.md` entry, push to `main`.
|
|
438
|
+
2. Publish a GitHub release with tag `vX.Y.Z` matching `VERSION`.
|
|
439
|
+
3. The [`Publish gem`](.github/workflows/release.yml) workflow runs the specs, checks the tag against `VERSION` and pushes the gem to RubyGems via Trusted Publishing.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## License
|
|
444
|
+
|
|
445
|
+
MIT
|