fastlane-plugin-pcloudy 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +69 -3
- data/lib/fastlane/plugin/pcloudy/actions/pcloudy_list_devices_action.rb +3 -22
- data/lib/fastlane/plugin/pcloudy/actions/pcloudy_resign_app_action.rb +86 -0
- data/lib/fastlane/plugin/pcloudy/actions/pcloudy_upload_app_action.rb +144 -0
- data/lib/fastlane/plugin/pcloudy/helper/pcloudy_client.rb +79 -4
- data/lib/fastlane/plugin/pcloudy/helper/pcloudy_options.rb +53 -0
- data/lib/fastlane/plugin/pcloudy/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5beb8f759d9c8483fbb1d9185edc83e0d09c6d22e900c4e848ff635adff4f1e7
|
|
4
|
+
data.tar.gz: 4c21078f470ecbf008c417c60a6afac259b84750850b9c6d489048715d8311cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baa15536e27dc025e5e56845f256fc35d4c4d4f37b2781a7e8052f946b9649d817caccd2adfaebe233e718d172f812433c103b81419ba51251a5639c9f06e696
|
|
7
|
+
data.tar.gz: 07a4aceb6833f999a52be92af8cd1aaf1b02c6f94ec2408768c70f18576c2b10014ad761c1caac714b55e4e66517f99e94526bf576ed8744fd669a1ae018f472
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- New `pcloudy_upload_app`: uploads an `.ipa` or `.apk` to pCloudy My Apps. Picks up the output of
|
|
6
|
+
`build_app` / `gradle` automatically, waits until pCloudy has registered the file, and returns the
|
|
7
|
+
name pCloudy stored it under (pCloudy adds a timestamp when the name already exists).
|
|
8
|
+
`resign: true` re-signs an `.ipa` in the same step.
|
|
9
|
+
- New `pcloudy_resign_app`: re-signs an uploaded `.ipa` with pCloudy's certificate and waits for it to finish.
|
|
10
|
+
- New shared values: `PCLOUDY_UPLOADED_FILE`, `PCLOUDY_RESIGNED_FILE`, `PCLOUDY_APP_FILE`.
|
|
11
|
+
|
|
3
12
|
## 0.1.0
|
|
4
13
|
|
|
5
14
|
First release.
|
data/README.md
CHANGED
|
@@ -5,8 +5,8 @@ A [fastlane](https://fastlane.tools) plugin for [pCloudy](https://www.pcloudy.co
|
|
|
5
5
|
| Action | Status |
|
|
6
6
|
|---|---|
|
|
7
7
|
| `pcloudy_list_devices`: list available devices | ✅ Available |
|
|
8
|
-
| `pcloudy_upload_app`: upload an `.ipa` / `.apk` |
|
|
9
|
-
| `pcloudy_resign_app`:
|
|
8
|
+
| `pcloudy_upload_app`: upload an `.ipa` / `.apk` | ✅ Available |
|
|
9
|
+
| `pcloudy_resign_app`: re-sign an `.ipa` for pCloudy devices | ✅ Available |
|
|
10
10
|
| `pcloudy_book_device` / `pcloudy_release_device` | Planned |
|
|
11
11
|
| `pcloudy_install_app`: install and launch on a booked device | Planned |
|
|
12
12
|
|
|
@@ -28,6 +28,70 @@ PCLOUDY_API_KEY=your-api-key # pCloudy > Settings > API Key
|
|
|
28
28
|
# PCLOUDY_CLOUD_URL=https://device.pcloudy.com # optional, for private clouds
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# iOS: build, upload and re-sign with pCloudy's certificate
|
|
35
|
+
lane :pcloudy_ios do
|
|
36
|
+
build_app(scheme: "MyApp", export_method: "development")
|
|
37
|
+
pcloudy_upload_app(resign: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Android: build and upload
|
|
41
|
+
lane :pcloudy_android do
|
|
42
|
+
gradle(task: "assembleDebug")
|
|
43
|
+
pcloudy_upload_app
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The app then appears in **pCloudy → My Apps**, ready to install on real devices.
|
|
48
|
+
|
|
49
|
+
## `pcloudy_upload_app`
|
|
50
|
+
|
|
51
|
+
Uploads an `.ipa` or `.apk` to pCloudy My Apps. If you don't pass `file:`, it uses the `.ipa` from `build_app` or the `.apk` from `gradle`.
|
|
52
|
+
|
|
53
|
+
When a file with the same name already exists, pCloudy saves the upload under a new, timestamped name (e.g. `MyApp-1794551836.ipa`). The action returns that name, so always use the return value or `PCLOUDY_UPLOADED_FILE`, never the local file name.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
pcloudy_upload_app # output of build_app / gradle
|
|
57
|
+
pcloudy_upload_app(resign: true) # iOS: upload + re-sign in one step
|
|
58
|
+
name = pcloudy_upload_app(file: "./build/MyApp.apk")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Option | Env var | Default | Description |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `file` | `PCLOUDY_APP_PATH` | output of `build_app` / `gradle` | Path to the `.ipa` or `.apk` |
|
|
64
|
+
| `resign` | `PCLOUDY_RESIGN` | `false` | Re-sign the `.ipa` after uploading (iOS only) |
|
|
65
|
+
| `wait_for_processing` | `PCLOUDY_WAIT_FOR_PROCESSING` | `true` | Wait until the file appears in My Apps |
|
|
66
|
+
| `upload_timeout` | `PCLOUDY_UPLOAD_TIMEOUT` | `600` | Seconds allowed for the upload |
|
|
67
|
+
| `processing_timeout` | `PCLOUDY_PROCESSING_TIMEOUT` | `120` | Seconds to wait for the file to appear in My Apps |
|
|
68
|
+
| `resign_timeout` | `PCLOUDY_RESIGN_TIMEOUT` | `600` | Seconds to wait for re-signing (with `resign: true`) |
|
|
69
|
+
|
|
70
|
+
Plus `username`, `api_key` and `cloud_url`, as for every action.
|
|
71
|
+
|
|
72
|
+
**Returns:** the file name in My Apps (the re-signed copy when `resign: true`).
|
|
73
|
+
**Sets:** `PCLOUDY_UPLOADED_FILE`, `PCLOUDY_APP_FILE`, and with `resign: true`, `PCLOUDY_RESIGNED_FILE`.
|
|
74
|
+
|
|
75
|
+
## `pcloudy_resign_app`
|
|
76
|
+
|
|
77
|
+
Re-signs an `.ipa` that's already in pCloudy My Apps with pCloudy's certificate, so it installs on pCloudy iPhones and iPads. If you don't pass `file:`, it uses the last `pcloudy_upload_app` upload.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
pcloudy_upload_app
|
|
81
|
+
pcloudy_resign_app
|
|
82
|
+
|
|
83
|
+
pcloudy_resign_app(file: "MyApp-1794551836.ipa") # a file already in My Apps
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Option | Env var | Default | Description |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `file` | `PCLOUDY_RESIGN_FILE` | last upload | File name in pCloudy My Apps |
|
|
89
|
+
| `timeout` | `PCLOUDY_RESIGN_TIMEOUT` | `600` | Seconds to wait for re-signing to finish |
|
|
90
|
+
| `poll_interval` | — | `5` | Seconds between progress checks |
|
|
91
|
+
|
|
92
|
+
**Returns:** the re-signed file name, e.g. `MyApp-1794551836_Resigned1794551854668.ipa`.
|
|
93
|
+
**Sets:** `PCLOUDY_RESIGNED_FILE` and `PCLOUDY_APP_FILE`.
|
|
94
|
+
|
|
31
95
|
## `pcloudy_list_devices`
|
|
32
96
|
|
|
33
97
|
```ruby
|
|
@@ -59,5 +123,7 @@ The action returns an array of device hashes (`id`, `full_name`, `display_name`,
|
|
|
59
123
|
```bash
|
|
60
124
|
bundle install
|
|
61
125
|
bundle exec rspec # unit tests (HTTP is stubbed)
|
|
62
|
-
bundle exec fastlane list_devices platform:ios # live
|
|
126
|
+
bundle exec fastlane list_devices platform:ios # live runs, need fastlane/.env
|
|
127
|
+
bundle exec fastlane upload file:/path/App.ipa resign:true
|
|
128
|
+
bundle exec fastlane resign file:App-1794551836.ipa
|
|
63
129
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require "fastlane/action"
|
|
2
2
|
require "terminal-table"
|
|
3
|
-
require_relative "../helper/
|
|
3
|
+
require_relative "../helper/pcloudy_options"
|
|
4
4
|
|
|
5
5
|
module Fastlane
|
|
6
6
|
module Actions
|
|
@@ -12,9 +12,7 @@ module Fastlane
|
|
|
12
12
|
PLATFORMS = %w(ios android all).freeze
|
|
13
13
|
|
|
14
14
|
def self.run(params)
|
|
15
|
-
client = Helper::
|
|
16
|
-
api_key: params[:api_key],
|
|
17
|
-
cloud_url: params[:cloud_url])
|
|
15
|
+
client = Helper::PcloudyOptions.client(params)
|
|
18
16
|
|
|
19
17
|
platforms = params[:platform] == "all" ? %w(ios android) : [params[:platform]]
|
|
20
18
|
devices = platforms.flat_map do |platform|
|
|
@@ -104,24 +102,7 @@ module Fastlane
|
|
|
104
102
|
end
|
|
105
103
|
|
|
106
104
|
def self.available_options
|
|
107
|
-
[
|
|
108
|
-
FastlaneCore::ConfigItem.new(key: :username,
|
|
109
|
-
env_name: "PCLOUDY_USERNAME",
|
|
110
|
-
description: "pCloudy account email",
|
|
111
|
-
optional: false,
|
|
112
|
-
type: String),
|
|
113
|
-
FastlaneCore::ConfigItem.new(key: :api_key,
|
|
114
|
-
env_name: "PCLOUDY_API_KEY",
|
|
115
|
-
description: "pCloudy API key (Settings > API Key)",
|
|
116
|
-
optional: false,
|
|
117
|
-
sensitive: true,
|
|
118
|
-
code_gen_sensitive: true,
|
|
119
|
-
type: String),
|
|
120
|
-
FastlaneCore::ConfigItem.new(key: :cloud_url,
|
|
121
|
-
env_name: "PCLOUDY_CLOUD_URL",
|
|
122
|
-
description: "pCloudy cloud URL (change for private clouds)",
|
|
123
|
-
default_value: Helper::PcloudyClient::DEFAULT_CLOUD_URL,
|
|
124
|
-
type: String),
|
|
105
|
+
Helper::PcloudyOptions.auth + [
|
|
125
106
|
FastlaneCore::ConfigItem.new(key: :platform,
|
|
126
107
|
env_name: "PCLOUDY_PLATFORM",
|
|
127
108
|
description: "Device platform: ios, android or all",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "fastlane/action"
|
|
2
|
+
require_relative "../helper/pcloudy_options"
|
|
3
|
+
require_relative "pcloudy_upload_app_action"
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module Actions
|
|
7
|
+
class PcloudyResignAppAction < Action
|
|
8
|
+
def self.run(params)
|
|
9
|
+
filename = params[:file] || Actions.lane_context[SharedValues::PCLOUDY_UPLOADED_FILE]
|
|
10
|
+
UI.user_error!("No app to re-sign. Run pcloudy_upload_app first or pass file: (a file name in pCloudy My Apps)") if filename.to_s.empty?
|
|
11
|
+
|
|
12
|
+
client = Helper::PcloudyOptions.client(params)
|
|
13
|
+
signed = Helper::PcloudyOptions.resign(client, filename, timeout: params[:timeout], interval: params[:poll_interval])
|
|
14
|
+
|
|
15
|
+
Actions.lane_context[SharedValues::PCLOUDY_RESIGNED_FILE] = signed
|
|
16
|
+
Actions.lane_context[SharedValues::PCLOUDY_APP_FILE] = signed
|
|
17
|
+
signed
|
|
18
|
+
rescue Helper::PcloudyClient::Error => e
|
|
19
|
+
UI.user_error!(e.message)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#####################################################
|
|
23
|
+
# @!group Documentation
|
|
24
|
+
#####################################################
|
|
25
|
+
|
|
26
|
+
def self.description
|
|
27
|
+
"Re-signs an uploaded .ipa with pCloudy's certificate so it installs on pCloudy iOS devices"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.details
|
|
31
|
+
"Starts re-signing on pCloudy, waits for it to reach 100% and returns the re-signed file name in My Apps. " \
|
|
32
|
+
"Uses the file from the last pcloudy_upload_app unless you pass file:."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.authors
|
|
36
|
+
["bikee1235"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.return_value
|
|
40
|
+
"The re-signed file name in pCloudy My Apps, e.g. MyApp-1794551836_Resigned1794551854668.ipa"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.output
|
|
44
|
+
[
|
|
45
|
+
["PCLOUDY_RESIGNED_FILE", "Re-signed file name in pCloudy My Apps"],
|
|
46
|
+
["PCLOUDY_APP_FILE", "Set to the re-signed file, for installing on devices"]
|
|
47
|
+
]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.available_options
|
|
51
|
+
Helper::PcloudyOptions.auth + [
|
|
52
|
+
FastlaneCore::ConfigItem.new(key: :file,
|
|
53
|
+
env_name: "PCLOUDY_RESIGN_FILE",
|
|
54
|
+
description: "File name in pCloudy My Apps. Defaults to the last pcloudy_upload_app upload",
|
|
55
|
+
optional: true,
|
|
56
|
+
type: String),
|
|
57
|
+
FastlaneCore::ConfigItem.new(key: :timeout,
|
|
58
|
+
env_name: "PCLOUDY_RESIGN_TIMEOUT",
|
|
59
|
+
description: "Seconds to wait for re-signing to finish",
|
|
60
|
+
default_value: 600,
|
|
61
|
+
type: Integer),
|
|
62
|
+
FastlaneCore::ConfigItem.new(key: :poll_interval,
|
|
63
|
+
description: "Seconds between progress checks",
|
|
64
|
+
default_value: 5,
|
|
65
|
+
type: Integer)
|
|
66
|
+
]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.example_code
|
|
70
|
+
[
|
|
71
|
+
'pcloudy_upload_app
|
|
72
|
+
pcloudy_resign_app',
|
|
73
|
+
'pcloudy_resign_app(file: "MyApp-1794551836.ipa")'
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.category
|
|
78
|
+
:testing
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.is_supported?(platform)
|
|
82
|
+
platform == :ios
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
require "fastlane/action"
|
|
2
|
+
require_relative "../helper/pcloudy_options"
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
module SharedValues
|
|
7
|
+
PCLOUDY_UPLOADED_FILE = :PCLOUDY_UPLOADED_FILE
|
|
8
|
+
PCLOUDY_RESIGNED_FILE = :PCLOUDY_RESIGNED_FILE
|
|
9
|
+
PCLOUDY_APP_FILE = :PCLOUDY_APP_FILE
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class PcloudyUploadAppAction < Action
|
|
13
|
+
EXTENSIONS = %w(.ipa .apk).freeze
|
|
14
|
+
|
|
15
|
+
def self.run(params)
|
|
16
|
+
path = params[:file] || default_file
|
|
17
|
+
UI.user_error!("No app to upload. Pass file: or run build_app (iOS) / gradle (Android) first") if path.to_s.empty?
|
|
18
|
+
|
|
19
|
+
path = File.expand_path(path)
|
|
20
|
+
UI.user_error!("File not found: #{path}") unless File.file?(path)
|
|
21
|
+
ext = File.extname(path).downcase
|
|
22
|
+
UI.user_error!("Unsupported file type '#{ext}'. pCloudy apps must be #{EXTENSIONS.join(' or ')}") unless EXTENSIONS.include?(ext)
|
|
23
|
+
UI.user_error!("resign: true only works for iOS apps (.ipa); #{File.basename(path)} is #{ext}") if params[:resign] && ext != ".ipa"
|
|
24
|
+
|
|
25
|
+
client = Helper::PcloudyOptions.client(params)
|
|
26
|
+
|
|
27
|
+
UI.message("Uploading #{File.basename(path)} (#{format_size(File.size(path))}) to pCloudy...")
|
|
28
|
+
name = client.upload(path, timeout: params[:upload_timeout])
|
|
29
|
+
UI.success("Uploaded as #{name}")
|
|
30
|
+
|
|
31
|
+
if params[:wait_for_processing]
|
|
32
|
+
UI.message("Waiting for pCloudy to register #{name}...")
|
|
33
|
+
client.wait_for_file(name, timeout: params[:processing_timeout])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Actions.lane_context[SharedValues::PCLOUDY_UPLOADED_FILE] = name
|
|
37
|
+
Actions.lane_context[SharedValues::PCLOUDY_APP_FILE] = name
|
|
38
|
+
|
|
39
|
+
if params[:resign]
|
|
40
|
+
name = Helper::PcloudyOptions.resign(client, name, timeout: params[:resign_timeout], interval: 5)
|
|
41
|
+
Actions.lane_context[SharedValues::PCLOUDY_RESIGNED_FILE] = name
|
|
42
|
+
Actions.lane_context[SharedValues::PCLOUDY_APP_FILE] = name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
name
|
|
46
|
+
rescue Helper::PcloudyClient::Error => e
|
|
47
|
+
UI.user_error!(e.message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The .ipa from build_app/gym, or else the .apk from gradle.
|
|
51
|
+
def self.default_file
|
|
52
|
+
context = Actions.lane_context
|
|
53
|
+
[context[:IPA_OUTPUT_PATH], context[:GRADLE_APK_OUTPUT_PATH]].find { |value| !value.to_s.empty? }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.format_size(bytes)
|
|
57
|
+
bytes >= 1024 * 1024 ? format("%.1f MB", bytes / 1024.0 / 1024) : format("%.0f KB", bytes / 1024.0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#####################################################
|
|
61
|
+
# @!group Documentation
|
|
62
|
+
#####################################################
|
|
63
|
+
|
|
64
|
+
def self.description
|
|
65
|
+
"Uploads an .ipa or .apk to pCloudy (My Apps), optionally re-signing the .ipa"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.details
|
|
69
|
+
"Uploads the app built by build_app or gradle (or any file you pass) to pCloudy, waits until pCloudy has " \
|
|
70
|
+
"registered it, and returns the name pCloudy stored it under. pCloudy adds a timestamp to the name when " \
|
|
71
|
+
"a file with the same name already exists, so always use the returned name or PCLOUDY_UPLOADED_FILE."
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.authors
|
|
75
|
+
["bikee1235"]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.return_value
|
|
79
|
+
"The file name in pCloudy My Apps (the re-signed copy when resign: true)"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.output
|
|
83
|
+
[
|
|
84
|
+
["PCLOUDY_UPLOADED_FILE", "File name pCloudy stored the upload under"],
|
|
85
|
+
["PCLOUDY_RESIGNED_FILE", "Re-signed file name (when resign: true)"],
|
|
86
|
+
["PCLOUDY_APP_FILE", "The file to install on devices: re-signed copy if any, otherwise the upload"]
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.available_options
|
|
91
|
+
Helper::PcloudyOptions.auth + [
|
|
92
|
+
FastlaneCore::ConfigItem.new(key: :file,
|
|
93
|
+
env_name: "PCLOUDY_APP_PATH",
|
|
94
|
+
description: "Path to the .ipa or .apk. Defaults to the output of build_app or gradle",
|
|
95
|
+
optional: true,
|
|
96
|
+
type: String),
|
|
97
|
+
FastlaneCore::ConfigItem.new(key: :resign,
|
|
98
|
+
env_name: "PCLOUDY_RESIGN",
|
|
99
|
+
description: "Re-sign the uploaded .ipa with pCloudy's certificate (iOS only)",
|
|
100
|
+
default_value: false,
|
|
101
|
+
type: Boolean),
|
|
102
|
+
FastlaneCore::ConfigItem.new(key: :wait_for_processing,
|
|
103
|
+
env_name: "PCLOUDY_WAIT_FOR_PROCESSING",
|
|
104
|
+
description: "Wait until the upload appears in pCloudy My Apps",
|
|
105
|
+
default_value: true,
|
|
106
|
+
type: Boolean),
|
|
107
|
+
FastlaneCore::ConfigItem.new(key: :upload_timeout,
|
|
108
|
+
env_name: "PCLOUDY_UPLOAD_TIMEOUT",
|
|
109
|
+
description: "Seconds to allow for the upload request",
|
|
110
|
+
default_value: 600,
|
|
111
|
+
type: Integer),
|
|
112
|
+
FastlaneCore::ConfigItem.new(key: :processing_timeout,
|
|
113
|
+
env_name: "PCLOUDY_PROCESSING_TIMEOUT",
|
|
114
|
+
description: "Seconds to wait for the upload to appear in My Apps",
|
|
115
|
+
default_value: 120,
|
|
116
|
+
type: Integer),
|
|
117
|
+
FastlaneCore::ConfigItem.new(key: :resign_timeout,
|
|
118
|
+
env_name: "PCLOUDY_RESIGN_TIMEOUT",
|
|
119
|
+
description: "Seconds to wait for re-signing to finish (when resign: true)",
|
|
120
|
+
default_value: 600,
|
|
121
|
+
type: Integer)
|
|
122
|
+
]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.example_code
|
|
126
|
+
[
|
|
127
|
+
'build_app(scheme: "MyApp", export_method: "development")
|
|
128
|
+
pcloudy_upload_app(resign: true)',
|
|
129
|
+
'gradle(task: "assembleDebug")
|
|
130
|
+
pcloudy_upload_app',
|
|
131
|
+
'name = pcloudy_upload_app(file: "./build/MyApp.ipa")'
|
|
132
|
+
]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.category
|
|
136
|
+
:testing
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.is_supported?(platform)
|
|
140
|
+
[:ios, :android].include?(platform)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -39,19 +39,93 @@ module Fastlane
|
|
|
39
39
|
result["models"] || []
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# POST /api/upload_file (multipart). Returns the file name pCloudy stored the app under.
|
|
43
|
+
# pCloudy renames the upload (adds a timestamp) when a file with the same name already exists,
|
|
44
|
+
# so callers must use this return value, never the local file name.
|
|
45
|
+
def upload(path, filter: "all", timeout: 600)
|
|
46
|
+
request = Net::HTTP::Post.new(endpoint("/api/upload_file"))
|
|
47
|
+
result = File.open(path, "rb") do |io|
|
|
48
|
+
request.set_form([["file", io, { filename: File.basename(path) }],
|
|
49
|
+
["source_type", "raw"],
|
|
50
|
+
["token", token],
|
|
51
|
+
["filter", filter]],
|
|
52
|
+
"multipart/form-data")
|
|
53
|
+
perform(request, timeout: timeout)
|
|
54
|
+
end
|
|
55
|
+
name = result["file"].to_s
|
|
56
|
+
raise(Error, "pCloudy upload succeeded but returned no file name") if name.empty?
|
|
57
|
+
|
|
58
|
+
name
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# POST /api/drive. Returns the files in "My Apps" as hashes ({"file" => "...", "size_KB" => ..., ...}).
|
|
62
|
+
def files(filter: "all")
|
|
63
|
+
post_json("/api/drive", token: token, limit: 0, filter: filter)["files"] || []
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Waits until an uploaded file shows up in "My Apps". Raises Error after timeout seconds.
|
|
67
|
+
def wait_for_file(name, timeout: 120, interval: 3)
|
|
68
|
+
deadline = monotonic_now + timeout
|
|
69
|
+
loop do
|
|
70
|
+
return true if files.any? { |f| f["file"] == name }
|
|
71
|
+
raise(Error, "#{name} did not appear in pCloudy My Apps within #{timeout}s") if monotonic_now >= deadline
|
|
72
|
+
|
|
73
|
+
sleep(interval)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Re-signs an uploaded .ipa with pCloudy's certificate: resign/initiate -> resign/progress -> resign/download.
|
|
78
|
+
# Yields the progress percentage while waiting. Returns the re-signed file name in "My Apps".
|
|
79
|
+
def resign(filename, timeout: 600, interval: 5)
|
|
80
|
+
deadline = monotonic_now + timeout
|
|
81
|
+
resign_token = with_retry_until(deadline, interval, /does not exist/i) do
|
|
82
|
+
post_json("/api/resign/initiate", token: token, filename: filename)["resign_token"]
|
|
83
|
+
end
|
|
84
|
+
raise(Error, "pCloudy resign/initiate returned no resign_token") if resign_token.to_s.empty?
|
|
85
|
+
|
|
86
|
+
loop do
|
|
87
|
+
status = post_json("/api/resign/progress", token: token, resign_token: resign_token, filename: filename)["resign_status"].to_i
|
|
88
|
+
yield(status) if block_given?
|
|
89
|
+
break if status >= 100
|
|
90
|
+
raise(Error, "Re-signing #{filename} did not finish within #{timeout}s (last progress #{status}%)") if monotonic_now >= deadline
|
|
91
|
+
|
|
92
|
+
sleep(interval)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
signed = post_json("/api/resign/download", token: token, resign_token: resign_token, filename: filename)["resign_file"].to_s
|
|
96
|
+
raise(Error, "pCloudy resign/download returned no file name") if signed.empty?
|
|
97
|
+
|
|
98
|
+
signed
|
|
99
|
+
end
|
|
100
|
+
|
|
42
101
|
private
|
|
43
102
|
|
|
103
|
+
# Runs the block, retrying while it raises an Error whose message matches `pattern`, until `deadline`.
|
|
104
|
+
def with_retry_until(deadline, interval, pattern)
|
|
105
|
+
yield
|
|
106
|
+
rescue Error => e
|
|
107
|
+
raise if e.message !~ pattern || monotonic_now >= deadline
|
|
108
|
+
|
|
109
|
+
sleep(interval)
|
|
110
|
+
retry
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def monotonic_now
|
|
114
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
115
|
+
end
|
|
116
|
+
|
|
44
117
|
def post_json(path, payload)
|
|
45
118
|
request = Net::HTTP::Post.new(endpoint(path), "Content-Type" => "application/json")
|
|
46
119
|
request.body = JSON.generate(payload)
|
|
47
120
|
perform(request)
|
|
48
121
|
end
|
|
49
122
|
|
|
50
|
-
def perform(request)
|
|
123
|
+
def perform(request, timeout: @timeout)
|
|
51
124
|
response = Net::HTTP.start(@base.host, @base.port,
|
|
52
125
|
use_ssl: @base.scheme == "https",
|
|
53
126
|
open_timeout: @timeout,
|
|
54
|
-
read_timeout:
|
|
127
|
+
read_timeout: timeout,
|
|
128
|
+
write_timeout: timeout) do |http|
|
|
55
129
|
http.request(request)
|
|
56
130
|
end
|
|
57
131
|
|
|
@@ -61,9 +135,10 @@ module Fastlane
|
|
|
61
135
|
result = JSON.parse(response.body.to_s)["result"]
|
|
62
136
|
raise(Error, "pCloudy #{path} returned an unexpected response") unless result.kind_of?(Hash)
|
|
63
137
|
|
|
64
|
-
|
|
138
|
+
# Some endpoints (e.g. upload_file) omit "code"; treat an explicit error or a non-200 code as failure.
|
|
139
|
+
if result["error"] || (result.key?("code") && result["code"].to_i != 200)
|
|
65
140
|
message = result["error"] || result["msg"] || result["message"] || "unknown error"
|
|
66
|
-
raise(Error, "pCloudy #{path} failed (code #{result['code']}): #{message}")
|
|
141
|
+
raise(Error, "pCloudy #{path} failed (code #{result['code'] || 'n/a'}): #{message}")
|
|
67
142
|
end
|
|
68
143
|
|
|
69
144
|
result
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "fastlane_core/configuration/config_item"
|
|
2
|
+
require_relative "pcloudy_client"
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Helper
|
|
6
|
+
# Options and client setup shared by every pcloudy_* action.
|
|
7
|
+
module PcloudyOptions
|
|
8
|
+
def self.auth
|
|
9
|
+
[
|
|
10
|
+
FastlaneCore::ConfigItem.new(key: :username,
|
|
11
|
+
env_name: "PCLOUDY_USERNAME",
|
|
12
|
+
description: "pCloudy account email",
|
|
13
|
+
optional: false,
|
|
14
|
+
type: String),
|
|
15
|
+
FastlaneCore::ConfigItem.new(key: :api_key,
|
|
16
|
+
env_name: "PCLOUDY_API_KEY",
|
|
17
|
+
description: "pCloudy API key (Settings > API Key)",
|
|
18
|
+
optional: false,
|
|
19
|
+
sensitive: true,
|
|
20
|
+
code_gen_sensitive: true,
|
|
21
|
+
type: String),
|
|
22
|
+
FastlaneCore::ConfigItem.new(key: :cloud_url,
|
|
23
|
+
env_name: "PCLOUDY_CLOUD_URL",
|
|
24
|
+
description: "pCloudy cloud URL (change for private clouds)",
|
|
25
|
+
default_value: PcloudyClient::DEFAULT_CLOUD_URL,
|
|
26
|
+
type: String)
|
|
27
|
+
]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.client(params)
|
|
31
|
+
PcloudyClient.new(username: params[:username],
|
|
32
|
+
api_key: params[:api_key],
|
|
33
|
+
cloud_url: params[:cloud_url])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Re-signs `filename` in pCloudy, logging progress. Shared by pcloudy_resign_app and pcloudy_upload_app(resign: true).
|
|
37
|
+
def self.resign(client, filename, timeout:, interval:)
|
|
38
|
+
unless File.extname(filename).casecmp?(".ipa")
|
|
39
|
+
UI.user_error!("Only iOS apps (.ipa) can be re-signed on pCloudy; got #{filename}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
UI.message("Re-signing #{filename} on pCloudy...")
|
|
43
|
+
last = nil
|
|
44
|
+
signed = client.resign(filename, timeout: timeout, interval: interval) do |progress|
|
|
45
|
+
UI.message("Re-sign progress: #{progress}%") unless progress == last
|
|
46
|
+
last = progress
|
|
47
|
+
end
|
|
48
|
+
UI.success("Re-signed app in pCloudy: #{signed}")
|
|
49
|
+
signed
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-pcloudy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bikee1235
|
|
@@ -91,9 +91,9 @@ dependencies:
|
|
|
91
91
|
- - "~>"
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
93
|
version: '3.0'
|
|
94
|
-
description:
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
description: 'Use pCloudy (device.pcloudy.com or a private cloud) from your fastlane
|
|
95
|
+
lanes: list available real iOS and Android devices, upload the .ipa or .apk you
|
|
96
|
+
just built, and re-sign iOS apps with pCloudy''s certificate.'
|
|
97
97
|
email:
|
|
98
98
|
- bikee.bihari@sstsinc.com
|
|
99
99
|
executables: []
|
|
@@ -105,7 +105,10 @@ files:
|
|
|
105
105
|
- README.md
|
|
106
106
|
- lib/fastlane/plugin/pcloudy.rb
|
|
107
107
|
- lib/fastlane/plugin/pcloudy/actions/pcloudy_list_devices_action.rb
|
|
108
|
+
- lib/fastlane/plugin/pcloudy/actions/pcloudy_resign_app_action.rb
|
|
109
|
+
- lib/fastlane/plugin/pcloudy/actions/pcloudy_upload_app_action.rb
|
|
108
110
|
- lib/fastlane/plugin/pcloudy/helper/pcloudy_client.rb
|
|
111
|
+
- lib/fastlane/plugin/pcloudy/helper/pcloudy_options.rb
|
|
109
112
|
- lib/fastlane/plugin/pcloudy/version.rb
|
|
110
113
|
homepage: https://github.com/Smart-Software-Testing-Solutions-Opkey/fastlane-plugin-pcloudy
|
|
111
114
|
licenses:
|