supply 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +151 -0
- data/bin/supply +6 -0
- data/lib/supply.rb +24 -0
- data/lib/supply/client.rb +297 -0
- data/lib/supply/commands_generator.rb +62 -0
- data/lib/supply/listing.rb +32 -0
- data/lib/supply/options.rb +56 -0
- data/lib/supply/setup.rb +82 -0
- data/lib/supply/uploader.rb +89 -0
- data/lib/supply/version.rb +4 -0
- metadata +213 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a1f6aec39713c442c7cf6c016adf72de551c4840
|
4
|
+
data.tar.gz: 4c90a7c8593ee2bdd1aadf4a2b7cde5ccf634391
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 172083dc2933041068b9baa25ae195459a80a190b13a7e8c7444586202965320810ef7f6e1ddc3dd9c8bcb6ecbce50653f797718c3625f5fa705999704f99a8c
|
7
|
+
data.tar.gz: 7fec136b36bda2b83559e68af798560e28058de0a51c792ae9675454a2697760deb87eb237899856453a93557b282acb4ed9422cbeeb6528c0cf6b81deb92afc
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Felix Krause
|
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.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
<h3 align="center">
|
2
|
+
<a href="https://github.com/KrauseFx/fastlane">
|
3
|
+
<img src="assets/fastlane.png" width="150" />
|
4
|
+
<br />
|
5
|
+
fastlane
|
6
|
+
</a>
|
7
|
+
</h3>
|
8
|
+
<p align="center">
|
9
|
+
<a href="https://github.com/KrauseFx/deliver">deliver</a> •
|
10
|
+
<a href="https://github.com/KrauseFx/snapshot">snapshot</a> •
|
11
|
+
<a href="https://github.com/KrauseFx/frameit">frameit</a> •
|
12
|
+
<a href="https://github.com/KrauseFx/PEM">PEM</a> •
|
13
|
+
<a href="https://github.com/KrauseFx/sigh">sigh</a> •
|
14
|
+
<a href="https://github.com/KrauseFx/produce">produce</a> •
|
15
|
+
<a href="https://github.com/KrauseFx/cert">cert</a> •
|
16
|
+
<a href="https://github.com/KrauseFx/codes">codes</a> •
|
17
|
+
<a href="https://github.com/fastlane/spaceship">spaceship</a> •
|
18
|
+
<a href="https://github.com/fastlane/pilot">pilot</a> •
|
19
|
+
<a href="https://github.com/fastlane/boarding">boarding</a> •
|
20
|
+
<a href="https://github.com/fastlane/gym">gym</a>
|
21
|
+
</p>
|
22
|
+
-------
|
23
|
+
|
24
|
+
<p align="center">
|
25
|
+
<img src="assets/supply.png" height="110">
|
26
|
+
</p>
|
27
|
+
|
28
|
+
supply
|
29
|
+
============
|
30
|
+
|
31
|
+
[![Twitter: @KauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx)
|
32
|
+
[![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/KrauseFx/supply/blob/master/LICENSE)
|
33
|
+
[![Gem](https://img.shields.io/gem/v/supply.svg?style=flat)](http://rubygems.org/gems/supply)
|
34
|
+
[![Build Status](https://img.shields.io/travis/KrauseFx/supply/master.svg?style=flat)](https://travis-ci.org/KrauseFx/supply)
|
35
|
+
|
36
|
+
###### Command line tool for updating Android apps and their metadata on the Google Play Store
|
37
|
+
|
38
|
+
Get in contact with the developer on Twitter: [@KrauseFx](https://twitter.com/KrauseFx)
|
39
|
+
|
40
|
+
|
41
|
+
-------
|
42
|
+
<p align="center">
|
43
|
+
<a href="#features">Features</a> •
|
44
|
+
<a href="#installation">Installation</a> •
|
45
|
+
<a href="#setup">Setup</a> •
|
46
|
+
<a href="#quick-start">Quick Start</a> •
|
47
|
+
<a href="#available-commands">Available Commands</a> •
|
48
|
+
<a href="#uploading-an-apk">Uploading an APK</a> •
|
49
|
+
<a href="#images-and-screenshots">Images and Screenshots</a> •
|
50
|
+
</p>
|
51
|
+
|
52
|
+
-------
|
53
|
+
|
54
|
+
<h5 align="center"><code>supply</code> is part of <a href="https://fastlane.tools">fastlane</a>: connect all deployment tools into one streamlined workflow.</h5>
|
55
|
+
|
56
|
+
## Features
|
57
|
+
- Update existing Android applications on Google Play via the command line
|
58
|
+
- Upload new builds (APKs)
|
59
|
+
- Retrieve and edit metadata, such as title and description, for multiple languages
|
60
|
+
- Upload the app icon, promo graphics and screenshots for multiple languages
|
61
|
+
- Have a local copy of the metadata in your git repository
|
62
|
+
|
63
|
+
##### [Like this tool? Be the first to know about updates and new fastlane tools](https://tinyletter.com/krausefx)
|
64
|
+
|
65
|
+
## Installation
|
66
|
+
|
67
|
+
Install the gem
|
68
|
+
|
69
|
+
sudo gem install supply
|
70
|
+
|
71
|
+
## Setup
|
72
|
+
|
73
|
+
- Open the [Google Play Console](https://play.google.com/apps/publish/)
|
74
|
+
- Open _Settings => API-Access_
|
75
|
+
- Create a new Service Account - follow the link of the dialog
|
76
|
+
- Create new Client ID
|
77
|
+
- Select _Service Account_
|
78
|
+
- Click _Generate new P12 key_ and store the downloaded file
|
79
|
+
- Make a note of the _Email address_ underneath _Service account_ - this is the issuer which you will need later
|
80
|
+
- Back on the Google Play developer console, click on _Grant Access_ for the newly added service account
|
81
|
+
- Choose _Release Manager_ from the dropdown and confirm
|
82
|
+
|
83
|
+
## Quick Start
|
84
|
+
|
85
|
+
- `cd [your_project_folder]`
|
86
|
+
- `supply init`
|
87
|
+
- Make changes to the downloaded metadata, add images, screenshots and/or an APK
|
88
|
+
- `supply run`
|
89
|
+
|
90
|
+
## Available Commands
|
91
|
+
|
92
|
+
- `supply`: update an app with metadata, a build, images and screenshots
|
93
|
+
- `supply init`: download metadata for an existing app to a local directory
|
94
|
+
- `supply --help`: show information on available commands, arguments and environment variables
|
95
|
+
|
96
|
+
You can either run `supply` on its own and use it interactively, or you can pass arguments or specify environment variables for all the options to skip the questions.
|
97
|
+
|
98
|
+
## Uploading an APK
|
99
|
+
|
100
|
+
Simply run `supply --apk path/to/app.apk`, or use the `SUPPLY_APK` environment variable.
|
101
|
+
|
102
|
+
This will also upload app metadata if you previously ran `supply init`.
|
103
|
+
|
104
|
+
## Images and Screenshots
|
105
|
+
|
106
|
+
After running `supply init`, you will have a metadata directory. This directory contains one or more locale directories (e.g. en-US, en-GB, etc.), and inside this directory are text files such as `title.txt` and `short.txt`.
|
107
|
+
|
108
|
+
Here you can supply images with the following file names (extension can be png, jpg or jpeg):
|
109
|
+
|
110
|
+
- `featureGraphic`
|
111
|
+
- `icon`
|
112
|
+
- `promoGraphic`
|
113
|
+
- `tvBanner`
|
114
|
+
|
115
|
+
And you can supply screenshots by creating directories with the following names, containing PNGs or JPEGs (image names are irrelevant):
|
116
|
+
|
117
|
+
- `phoneScreenshots/`
|
118
|
+
- `sevenInchScreenshots/` (7-inch tablets)
|
119
|
+
- `tenInchScreenshots/` (10-inch tablets)
|
120
|
+
- `tvScreenshots/`
|
121
|
+
|
122
|
+
Note that these will replace the current images and screenshots on the play store listing, not add to them.
|
123
|
+
|
124
|
+
## Tips
|
125
|
+
|
126
|
+
### [`fastlane`](https://fastlane.tools) Toolchain
|
127
|
+
|
128
|
+
- [`fastlane`](https://fastlane.tools): Connect all deployment tools into one streamlined workflow
|
129
|
+
- [`deliver`](https://github.com/KrauseFx/deliver): Upload screenshots, metadata and your app to the App Store
|
130
|
+
- [`snapshot`](https://github.com/KrauseFx/snapshot): Automate taking localized screenshots of your iOS app on every device
|
131
|
+
- [`frameit`](https://github.com/KrauseFx/frameit): Quickly put your screenshots into the right device frames
|
132
|
+
- [`PEM`](https://github.com/KrauseFx/pem): Automatically generate and renew your push notification profiles
|
133
|
+
- [`sigh`](https://github.com/KrauseFx/sigh): Because you would rather spend your time building stuff than fighting provisioning
|
134
|
+
- [`produce`](https://github.com/KrauseFx/produce): Create new iOS apps on iTunes Connect and Dev Portal using the command line
|
135
|
+
- [`cert`](https://github.com/KrauseFx/cert): Automatically create and maintain iOS code signing certificates
|
136
|
+
- [`codes`](https://github.com/KrauseFx/codes): Create promo codes for iOS Apps using the command line
|
137
|
+
- [`spaceship`](https://github.com/fastlane/spaceship): Ruby library to access the Apple Dev Center and iTunes Connect
|
138
|
+
- [`pilot`](https://github.com/fastlane/pilot): The best way to manage your TestFlight testers and builds from your terminal
|
139
|
+
- [`boarding`](https://github.com/fastlane/boarding): The easiest way to invite your TestFlight beta testers
|
140
|
+
- [`gym`](https://github.com/fastlane/gym): Building your iOS apps has never been easier
|
141
|
+
|
142
|
+
##### [Like this tool? Be the first to know about updates and new fastlane tools](https://tinyletter.com/krausefx)
|
143
|
+
|
144
|
+
# Need help?
|
145
|
+
Please submit an issue on GitHub and provide information about your setup
|
146
|
+
|
147
|
+
## License
|
148
|
+
|
149
|
+
This project is licensed under the terms of the MIT license. See the LICENSE file.
|
150
|
+
|
151
|
+
> This project and all fastlane tools are in no way affiliated with Apple Inc. This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. All fastlane tools run on your own computer or server, so your credentials or other sensitive information will never leave your own computer. You are responsible for how you use fastlane tools.
|
data/bin/supply
ADDED
data/lib/supply.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'supply/version'
|
3
|
+
require 'supply/options'
|
4
|
+
require 'supply/client'
|
5
|
+
require 'supply/listing'
|
6
|
+
require 'supply/uploader'
|
7
|
+
|
8
|
+
require 'fastlane_core'
|
9
|
+
|
10
|
+
module Supply
|
11
|
+
# Use this to just setup the configuration attribute and set it later somewhere else
|
12
|
+
class << self
|
13
|
+
attr_accessor :config
|
14
|
+
end
|
15
|
+
|
16
|
+
AVAILABLE_METADATA_FIELDS = %w(title short_description full_description video)
|
17
|
+
IMAGES_TYPES = %w(featureGraphic icon promoGraphic tvBanner)
|
18
|
+
SCREENSHOT_TYPES = %w(phoneScreenshots sevenInchScreenshots tenInchScreenshots tvScreenshots wearScreenshots)
|
19
|
+
|
20
|
+
IMAGES_FOLDER_NAME = "images"
|
21
|
+
IMAGE_FILE_EXTENSIONS = "{png,jpg,jpeg}"
|
22
|
+
|
23
|
+
Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
|
24
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'google/api_client'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Supply
|
5
|
+
class Client
|
6
|
+
# Connecting with Google
|
7
|
+
attr_accessor :auth_client
|
8
|
+
attr_accessor :api_client
|
9
|
+
attr_accessor :android_publisher
|
10
|
+
|
11
|
+
# Editing something
|
12
|
+
# Reference to the entry we're currently editing. Might be nil if don't have one open
|
13
|
+
attr_accessor :current_edit
|
14
|
+
# Package name of the currently edited element
|
15
|
+
attr_accessor :current_package_name
|
16
|
+
|
17
|
+
#####################################################
|
18
|
+
# @!group Login
|
19
|
+
#####################################################
|
20
|
+
|
21
|
+
# Initializes the auth_client and api_client using the specified information
|
22
|
+
# @param path_to_key: The path to your p12 file
|
23
|
+
# @param issuer: Email addresss for oauth
|
24
|
+
# @param passphrase: Passphrase for the p12 file
|
25
|
+
def initialize(path_to_key: nil, issuer: nil, passphrase: nil)
|
26
|
+
passphrase ||= "notasecret"
|
27
|
+
|
28
|
+
key = Google::APIClient::KeyUtils.load_from_pkcs12(File.expand_path(path_to_key), passphrase)
|
29
|
+
|
30
|
+
begin
|
31
|
+
self.auth_client = Signet::OAuth2::Client.new(
|
32
|
+
token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
|
33
|
+
audience: 'https://accounts.google.com/o/oauth2/token',
|
34
|
+
scope: 'https://www.googleapis.com/auth/androidpublisher',
|
35
|
+
issuer: issuer,
|
36
|
+
signing_key: key
|
37
|
+
)
|
38
|
+
rescue => ex
|
39
|
+
Helper.log.fatal ex
|
40
|
+
raise "Authentification unsuccessful, make sure to pass a valid key file".red
|
41
|
+
end
|
42
|
+
|
43
|
+
Helper.log.debug "Fetching a new access token from Google..."
|
44
|
+
|
45
|
+
self.auth_client.fetch_access_token!
|
46
|
+
|
47
|
+
self.api_client = Google::APIClient.new(
|
48
|
+
application_name: "fastlane - supply",
|
49
|
+
application_version: Supply::VERSION
|
50
|
+
)
|
51
|
+
|
52
|
+
self.android_publisher = api_client.discovered_api('androidpublisher', 'v2')
|
53
|
+
end
|
54
|
+
|
55
|
+
#####################################################
|
56
|
+
# @!group Handling the edit lifecycle
|
57
|
+
#####################################################
|
58
|
+
|
59
|
+
# Begin modifying a certain package
|
60
|
+
def begin_edit(package_name: nil)
|
61
|
+
raise "You currently have an active edit" if @current_edit
|
62
|
+
|
63
|
+
self.current_edit = api_client.execute(
|
64
|
+
api_method: android_publisher.edits.insert,
|
65
|
+
parameters: { 'packageName' => package_name },
|
66
|
+
authorization: auth_client
|
67
|
+
)
|
68
|
+
|
69
|
+
if current_edit.error?
|
70
|
+
error_message = current_edit.error_message
|
71
|
+
self.current_edit = nil
|
72
|
+
raise error_message
|
73
|
+
end
|
74
|
+
|
75
|
+
self.current_package_name = package_name
|
76
|
+
end
|
77
|
+
|
78
|
+
# Aborts the current edit deleting all pending changes
|
79
|
+
def abort_current_edit
|
80
|
+
ensure_active_edit!
|
81
|
+
|
82
|
+
result = api_client.execute(
|
83
|
+
api_method: android_publisher.edits.delete,
|
84
|
+
parameters: {
|
85
|
+
'editId' => current_edit.data.id,
|
86
|
+
'packageName' => current_package_name
|
87
|
+
},
|
88
|
+
authorization: auth_client
|
89
|
+
)
|
90
|
+
|
91
|
+
raise result.error_message.red if result.error?
|
92
|
+
|
93
|
+
self.current_edit = nil
|
94
|
+
self.current_package_name = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Commits the current edit saving all pending changes on Google Play
|
98
|
+
def commit_current_edit!
|
99
|
+
ensure_active_edit!
|
100
|
+
|
101
|
+
result = api_client.execute(
|
102
|
+
api_method: android_publisher.edits.commit,
|
103
|
+
parameters: {
|
104
|
+
'editId' => current_edit.data.id,
|
105
|
+
'packageName' => current_package_name
|
106
|
+
},
|
107
|
+
authorization: auth_client
|
108
|
+
)
|
109
|
+
|
110
|
+
raise result.error_message.red if result.error?
|
111
|
+
|
112
|
+
self.current_edit = nil
|
113
|
+
self.current_package_name = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
#####################################################
|
117
|
+
# @!group Getting data
|
118
|
+
#####################################################
|
119
|
+
|
120
|
+
# Get a list of all languages - returns the list
|
121
|
+
# make sure to have an active edit
|
122
|
+
def listings
|
123
|
+
ensure_active_edit!
|
124
|
+
|
125
|
+
result = api_client.execute(
|
126
|
+
api_method: android_publisher.edits.listings.list,
|
127
|
+
parameters: {
|
128
|
+
'editId' => current_edit.data.id,
|
129
|
+
'packageName' => current_package_name
|
130
|
+
},
|
131
|
+
authorization: auth_client
|
132
|
+
)
|
133
|
+
|
134
|
+
raise result.error_message.red if result.error? && result.status != 404
|
135
|
+
|
136
|
+
return result.data.listings.collect do |row|
|
137
|
+
Listing.new(self, row.language, row)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns the listing for the given language filled with the current values if it already exists
|
142
|
+
def listing_for_language(language)
|
143
|
+
ensure_active_edit!
|
144
|
+
|
145
|
+
result = api_client.execute(
|
146
|
+
api_method: android_publisher.edits.listings.get,
|
147
|
+
parameters: {
|
148
|
+
'editId' => current_edit.data.id,
|
149
|
+
'packageName' => current_package_name,
|
150
|
+
'language' => language
|
151
|
+
},
|
152
|
+
authorization: auth_client
|
153
|
+
)
|
154
|
+
|
155
|
+
raise result.error_message.red if result.error? && result.status != 404
|
156
|
+
|
157
|
+
if result.status == 404
|
158
|
+
return Listing.new(self, language) # create a new empty listing
|
159
|
+
else
|
160
|
+
return Listing.new(self, language, result.data)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
#####################################################
|
165
|
+
# @!group Modifying data
|
166
|
+
#####################################################
|
167
|
+
|
168
|
+
# Updates or creates the listing for the specified language
|
169
|
+
def update_listing_for_language(language: nil, title: nil, short_description: nil, full_description: nil, video: nil)
|
170
|
+
ensure_active_edit!
|
171
|
+
|
172
|
+
listing = {
|
173
|
+
'language' => language,
|
174
|
+
'title' => title,
|
175
|
+
'fullDescription' => full_description,
|
176
|
+
'shortDescription' => short_description,
|
177
|
+
'video' => video
|
178
|
+
}
|
179
|
+
|
180
|
+
result = api_client.execute(
|
181
|
+
api_method: android_publisher.edits.listings.update,
|
182
|
+
parameters: {
|
183
|
+
'editId' => current_edit.data.id,
|
184
|
+
'packageName' => current_package_name,
|
185
|
+
'language' => language
|
186
|
+
},
|
187
|
+
body_object: listing,
|
188
|
+
authorization: auth_client
|
189
|
+
)
|
190
|
+
raise result.error_message.red if result.error?
|
191
|
+
end
|
192
|
+
|
193
|
+
def upload_apk_to_track(path_to_apk, track)
|
194
|
+
ensure_active_edit!
|
195
|
+
|
196
|
+
apk = Google::APIClient::UploadIO.new(File.expand_path(path_to_apk), 'application/vnd.android.package-archive')
|
197
|
+
result_upload = api_client.execute(
|
198
|
+
api_method: android_publisher.edits.apks.upload,
|
199
|
+
parameters: {
|
200
|
+
'editId' => current_edit.data.id,
|
201
|
+
'packageName' => current_package_name,
|
202
|
+
'uploadType' => 'media'
|
203
|
+
},
|
204
|
+
media: apk,
|
205
|
+
authorization: auth_client
|
206
|
+
)
|
207
|
+
|
208
|
+
raise result_upload.error_message.red if result_upload.error?
|
209
|
+
|
210
|
+
track_body = {
|
211
|
+
'track' => track,
|
212
|
+
'userFraction' => 1,
|
213
|
+
'versionCodes' => [result_upload.data.versionCode]
|
214
|
+
}
|
215
|
+
|
216
|
+
result_update = api_client.execute(
|
217
|
+
api_method: android_publisher.edits.tracks.update,
|
218
|
+
parameters:
|
219
|
+
{
|
220
|
+
'editId' => current_edit.data.id,
|
221
|
+
'packageName' => current_package_name,
|
222
|
+
'track' => track
|
223
|
+
},
|
224
|
+
body_object: track_body,
|
225
|
+
authorization: auth_client)
|
226
|
+
|
227
|
+
raise result_update.error_message.red if result_update.error?
|
228
|
+
end
|
229
|
+
|
230
|
+
#####################################################
|
231
|
+
# @!group Screenshots
|
232
|
+
#####################################################
|
233
|
+
|
234
|
+
def fetch_images(image_type: nil, language: nil)
|
235
|
+
ensure_active_edit!
|
236
|
+
|
237
|
+
result = api_client.execute(
|
238
|
+
api_method: android_publisher.edits.images.list,
|
239
|
+
parameters: {
|
240
|
+
'editId' => current_edit.data.id,
|
241
|
+
'packageName' => current_package_name,
|
242
|
+
'language' => language,
|
243
|
+
'imageType' => image_type
|
244
|
+
},
|
245
|
+
authorization: auth_client
|
246
|
+
)
|
247
|
+
|
248
|
+
raise result.error_message.red if result.error?
|
249
|
+
|
250
|
+
result.data.images.collect(&:url)
|
251
|
+
end
|
252
|
+
|
253
|
+
# @param image_type (e.g. phoneScreenshots, sevenInchScreenshots, ...)
|
254
|
+
def upload_image(image_path: nil, image_type: nil, language: nil)
|
255
|
+
ensure_active_edit!
|
256
|
+
|
257
|
+
image = Google::APIClient::UploadIO.new(image_path, 'image/*')
|
258
|
+
result = api_client.execute(
|
259
|
+
api_method: android_publisher.edits.images.upload,
|
260
|
+
parameters: {
|
261
|
+
'editId' => current_edit.data.id,
|
262
|
+
'packageName' => current_package_name,
|
263
|
+
'language' => language,
|
264
|
+
'imageType' => image_type,
|
265
|
+
'uploadType' => 'media'
|
266
|
+
},
|
267
|
+
media: image,
|
268
|
+
authorization: auth_client
|
269
|
+
)
|
270
|
+
|
271
|
+
raise result.error_message.red if result.error?
|
272
|
+
end
|
273
|
+
|
274
|
+
def clear_screenshots(image_type: nil, language: nil)
|
275
|
+
ensure_active_edit!
|
276
|
+
|
277
|
+
result = @api_client.execute(
|
278
|
+
api_method: @android_publisher.edits.images.deleteall,
|
279
|
+
parameters: {
|
280
|
+
'editId' => current_edit.data.id,
|
281
|
+
'packageName' => current_package_name,
|
282
|
+
'language' => language,
|
283
|
+
'imageType' => image_type
|
284
|
+
},
|
285
|
+
authorization: auth_client
|
286
|
+
)
|
287
|
+
|
288
|
+
raise result.error_message if result.error?
|
289
|
+
end
|
290
|
+
|
291
|
+
private
|
292
|
+
|
293
|
+
def ensure_active_edit!
|
294
|
+
raise "You need to have an active edit, make sure to call `begin_edit`" unless @current_edit
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "commander"
|
2
|
+
require "fastlane_core"
|
3
|
+
require "supply"
|
4
|
+
|
5
|
+
HighLine.track_eof = false
|
6
|
+
|
7
|
+
module Supply
|
8
|
+
class CommandsGenerator
|
9
|
+
include Commander::Methods
|
10
|
+
|
11
|
+
FastlaneCore::CommanderGenerator.new.generate(Supply::Options.available_options)
|
12
|
+
|
13
|
+
def self.start
|
14
|
+
FastlaneCore::UpdateChecker.start_looking_for_update("supply")
|
15
|
+
new.run
|
16
|
+
ensure
|
17
|
+
FastlaneCore::UpdateChecker.show_update_status("supply", Supply::VERSION)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
program :version, Supply::VERSION
|
22
|
+
program :description, 'CLI for \'supply\' - TODO'
|
23
|
+
program :help, 'Author', 'Felix Krause <supply@krausefx.com>, Reinhard Hafenscher <TODO>'
|
24
|
+
program :help, 'Website', 'https://fastlane.tools'
|
25
|
+
program :help, 'GitHub', 'https://github.com/fastlane/supply'
|
26
|
+
program :help_formatter, :compact
|
27
|
+
|
28
|
+
always_trace!
|
29
|
+
|
30
|
+
command :run do |c|
|
31
|
+
c.syntax = 'supply'
|
32
|
+
c.description = 'Run a deploy process'
|
33
|
+
c.action do |args, options|
|
34
|
+
Supply.config = FastlaneCore::Configuration.create(Supply::Options.available_options, options.__hash__)
|
35
|
+
load_supplyfile
|
36
|
+
|
37
|
+
Supply::Uploader.new.perform_upload
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
command :init do |c|
|
42
|
+
c.syntax = 'supply init'
|
43
|
+
c.description = 'Sets up supply for you'
|
44
|
+
c.action do |args, options|
|
45
|
+
require 'supply/setup'
|
46
|
+
Supply.config = FastlaneCore::Configuration.create(Supply::Options.available_options, options.__hash__)
|
47
|
+
load_supplyfile
|
48
|
+
|
49
|
+
Supply::Setup.new.perform_download
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
default_command :run
|
54
|
+
|
55
|
+
run!
|
56
|
+
end
|
57
|
+
|
58
|
+
def load_supplyfile
|
59
|
+
Supply.config.load_configuration_file('Supplyfile')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Supply
|
2
|
+
class Listing
|
3
|
+
attr_reader :language
|
4
|
+
|
5
|
+
attr_accessor :title
|
6
|
+
attr_accessor :short_description
|
7
|
+
attr_accessor :full_description
|
8
|
+
attr_accessor :video
|
9
|
+
|
10
|
+
# Initializes the listing to use the given api client, language, and fills it with the current listing if available
|
11
|
+
def initialize(google_api, language, source_listing = nil)
|
12
|
+
@google_api = google_api
|
13
|
+
@language = language
|
14
|
+
|
15
|
+
if source_listing # this might be nil, e.g. when creating a new locale
|
16
|
+
self.title = source_listing.title
|
17
|
+
self.short_description = source_listing.short_description
|
18
|
+
self.full_description = source_listing.full_description
|
19
|
+
self.video = source_listing.video
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Updates the listing in the current edit
|
24
|
+
def save
|
25
|
+
@google_api.update_listing_for_language(language: language,
|
26
|
+
title: title,
|
27
|
+
short_description: short_description,
|
28
|
+
full_description: full_description,
|
29
|
+
video: video)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'fastlane_core'
|
2
|
+
require 'credentials_manager'
|
3
|
+
|
4
|
+
module Supply
|
5
|
+
class Options
|
6
|
+
def self.available_options
|
7
|
+
@options ||= [
|
8
|
+
FastlaneCore::ConfigItem.new(key: :package_name,
|
9
|
+
env_name: "SUPPLY_PACKAGE_NAME",
|
10
|
+
short_option: "-p",
|
11
|
+
description: "The package name of the Application to modify",
|
12
|
+
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:package_name)),
|
13
|
+
FastlaneCore::ConfigItem.new(key: :track,
|
14
|
+
short_option: "-a",
|
15
|
+
env_name: "SUPPLY_TRACK",
|
16
|
+
description: "The Track to upload the Application to: production, beta, alpha",
|
17
|
+
default_value: 'production',
|
18
|
+
verify_block: proc do |value|
|
19
|
+
available = %w(production beta alpha)
|
20
|
+
raise "Invalid value '#{value}', must be #{available.join(', ')}".red unless available.include? value
|
21
|
+
end),
|
22
|
+
FastlaneCore::ConfigItem.new(key: :metadata_path,
|
23
|
+
env_name: "SUPPLY_METADATA_PATH",
|
24
|
+
short_option: "-m",
|
25
|
+
optional: true,
|
26
|
+
description: "Path to the directory containing the metadata files",
|
27
|
+
default_value: (Dir["./fastlane/metadata/android"] + Dir["./metadata"]).first,
|
28
|
+
verify_block: proc do |value|
|
29
|
+
raise "Could not find folder".red unless File.directory? value
|
30
|
+
end),
|
31
|
+
FastlaneCore::ConfigItem.new(key: :key,
|
32
|
+
env_name: "SUPPLY_KEY",
|
33
|
+
description: "The p12 File used to authenticate with Google",
|
34
|
+
default_value: Dir["*.p12"].first || CredentialsManager::AppfileConfig.try_fetch_value(:keyfile),
|
35
|
+
verify_block: proc do |value|
|
36
|
+
raise "Could not find p12 file at path '#{File.expand_path(value)}'".red unless File.exist?(File.expand_path(value))
|
37
|
+
end),
|
38
|
+
FastlaneCore::ConfigItem.new(key: :issuer,
|
39
|
+
short_option: "-i",
|
40
|
+
env_name: "SUPPLY_ISSUER",
|
41
|
+
description: "The issuer of the p12 file (email address of the service account)",
|
42
|
+
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:issuer)),
|
43
|
+
FastlaneCore::ConfigItem.new(key: :apk,
|
44
|
+
env_name: "SUPPLY_APK",
|
45
|
+
description: "Path to the APK file to upload",
|
46
|
+
short_option: "-b",
|
47
|
+
default_value: Dir["*.apk"].last || Dir[File.join("app", "build", "outputs", "apk", "app-Release.apk")].last,
|
48
|
+
optional: true,
|
49
|
+
verify_block: proc do |value|
|
50
|
+
raise "Could not find apk file at path '#{value}'".red unless File.exist?(value)
|
51
|
+
raise "apk file is not an apk".red unless value.end_with?(value)
|
52
|
+
end)
|
53
|
+
]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/supply/setup.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Supply
|
2
|
+
class Setup
|
3
|
+
def perform_download
|
4
|
+
if File.exist?(metadata_path)
|
5
|
+
Helper.log.info "Metadata already exists at path '#{metadata_path}'".yellow
|
6
|
+
return
|
7
|
+
end
|
8
|
+
|
9
|
+
client.begin_edit(package_name: Supply.config[:package_name])
|
10
|
+
|
11
|
+
client.listings.each do |listing|
|
12
|
+
store_metadata(listing)
|
13
|
+
create_screenshots_folder(listing)
|
14
|
+
download_images(listing)
|
15
|
+
end
|
16
|
+
|
17
|
+
client.abort_current_edit
|
18
|
+
|
19
|
+
Helper.log.info "Successfully stored metadata in '#{metadata_path}'".green
|
20
|
+
end
|
21
|
+
|
22
|
+
def store_metadata(listing)
|
23
|
+
containing = File.join(metadata_path, listing.language)
|
24
|
+
FileUtils.mkdir_p(containing)
|
25
|
+
|
26
|
+
Supply::AVAILABLE_METADATA_FIELDS.each do |key|
|
27
|
+
path = File.join(containing, "#{key}.txt")
|
28
|
+
Helper.log.info "Writing to #{path}..."
|
29
|
+
File.write(path, listing.send(key))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def download_images(listing)
|
34
|
+
# We cannot download existing screenshots as they are compressed
|
35
|
+
# But we can at least download the images
|
36
|
+
require 'net/http'
|
37
|
+
|
38
|
+
IMAGES_TYPES.each do |image_type|
|
39
|
+
next if ['featureGraphic'].include?(image_type) # we don't get all files in full resolution :(
|
40
|
+
|
41
|
+
begin
|
42
|
+
Helper.log.info "Downloading #{image_type} for #{listing.language}..."
|
43
|
+
|
44
|
+
url = client.fetch_images(image_type: image_type, language: listing.language).last
|
45
|
+
next unless url
|
46
|
+
|
47
|
+
path = File.join(metadata_path, listing.language, IMAGES_FOLDER_NAME, "#{image_type}.png")
|
48
|
+
File.write(path, Net::HTTP.get(URI.parse(url)))
|
49
|
+
rescue => ex
|
50
|
+
Helper.log.error ex.to_s
|
51
|
+
Helper.log.error "Error downloading '#{image_type}' for #{listing.language}...".red
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_screenshots_folder(listing)
|
57
|
+
containing = File.join(metadata_path, listing.language)
|
58
|
+
|
59
|
+
FileUtils.mkdir_p(File.join(containing, IMAGES_FOLDER_NAME))
|
60
|
+
Supply::SCREENSHOT_TYPES.each do |screenshot_type|
|
61
|
+
FileUtils.mkdir_p(File.join(containing, IMAGES_FOLDER_NAME, screenshot_type))
|
62
|
+
end
|
63
|
+
|
64
|
+
Helper.log.info "Due to the limit of the Google Play API `supply` can't download your existing screenshots..."
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def metadata_path
|
70
|
+
@metadata_path ||= Supply.config[:metadata_path]
|
71
|
+
@metadata_path ||= "fastlane/metadata/android" if Helper.fastlane_enabled?
|
72
|
+
@metadata_path ||= "metadata" unless Helper.fastlane_enabled?
|
73
|
+
|
74
|
+
return @metadata_path
|
75
|
+
end
|
76
|
+
|
77
|
+
def client
|
78
|
+
@client ||= Client.new(path_to_key: Supply.config[:key],
|
79
|
+
issuer: Supply.config[:issuer])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Supply
|
2
|
+
class Uploader
|
3
|
+
def perform_upload
|
4
|
+
FastlaneCore::PrintTable.print_values(config: Supply.config, hide_keys: [:issuer], title: "Summary")
|
5
|
+
|
6
|
+
client.begin_edit(package_name: Supply.config[:package_name])
|
7
|
+
|
8
|
+
raise "No local metadata found, make sure to run `supply init` to setup supply".red unless metadata_path || Supply.config[:apk]
|
9
|
+
|
10
|
+
if metadata_path
|
11
|
+
Dir.foreach(metadata_path) do |language|
|
12
|
+
next if language.start_with?('.') # e.g. . or .. or hidden folders
|
13
|
+
|
14
|
+
listing = client.listing_for_language(language)
|
15
|
+
|
16
|
+
upload_metadata(language, listing)
|
17
|
+
upload_images(language)
|
18
|
+
upload_screenshots(language)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
upload_binary
|
23
|
+
|
24
|
+
Helper.log.info "Uploading all changes to Google Play..."
|
25
|
+
client.commit_current_edit!
|
26
|
+
Helper.log.info "Successfully finished the upload to Google Play".green
|
27
|
+
end
|
28
|
+
|
29
|
+
def upload_metadata(language, listing)
|
30
|
+
Helper.log.info "Loading metadata for language '#{language}'..."
|
31
|
+
|
32
|
+
Supply::AVAILABLE_METADATA_FIELDS.each do |key|
|
33
|
+
path = File.join(metadata_path, language, "#{key}.txt")
|
34
|
+
listing.send("#{key}=".to_sym, File.read(path)) if File.exist?(path)
|
35
|
+
end
|
36
|
+
listing.save
|
37
|
+
end
|
38
|
+
|
39
|
+
def upload_images(language)
|
40
|
+
Supply::IMAGES_TYPES.each do |image_type|
|
41
|
+
search = File.join(metadata_path, language, Supply::IMAGES_FOLDER_NAME, image_type) + ".#{IMAGE_FILE_EXTENSIONS}"
|
42
|
+
path = Dir.glob(search, File::FNM_CASEFOLD).last
|
43
|
+
next unless path
|
44
|
+
|
45
|
+
Helper.log.info "Uploading image file #{path}..."
|
46
|
+
client.upload_image(image_path: File.expand_path(path),
|
47
|
+
image_type: image_type,
|
48
|
+
language: language)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def upload_screenshots(language)
|
53
|
+
Supply::SCREENSHOT_TYPES.each do |screenshot_type|
|
54
|
+
search = File.join(metadata_path, language, Supply::IMAGES_FOLDER_NAME, screenshot_type, "*.#{IMAGE_FILE_EXTENSIONS}")
|
55
|
+
paths = Dir.glob(search, File::FNM_CASEFOLD)
|
56
|
+
next unless paths.count > 0
|
57
|
+
|
58
|
+
client.clear_screenshots(image_type: screenshot_type, language: language)
|
59
|
+
|
60
|
+
paths.each do |path|
|
61
|
+
Helper.log.info "Uploading screenshot #{path}..."
|
62
|
+
client.upload_image(image_path: File.expand_path(path),
|
63
|
+
image_type: screenshot_type,
|
64
|
+
language: language)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def upload_binary
|
70
|
+
if Supply.config[:apk]
|
71
|
+
Helper.log.info "Preparing apk at path '#{Supply.config[:apk]}' for upload..."
|
72
|
+
client.upload_apk_to_track(Supply.config[:apk], Supply.config[:track])
|
73
|
+
else
|
74
|
+
Helper.log.info "No apk file found, you can pass the path to your apk using the `apk` option"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def client
|
81
|
+
@client ||= Client.new(path_to_key: Supply.config[:key],
|
82
|
+
issuer: Supply.config[:issuer])
|
83
|
+
end
|
84
|
+
|
85
|
+
def metadata_path
|
86
|
+
Supply.config[:metadata_path]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: supply
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Felix Krause
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: google-api-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fastlane_core
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.19.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.19.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: credentials_manager
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.1.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.1.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yard
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.8.7.4
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.8.7.4
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: coveralls
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: fastlane
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.34'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.34'
|
167
|
+
description: Command line tool for updating Android apps and their metadata on the
|
168
|
+
Google Play Store
|
169
|
+
email:
|
170
|
+
- supply@krausefx.com
|
171
|
+
executables:
|
172
|
+
- supply
|
173
|
+
extensions: []
|
174
|
+
extra_rdoc_files: []
|
175
|
+
files:
|
176
|
+
- LICENSE
|
177
|
+
- README.md
|
178
|
+
- bin/supply
|
179
|
+
- lib/supply.rb
|
180
|
+
- lib/supply/client.rb
|
181
|
+
- lib/supply/commands_generator.rb
|
182
|
+
- lib/supply/listing.rb
|
183
|
+
- lib/supply/options.rb
|
184
|
+
- lib/supply/setup.rb
|
185
|
+
- lib/supply/uploader.rb
|
186
|
+
- lib/supply/version.rb
|
187
|
+
homepage: https://fastlane.tools
|
188
|
+
licenses:
|
189
|
+
- MIT
|
190
|
+
metadata: {}
|
191
|
+
post_install_message:
|
192
|
+
rdoc_options: []
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 2.0.0
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.4.8
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Command line tool for updating Android apps and their metadata on the Google
|
211
|
+
Play Store
|
212
|
+
test_files: []
|
213
|
+
has_rdoc:
|