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 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
+ ![RuStore × fastlane — automated Android publishing](docs/assets/cover.jpg)
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