Fastlane plugin for publishing Android applications to RuStore — the Russian app store — via the RuStore Public API.
- Upload AAB (Google/GMS, main artifact) and APK (Huawei/HMS, secondary) in a single version
- Full publish workflow: authenticate → create draft → upload files → submit for moderation → configure publication
- The pipeline always finishes immediately after submission — no waiting for moderation
- GitLab CI collapsible log sections (foldable blocks in Pipeline UI)
- Staged rollout support (
rollout_percentage) - Scheduled publishing (
DELAYED+release_date) - Auto-cleanup of existing drafts before creating a new one
- JWE token auto-refresh (900 s TTL)
- Fastlane ≥ 2.200.0
- Ruby ≥ 2.6
- At least one active published version must already exist in RuStore Console (API requirement)
Add the plugin to your Pluginfile:
# Pluginfile
gem "fastlane-plugin-rustore_publish"Then run:
fastlane install_plugins- Log in to RuStore Console with a company owner account
- Go to Company → API RuStore
- Click Generate a key, give it a name, select your app and required API methods
- Download the private key (PEM file) — save it securely, RuStore does not store it
You will get two values:
- Key ID (
key_id) — shown in the Console after generation - Private key — the downloaded
.pemfile
| Parameter | Env var | Required | Default | Description |
|---|---|---|---|---|
key_id |
RUSTORE_KEY_ID |
yes | — | API key ID from RuStore Console |
private_key_path |
RUSTORE_PRIVATE_KEY_PATH |
one of two | — | Path to RSA private key PEM file |
private_key |
RUSTORE_PRIVATE_KEY |
one of two | — | PEM content as string (for CI secrets) |
package_name |
RUSTORE_PACKAGE_NAME |
yes | — | App package name, e.g. com.example.app |
aab_path |
RUSTORE_AAB_PATH |
one of two | — | Path to AAB (Google/GMS — becomes main artifact) |
apk_path |
RUSTORE_APK_PATH |
one of two | — | Path to APK (used when aab_path not set) |
hms_apk_path |
RUSTORE_HMS_APK_PATH |
no | — | Path to Huawei/HMS APK (servicesType=HMS, isMainApk=false) |
whats_new |
RUSTORE_WHATS_NEW |
no | — | Release notes shown to users (plain text, max 5000 chars) |
moder_info |
RUSTORE_MODER_INFO |
no | — | Notes for the RuStore moderator, e.g. test credentials (max 180 chars) |
publish_type |
RUSTORE_PUBLISH_TYPE |
no | INSTANTLY |
INSTANTLY / MANUAL / DELAYED |
release_date |
RUSTORE_RELEASE_DATE |
no | — | ISO 8601 datetime, only for DELAYED |
rollout_percentage |
RUSTORE_ROLLOUT_PERCENTAGE |
no | — | Staged rollout: allowed values 5, 10, 25, 50, 75, 100 |
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.
| Parameter | Env var | Type | Description |
|---|---|---|---|
developer_contacts |
— | Hash | { email:, website:, vk_community: } — at least one sub-key required |
app_name |
RUSTORE_APP_NAME |
String | App display name, max 50 chars |
app_type |
RUSTORE_APP_TYPE |
String | GAMES or MAIN |
categories |
— | Array<String> | Up to 2 categories, e.g. ["news", "education"] |
age_legal |
RUSTORE_AGE_LEGAL |
String | One of 0+, 6+, 12+, 16+, 18+ |
short_description |
RUSTORE_SHORT_DESCRIPTION |
String | Brief description, max 80 chars |
full_description |
RUSTORE_FULL_DESCRIPTION |
String | Full description, max 4000 chars |
price_value |
RUSTORE_PRICE_VALUE |
String | Price in kopecks as digits, e.g. "8799" for 87.99 RUB |
seo_tag_ids |
— | Array<Integer> | Up to 5 search tag IDs |
faq |
— | Array<Hash> | Up to 10 items, each { question: ≤120 chars, answer: ≤500 chars } |
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:
developer_contacts: JSON.parse(ENV.fetch("RUSTORE_DEVELOPER_CONTACTS"), symbolize_names: true)Example:
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key: ENV["RUSTORE_PRIVATE_KEY"],
aab_path: "app/build/outputs/bundle/release/app-release.aab",
# Draft metadata (v2)
developer_contacts: {
email: "support@example.com",
website: "https://example.com",
vk_community: "https://vk.com/example"
},
app_name: "Example App",
app_type: "MAIN",
categories: ["news", "education"],
age_legal: "0+",
short_description: "Best example app",
full_description: "A longer description of what the app does and why people should install it...",
faq: [
{ question: "How do I sign in?", answer: "Use your email and password." },
{ question: "Is it free?", answer: "Yes." }
]
)publish_type |
Behaviour |
|---|---|
INSTANTLY |
RuStore auto-publishes after moderation passes (default) |
DELAYED |
Scheduled publication via release_date in ISO 8601 |
MANUAL |
Moderation runs normally; publish manually from RuStore Console |
The pipeline finishes immediately after submitting for moderation for all three types.
# fastlane/Fastfile
lane :deploy_rustore do
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key_path: "fastlane/rustore_private_key.pem",
aab_path: "app/build/outputs/bundle/release/app-release.aab"
)
endlane :deploy_rustore do
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key: ENV["RUSTORE_PRIVATE_KEY"],
# Primary build — Google/GMS (AAB is always isMainApk)
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
# Secondary build — Huawei/HMS (servicesType=HMS, isMainApk=false)
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
publish_type: "INSTANTLY",
rollout_percentage: 25 # release to 25% of users first
)
endlane :deploy_rustore_scheduled do
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key: ENV["RUSTORE_PRIVATE_KEY"],
aab_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
publish_type: "DELAYED",
release_date: "2025-03-01T10:00:00+03:00" # Moscow time
)
endlane :build_and_deploy do
gradle(task: "bundle", flavor: "gms", build_type: "Release")
gradle(task: "assemble", flavor: "hms", build_type: "Release")
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key: ENV["RUSTORE_PRIVATE_KEY"],
aab_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
publish_type: "INSTANTLY"
)
end# .gitlab-ci.yml
variables:
RUSTORE_KEY_ID: $RUSTORE_KEY_ID # set in GitLab CI/CD → Variables
RUSTORE_PRIVATE_KEY: $RUSTORE_PRIVATE_KEY # set as "File" type variable — path to PEM
stages:
- build
- deploy
build:
stage: build
script:
- bundle exec fastlane build_release
artifacts:
paths:
- app/build/outputs/bundle/gmsRelease/
- app/build/outputs/apk/hmsRelease/
expire_in: 1 hour
deploy_rustore:
stage: deploy
needs: [build]
script:
- bundle exec fastlane deploy_rustore
environment:
name: production
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ # run only on version tags# fastlane/Fastfile
lane :deploy_rustore do
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key_path: ENV["RUSTORE_PRIVATE_KEY"], # "File" variable = path to PEM
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
publish_type: "INSTANTLY"
)
endStoring the private key in GitLab:
- Go to Settings → CI/CD → Variables
- Add
RUSTORE_KEY_IDas a regular masked variable - Add
RUSTORE_PRIVATE_KEYas type: File — GitLab writes the PEM to a temp file and exports the path. Useprivate_key_path: ENV["RUSTORE_PRIVATE_KEY"] - Alternatively, paste the PEM content as a masked variable and use
private_key: ENV["RUSTORE_PRIVATE_KEY"]
GitLab CI log output (steps appear as collapsible sections):
▶ [RuStore] Step 1/5: Authentication ← click to expand
▶ [RuStore] Step 2/5: Draft Management
▶ [RuStore] Step 3/5: Uploading AAB (GMS/main)
▶ [RuStore] Step 4/5: Uploading APK (HMS/secondary)
▶ [RuStore] Step 5/5: Submitting for moderation
[RuStore] ✓ All done! com.example.app versionId=12345 submitted to RuStore.
# .github/workflows/deploy-rustore.yml
name: Deploy to RuStore
on:
push:
tags:
- 'v*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Build release
run: bundle exec fastlane build_release
- name: Deploy to RuStore
env:
RUSTORE_KEY_ID: ${{ secrets.RUSTORE_KEY_ID }}
RUSTORE_PRIVATE_KEY: ${{ secrets.RUSTORE_PRIVATE_KEY }}
run: bundle exec fastlane deploy_rustore# fastlane/Fastfile
lane :deploy_rustore do
rustore_upload(
package_name: "com.example.app",
key_id: ENV["RUSTORE_KEY_ID"],
private_key: ENV["RUSTORE_PRIVATE_KEY"], # PEM content from GitHub secret
aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",
hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",
publish_type: "INSTANTLY"
)
endStoring secrets in GitHub:
- Go to Settings → Secrets and variables → Actions
- Add
RUSTORE_KEY_ID— the key ID string - Add
RUSTORE_PRIVATE_KEY— paste the full PEM content (including-----BEGIN/END PRIVATE KEY-----lines)
# bitrise.yml (relevant step)
- fastlane@3:
inputs:
- lane: deploy_rustore
envs:
- RUSTORE_KEY_ID: $RUSTORE_KEY_ID
- RUSTORE_PRIVATE_KEY: $RUSTORE_PRIVATE_KEYAdd RUSTORE_KEY_ID and RUSTORE_PRIVATE_KEY as Secret Environment Variables in Bitrise Secrets tab.
RuStore allows up to 1 AAB + 8 APK (or 10 APK) files per version. The plugin supports the most common scenario:
Version
├── app-gms-release.aab ← AAB, servicesType=Unknown, isMainApk=true (implicit)
└── app-hms-release.apk ← APK, servicesType=HMS, isMainApk=false
RuStore automatically serves the appropriate file to each user based on their device.
Enable verbose HTTP logging:
RUSTORE_DEBUG=1 bundle exec fastlane deploy_rustoreRun Fastlane with full verbose output:
bundle exec fastlane deploy_rustore --verbose| Error | Cause | Fix |
|---|---|---|
Authentication failed: ... |
Wrong key_id or corrupted key |
Check key in RuStore Console → API RuStore |
Failed to load RSA private key |
Invalid PEM format | Ensure the key includes -----BEGIN/END PRIVATE KEY----- |
API request failed [404] |
Package not found | Verify package_name; at least 1 active version must exist in RuStore Console |
API request failed [403] |
Key lacks permissions | Ensure the key has access to the required API methods for your app in Console |
version_code must be greater than current active |
Old build uploaded | Increment versionCode in build.gradle |
Moderation declined |
RuStore reviewer rejected the update | Check reviewer comments in RuStore Console |
git clone https://github.com/gorban-dev/rustore-fastlane-plugin
cd fastlane-plugin-rustore_publish
bundle install
bundle exec rspec # run tests
bundle exec rspec --format documentation # verbose outputMIT