fastlane-plugin-rustored 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6c803f43cb99f9a9b6a73af925c53e4e66aaa9b4609856873d11c0c187384f7
4
- data.tar.gz: df043511b75ec03a565cd2b1cef586019c42a44c57036aea56e4c00572ef8f18
3
+ metadata.gz: c1147f69aca62793f1507a37818bf8dab5568b33e37df2a8ddfc74cefa73dcc8
4
+ data.tar.gz: 2689e265f60f6147cdf3c5daa87f61bcc226d3360c67a85103a01f7bd3545e55
5
5
  SHA512:
6
- metadata.gz: 2e797187bd06c5b623026943c0599677f4a282307e97f65ffd055c37985ee279ef1faddf47a105bd6cbc2830c9de3470546d4d7c34849da6e5f17ce9540acdc8
7
- data.tar.gz: e9df9811c147d34f58313f4ae9e2000dedc5d3f815c5be4111d1c8d0bbee76ad548929103f6332920fe6397f6cbace3a0d53cecac4ae907d964d2f5c98f2919f
6
+ metadata.gz: fe622044ff5b889921a385f1fecee80816d5290b3e420edc60e941a324a0dd9dbb96051b0d777c67e696dee88f64af593f4178b69d1d8986c1d931372abfa843
7
+ data.tar.gz: a56d20e1fa4728b51e878b1e423965f8640ca5389b765becb017fa29ea742ee538ecdb1542753860323c57cb0c152cabd2dd53b5b81f9009bd88f817a53436cc
data/README.md CHANGED
@@ -1,8 +1,18 @@
1
1
  # rustored
2
2
 
3
3
  [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-rustored)
4
+ ![Made with Love](https://img.shields.io/badge/Made%20with-❤️-ff69b4)
4
5
 
5
- Fastlane plugin for publishing Android builds to RuStore.
6
+ Fastlane plugin for uploading APK and AAB files to an existing RuStore draft version through the public API.
7
+
8
+ The plugin does not create a draft, send it to moderation, or publish it. Pass the `version_id` of a draft created in the RuStore Console or through the [RuStore API](https://www.rustore.ru/help/work-with-rustore-api/api-upload-publication-app/create-draft-version).
9
+
10
+ ## Prerequisites
11
+
12
+ - The application must have at least one active version in RuStore.
13
+ - The API key must have access to the application and the required APK/AAB upload method.
14
+ - A draft version and its `version_id` must already exist.
15
+ - Before uploading an AAB, configure its publication signature in the RuStore Console.
6
16
 
7
17
  ## Install
8
18
 
@@ -14,11 +24,12 @@ fastlane add_plugin rustored
14
24
 
15
25
  ### `rustored_publish_aab`
16
26
 
17
- Publishes an AAB file.
27
+ Uploads an AAB file to the draft.
18
28
 
19
29
  ```ruby
20
30
  rustored_publish_aab(
21
- token: ENV["RUSTORE_TOKEN"],
31
+ key_id: ENV["RUSTORE_KEY_ID"],
32
+ private_key: ENV["RUSTORE_PRIVATE_KEY"],
22
33
  package_name: "com.example.app",
23
34
  version_id: 123456,
24
35
  aab_path: "app/build/outputs/bundle/release/app-release.aab"
@@ -27,11 +38,12 @@ rustored_publish_aab(
27
38
 
28
39
  ### `rustored_publish_apk`
29
40
 
30
- Publishes an APK file.
41
+ Uploads an APK file to the draft.
31
42
 
32
43
  ```ruby
33
44
  rustored_publish_apk(
34
- token: ENV["RUSTORE_TOKEN"],
45
+ key_id: ENV["RUSTORE_KEY_ID"],
46
+ private_key: ENV["RUSTORE_PRIVATE_KEY"],
35
47
  package_name: "com.example.app",
36
48
  version_id: 123456,
37
49
  apk_path: "app/build/outputs/apk/release/app-release.apk",
@@ -40,13 +52,20 @@ rustored_publish_apk(
40
52
  )
41
53
  ```
42
54
 
55
+ The plugin signs the auth request and receives the RuStore public token automatically. Pass `private_key` exactly as copied from the RuStore Console: it is already Base64-encoded and must not be encoded again.
56
+
57
+ `services_type` accepts `Unknown` (the default) or `HMS`. `is_main_apk` is required by the RuStore API.
58
+
59
+ RuStore does not show an API-uploaded artifact in the Console until the draft is sent to moderation. Builds containing new sensitive permissions must be uploaded and declared through the Console instead.
60
+
43
61
  ## Options
44
62
 
45
63
  Shared:
46
64
 
47
65
  | Option | Env | Required |
48
66
  | --- | --- | --- |
49
- | `token` | `RUSTORE_TOKEN` | yes |
67
+ | `key_id` | `RUSTORE_KEY_ID` | yes |
68
+ | `private_key` | `RUSTORE_PRIVATE_KEY` | yes |
50
69
  | `package_name` | `RUSTORE_PACKAGE_NAME` | yes |
51
70
  | `version_id` | `RUSTORE_VERSION_ID` | yes |
52
71
 
@@ -61,12 +80,13 @@ APK:
61
80
  | Option | Env | Required |
62
81
  | --- | --- | --- |
63
82
  | `apk_path` | `RUSTORE_APK_PATH` | yes |
64
- | `services_type` | `RUSTORE_SERVICES_TYPE` | no |
83
+ | `services_type` | `RUSTORE_SERVICES_TYPE` | no (`Unknown`) |
65
84
  | `is_main_apk` | `RUSTORE_IS_MAIN_APK` | yes |
66
85
 
67
- ## Test
86
+ ## Development
68
87
 
69
88
  ```bash
70
- bundle exec rspec
71
- bundle exec rubocop
89
+ bundle exec rake
72
90
  ```
91
+
92
+ API reference: [authorization](https://www.rustore.ru/help/work-with-rustore-api/api-authorization-token), [APK upload](https://www.rustore.ru/help/work-with-rustore-api/api-upload-publication-app/apk-file-upload/file-upload-apk), and [AAB upload](https://www.rustore.ru/help/work-with-rustore-api/api-upload-publication-app/apk-file-upload/file-upload-aab).
@@ -9,7 +9,8 @@ module Fastlane
9
9
  class RustoredPublishAabAction < Action
10
10
  def self.run(params)
11
11
  Fastlane::Helper::RustoredHelper.publish_aab(
12
- token: params[:token],
12
+ key_id: params[:key_id],
13
+ private_key: params[:private_key],
13
14
  package_name: params[:package_name],
14
15
  version_id: params[:version_id],
15
16
  aab_path: params[:aab_path]
@@ -17,7 +18,7 @@ module Fastlane
17
18
  end
18
19
 
19
20
  def self.description
20
- "Publish an AAB file to RuStore"
21
+ "Upload an AAB file to an existing RuStore draft version"
21
22
  end
22
23
 
23
24
  def self.authors
@@ -25,7 +26,7 @@ module Fastlane
25
26
  end
26
27
 
27
28
  def self.details
28
- "Uploads an Android App Bundle to an existing RuStore app version."
29
+ "Uploads an Android App Bundle to an existing RuStore draft version."
29
30
  end
30
31
 
31
32
  def self.available_options
@@ -9,7 +9,8 @@ module Fastlane
9
9
  class RustoredPublishApkAction < Action
10
10
  def self.run(params)
11
11
  Fastlane::Helper::RustoredHelper.publish_apk(
12
- token: params[:token],
12
+ key_id: params[:key_id],
13
+ private_key: params[:private_key],
13
14
  package_name: params[:package_name],
14
15
  version_id: params[:version_id],
15
16
  services_type: params[:services_type],
@@ -19,7 +20,7 @@ module Fastlane
19
20
  end
20
21
 
21
22
  def self.description
22
- "Publish an APK file to RuStore"
23
+ "Upload an APK file to an existing RuStore draft version"
23
24
  end
24
25
 
25
26
  def self.authors
@@ -27,7 +28,7 @@ module Fastlane
27
28
  end
28
29
 
29
30
  def self.details
30
- "Uploads an APK to an existing RuStore app version."
31
+ "Uploads an APK to an existing RuStore draft version."
31
32
  end
32
33
 
33
34
  def self.available_options
@@ -44,7 +45,11 @@ module Fastlane
44
45
  env_name: "RUSTORE_SERVICES_TYPE",
45
46
  description: "The APK services type",
46
47
  optional: true,
47
- type: String
48
+ type: String,
49
+ default_value: "Unknown",
50
+ verify_block: proc do |value|
51
+ UI.user_error!("services_type must be HMS or Unknown") unless %w[HMS Unknown].include?(value)
52
+ end
48
53
  ),
49
54
  FastlaneCore::ConfigItem.new(
50
55
  key: :is_main_apk,
@@ -1,46 +1,51 @@
1
1
  require 'fastlane_core/ui/ui'
2
2
  require 'faraday'
3
- require 'json'
3
+ require 'faraday_middleware'
4
+ require_relative 'rustored_token_helper'
4
5
 
5
6
  module Fastlane
6
7
  UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
7
8
 
8
9
  module Helper
9
10
  class RustoredHelper
10
- BASE_URL = 'https://public-api.rustore.ru/public/v1'
11
+ BASE_URL = 'https://public-api.rustore.ru'
11
12
 
12
13
  def self.client
13
14
  @client ||= Faraday.new(url: BASE_URL) do |f|
14
- f.request(:json)
15
15
  f.request(:multipart)
16
16
  f.response(:json)
17
17
  end
18
18
  end
19
19
 
20
- def self.publish_aab(token:, package_name:, version_id:, aab_path:)
21
- UI.message("Publishing AAB to Rustore...")
20
+ def self.publish_aab(key_id:, private_key:, package_name:, version_id:, aab_path:)
21
+ UI.user_error!("AAB file not found: #{aab_path}") unless File.file?(aab_path)
22
+ UI.message("Uploading AAB to RuStore...")
22
23
 
23
- response = client.post("/application/#{package_name}/version/#{version_id}/aab") do |req|
24
+ token = RustoredTokenHelper.get_token(key_id: key_id, private_key: private_key)
25
+
26
+ response = client.post("/public/v1/application/#{package_name}/version/#{version_id}/aab") do |req|
24
27
  req.headers['Public-Token'] = token
25
28
  req.body = {
26
29
  file: Faraday::UploadIO.new(aab_path, 'application/octet-stream')
27
30
  }
28
31
  end
29
32
 
30
- if response.success? && response.body['code'] == 'OK'
31
- UI.success("AAB published successfully!")
32
- else
33
- UI.error("Failed to publish AAB: #{response.status} - #{response.body['message']}")
34
- end
33
+ check_response!(response, 'AAB')
34
+ UI.success("AAB uploaded successfully!")
35
+ rescue Faraday::Error => e
36
+ UI.user_error!("Failed to upload AAB: #{e.message}")
35
37
  end
36
38
 
37
- def self.publish_apk(token:, package_name:, version_id:, services_type:, is_main_apk:, apk_path:)
38
- UI.message("Publishing APK to Rustore...")
39
+ def self.publish_apk(key_id:, private_key:, package_name:, version_id:, services_type:, is_main_apk:, apk_path:)
40
+ UI.user_error!("APK file not found: #{apk_path}") unless File.file?(apk_path)
41
+ UI.message("Uploading APK to RuStore...")
39
42
 
40
- response = client.post("/application/#{package_name}/version/#{version_id}/apk") do |req|
43
+ token = RustoredTokenHelper.get_token(key_id: key_id, private_key: private_key)
44
+
45
+ response = client.post("/public/v1/application/#{package_name}/version/#{version_id}/apk") do |req|
41
46
  req.headers['Public-Token'] = token
42
47
 
43
- req.params['servicesType'] = services_type
48
+ req.params['servicesType'] = services_type if services_type
44
49
  req.params['isMainApk'] = is_main_apk
45
50
 
46
51
  req.body = {
@@ -48,12 +53,20 @@ module Fastlane
48
53
  }
49
54
  end
50
55
 
51
- if response.success? && response.body['code'] == 'OK'
52
- UI.success("APK published successfully!")
53
- else
54
- UI.error("Failed to publish APK: #{response.status} - #{response.body['message']}")
55
- end
56
+ check_response!(response, 'APK')
57
+ UI.success("APK uploaded successfully!")
58
+ rescue Faraday::Error => e
59
+ UI.user_error!("Failed to upload APK: #{e.message}")
60
+ end
61
+
62
+ def self.check_response!(response, artifact)
63
+ body = response.body.kind_of?(Hash) ? response.body : {}
64
+ return if response.success? && body['code'] == 'OK'
65
+
66
+ error = body['message'] || body['code'] || 'Unknown RuStore response'
67
+ UI.user_error!("Failed to upload #{artifact}: #{response.status} - #{error}")
56
68
  end
69
+ private_class_method(:check_response!)
57
70
  end
58
71
  end
59
72
  end
@@ -6,12 +6,21 @@ module Fastlane
6
6
  def self.common_options
7
7
  [
8
8
  FastlaneCore::ConfigItem.new(
9
- key: :token,
10
- env_name: "RUSTORE_TOKEN",
11
- description: "The token for authenticating with RuStore",
9
+ key: :key_id,
10
+ env_name: "RUSTORE_KEY_ID",
11
+ description: "The key id for authenticating with RuStore",
12
12
  optional: false,
13
13
  type: String
14
14
  ),
15
+ FastlaneCore::ConfigItem.new(
16
+ key: :private_key,
17
+ env_name: "RUSTORE_PRIVATE_KEY",
18
+ description: "The private key for authenticating with RuStore",
19
+ optional: false,
20
+ type: String,
21
+ sensitive: true,
22
+ display_in_shell: false
23
+ ),
15
24
  FastlaneCore::ConfigItem.new(
16
25
  key: :package_name,
17
26
  env_name: "RUSTORE_PACKAGE_NAME",
@@ -0,0 +1,58 @@
1
+ require 'fastlane_core/ui/ui'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'openssl'
5
+ require 'time'
6
+ require 'faraday'
7
+ require 'faraday_middleware'
8
+
9
+ module Fastlane
10
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
11
+
12
+ module Helper
13
+ class RustoredTokenHelper
14
+ BASE_URL = 'https://public-api.rustore.ru'
15
+
16
+ def self.client
17
+ @client ||= Faraday.new(url: BASE_URL) do |f|
18
+ f.request(:json)
19
+ f.response(:json)
20
+ end
21
+ end
22
+
23
+ def self.gen_token_signature(key_id:, private_key:, timestamp:)
24
+ private_key_data = Base64.strict_decode64(private_key)
25
+ rsa_private_key = OpenSSL::PKey::RSA.new(private_key_data)
26
+
27
+ message_to_sign = key_id + timestamp
28
+
29
+ digest = OpenSSL::Digest.new('SHA512')
30
+ signature_bytes = rsa_private_key.sign(digest, message_to_sign)
31
+ Base64.strict_encode64(signature_bytes)
32
+ end
33
+
34
+ def self.get_token(key_id:, private_key:)
35
+ timestamp = Time.now.utc.iso8601(3)
36
+ sig = gen_token_signature(key_id: key_id, private_key: private_key, timestamp: timestamp)
37
+
38
+ response = client.post('/public/auth') do |req|
39
+ req.body = {
40
+ keyId: key_id,
41
+ signature: sig,
42
+ timestamp: timestamp
43
+ }
44
+ end
45
+
46
+ body = response.body.kind_of?(Hash) ? response.body : {}
47
+ token = body.dig('body', 'jwe')
48
+ return token if response.success? && body['code'] == 'OK' && !token.to_s.empty?
49
+
50
+ UI.user_error!("Failed to get token: #{response.status} - #{body['message'] || body['code'] || 'Unknown RuStore response'}")
51
+ rescue ArgumentError, OpenSSL::PKey::PKeyError => e
52
+ UI.user_error!("Invalid RuStore private key: #{e.message}")
53
+ rescue Faraday::Error => e
54
+ UI.user_error!("Failed to get RuStore token: #{e.message}")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Rustored
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-rustored
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - 06ED
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.8'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday_middleware
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
12
40
  email: hsbest123@gmail.com
13
41
  executables: []
14
42
  extensions: []
@@ -21,6 +49,7 @@ files:
21
49
  - lib/fastlane/plugin/rustored/actions/rustored_publish_apk_action.rb
22
50
  - lib/fastlane/plugin/rustored/helper/rustored_helper.rb
23
51
  - lib/fastlane/plugin/rustored/helper/rustored_options.rb
52
+ - lib/fastlane/plugin/rustored/helper/rustored_token_helper.rb
24
53
  - lib/fastlane/plugin/rustored/version.rb
25
54
  homepage: https://github.com/06ED/fastlane-rustored
26
55
  licenses: