fastlane-plugin-sync_devices 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/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +128 -0
- data/lib/fastlane/plugin/sync_devices/actions/sync_devices_action.rb +222 -0
- data/lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/command.rb +174 -0
- data/lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/device_patch.rb +116 -0
- data/lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/devices_file.rb +325 -0
- data/lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/devices_patch.rb +50 -0
- data/lib/fastlane/plugin/sync_devices/helper/sync_devices_helper.rb +13 -0
- data/lib/fastlane/plugin/sync_devices/version.rb +8 -0
- data/lib/fastlane/plugin/sync_devices.rb +18 -0
- metadata +56 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a36daeccb8615f994ee412d9f469e416698636b975796152a2b2c8f27d889b43
|
4
|
+
data.tar.gz: 29a12e29ba18a01f7958eb98ac1b2c4e97a22a4c1f256f3521cf4f67bed4f529
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3987bece7d43b523a1df6cf1303b39511f310ae2701c0903f385d2e714a7ff61f734f8c21daa3d884de50c118557f0793db7e71337dc748bdaa245684ae335a
|
7
|
+
data.tar.gz: a6ed24116431af31c5cd6fe141c9fc365de7be8ebcb88e3631eb1d461e24958c6c98aec0730d500aae4dbc85d53173db414634ffb7b58a8ae834d708ea8b2134
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Ryosuke Ito <rito.0305@gmail.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# sync\_devices plugin
|
2
|
+
|
3
|
+
[![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-sync_devices)
|
4
|
+
[![Test](https://github.com/manicmaniac/fastlane-plugin-sync_devices/actions/workflows/test.yml/badge.svg)](https://github.com/manicmaniac/fastlane-plugin-sync_devices/actions/workflows/test.yml)
|
5
|
+
|
6
|
+
## Getting Started
|
7
|
+
|
8
|
+
This project is a [fastlane](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-sync_devices`, add it to your project by running:
|
9
|
+
|
10
|
+
```bash
|
11
|
+
fastlane add_plugin sync_devices
|
12
|
+
```
|
13
|
+
|
14
|
+
## About sync\_devices
|
15
|
+
|
16
|
+
This plugin provides a single action `sync_devices`.
|
17
|
+
|
18
|
+
`sync_devices` synchronizes your devices with Apple Developer Portal.
|
19
|
+
|
20
|
+
This plugin works similarly to fastlane official [register\_devices](https://docs.fastlane.tools/actions/register_devices/) plugin, but `sync_devices` can disable, enable and rename devices on Apple Developer Portal while `register_devices` is only capable to create new devices.
|
21
|
+
|
22
|
+
Since we can only actually _delete_ a device once a year, `sync_devices` does not _delete_ devices but just disables them when they were removed from a devices file. It's safe because you can re-enable devices whenever you want.
|
23
|
+
|
24
|
+
## Basic Usage
|
25
|
+
|
26
|
+
First of all, you need to create your own `devices.tsv` under your project repository. It is a simple tab-separated text file like the following example.
|
27
|
+
|
28
|
+
```
|
29
|
+
Device ID Device Name Device Platform
|
30
|
+
01234567-89ABCDEF01234567 NAME1 ios
|
31
|
+
abcdef0123456789abcdef0123456789abcdef01 NAME2 ios
|
32
|
+
01234567-89AB-CDEF-0123-4567890ABCDE NAME3 mac
|
33
|
+
ABCDEF01-2345-6789-ABCD-EF0123456789 NAME4 mac
|
34
|
+
```
|
35
|
+
|
36
|
+
Then you can run `sync_devices` from command line.
|
37
|
+
|
38
|
+
Run `sync_devices` in dry-run mode, which does not change remote devices, so that you can see what will be done when it actually runs.
|
39
|
+
|
40
|
+
```
|
41
|
+
fastlane run sync_devices devices_file:devices.tsv dry_run:true
|
42
|
+
```
|
43
|
+
|
44
|
+
After carefully checking if the result is the same as expected, run
|
45
|
+
|
46
|
+
```
|
47
|
+
fastlane run sync_devices devices_file:devices.tsv
|
48
|
+
```
|
49
|
+
|
50
|
+
You will see the remote devices are synchronized with your devices.tsv.
|
51
|
+
|
52
|
+
|
53
|
+
## Advanced Usage
|
54
|
+
|
55
|
+
### Use Property List file instead of TSV
|
56
|
+
|
57
|
+
Apple Developer Portal also accepts a devices file in Property List format like this.
|
58
|
+
|
59
|
+
```xml
|
60
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
61
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
62
|
+
<plist version="1.0">
|
63
|
+
<dict>
|
64
|
+
<key>Device UDIDs</key>
|
65
|
+
<array>
|
66
|
+
<dict>
|
67
|
+
<key>deviceIdentifier</key>
|
68
|
+
<string>01234567-89ABCDEF01234567</string>
|
69
|
+
<key>deviceName</key>
|
70
|
+
<string>NAME1</string>
|
71
|
+
<key>devicePlatform</key>
|
72
|
+
<string>ios</string>
|
73
|
+
</dict>
|
74
|
+
</array>
|
75
|
+
</dict>
|
76
|
+
</plist>
|
77
|
+
```
|
78
|
+
|
79
|
+
If you want to use Property List format, just pass the file to `sync_devices`.
|
80
|
+
|
81
|
+
```
|
82
|
+
fastlane run sync_devices devices_file:devices.deviceids
|
83
|
+
```
|
84
|
+
|
85
|
+
Following Apple's guide, I added `.deviceids` file extension but you can use standard `.xml` or `.plist` as well.
|
86
|
+
|
87
|
+
```
|
88
|
+
fastlane run sync_devices devices_file:devices.xml
|
89
|
+
```
|
90
|
+
|
91
|
+
## Example
|
92
|
+
|
93
|
+
Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
|
94
|
+
|
95
|
+
## Run tests for this plugin
|
96
|
+
|
97
|
+
To run both the tests, and code style validation, run
|
98
|
+
|
99
|
+
```
|
100
|
+
bundle exec rake
|
101
|
+
```
|
102
|
+
|
103
|
+
To automatically fix many of the styling issues, use
|
104
|
+
```
|
105
|
+
bundle exec rake rubocop:autocorrect
|
106
|
+
```
|
107
|
+
|
108
|
+
You can check other useful tasks by running
|
109
|
+
|
110
|
+
```
|
111
|
+
bundle exec rake -T
|
112
|
+
```
|
113
|
+
|
114
|
+
## Issues and Feedback
|
115
|
+
|
116
|
+
For any other issues and feedback about this plugin, please submit it to [this repository](https://github.com/manicmaniac/fastlane-plugin-sync_devices).
|
117
|
+
|
118
|
+
## Troubleshooting
|
119
|
+
|
120
|
+
If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
|
121
|
+
|
122
|
+
## Using _fastlane_ Plugins
|
123
|
+
|
124
|
+
For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
|
125
|
+
|
126
|
+
## About _fastlane_
|
127
|
+
|
128
|
+
_fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'credentials_manager'
|
4
|
+
require 'fastlane/action'
|
5
|
+
require_relative '../helper/sync_devices_helper'
|
6
|
+
|
7
|
+
module Fastlane # rubocop:disable Style/Documentation
|
8
|
+
# @see https://rubydoc.info/gems/fastlane/FastlaneCore/UI
|
9
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
10
|
+
|
11
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane/Actions
|
12
|
+
module Actions
|
13
|
+
# Fastlane action to synchronize remote devices on Apple Developer Portal with local devices file.
|
14
|
+
class SyncDevicesAction < Action # rubocop:disable Metrics/ClassLength
|
15
|
+
include Fastlane::Helper::SyncDevicesHelper
|
16
|
+
|
17
|
+
# The main entry point of this plugin.
|
18
|
+
#
|
19
|
+
# @param params [Hash] options passed to this plugin. See {.available_options} for details.
|
20
|
+
# @option params [Boolean] :dry_run
|
21
|
+
# @option params [String] :devices_file
|
22
|
+
# @option params [String, nil] :api_key_path
|
23
|
+
# @option params [Hash, nil] :api_key
|
24
|
+
# @option params [String, nil] :team_id
|
25
|
+
# @option params [String, nil] :team_name
|
26
|
+
# @option params [String, nil] :username
|
27
|
+
# @return [void]
|
28
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.run Fastlane::Action.run
|
29
|
+
def self.run(params)
|
30
|
+
require 'spaceship/connect_api'
|
31
|
+
|
32
|
+
devices_file = params[:devices_file]
|
33
|
+
UI.user_error!('You must pass `devices_file`. Please check the readme.') unless devices_file
|
34
|
+
spaceship_login(params)
|
35
|
+
new_devices = DevicesFile.load(devices_file)
|
36
|
+
|
37
|
+
UI.message('Fetching list of currently registered devices...')
|
38
|
+
current_devices = Spaceship::ConnectAPI::Device.all
|
39
|
+
patch = DevicesPatch.new(current_devices, new_devices)
|
40
|
+
patch.apply!(dry_run: params[:dry_run])
|
41
|
+
|
42
|
+
UI.success('Successfully registered new devices.')
|
43
|
+
end
|
44
|
+
|
45
|
+
# Authenticates the user with AppStore Connect API or Apple Developer Portal.
|
46
|
+
#
|
47
|
+
# @param (see .run)
|
48
|
+
# @return [void]
|
49
|
+
def self.spaceship_login(params)
|
50
|
+
api_token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path])
|
51
|
+
if api_token
|
52
|
+
UI.message('Creating authorization token for App Store Connect API')
|
53
|
+
Spaceship::ConnectAPI.token = api_token
|
54
|
+
elsif Spaceship::ConnectAPI.token
|
55
|
+
UI.message('Using existing authorization token for App Store Connect API')
|
56
|
+
else
|
57
|
+
UI.message("Login to App Store Connect (#{params[:username]})")
|
58
|
+
credentials = CredentialsManager::AccountManager.new(user: params[:username])
|
59
|
+
Spaceship::ConnectAPI.login(credentials.user, credentials.password, use_portal: true, use_tunes: false)
|
60
|
+
UI.message('Login Successful')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
private_class_method :spaceship_login
|
64
|
+
|
65
|
+
# Description of this plugin.
|
66
|
+
#
|
67
|
+
# @return [String]
|
68
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.description Fastlane::Action.description
|
69
|
+
def self.description
|
70
|
+
'Synchronize your devices with Apple Developer Portal.'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Authors of this plugin.
|
74
|
+
#
|
75
|
+
# @return [Array<String>]
|
76
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.authors Fastlane::Action.authors
|
77
|
+
def self.authors
|
78
|
+
['Ryosuke Ito']
|
79
|
+
end
|
80
|
+
|
81
|
+
# Detailed description of this plugin.
|
82
|
+
#
|
83
|
+
# @return [String]
|
84
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.details Fastlane::Action.details
|
85
|
+
def self.details
|
86
|
+
<<~DETAILS
|
87
|
+
This will synchronize iOS/Mac devices with the Apple Developer Portal so that you can include them in your provisioning profiles.
|
88
|
+
Unlike `register_devices` action, this action may disable, enable or rename devices.
|
89
|
+
Maybe it sounds dangerous but actually it does not delete anything, so you can recover the changes by yourself if needed.
|
90
|
+
|
91
|
+
The action will connect to the Apple Developer Portal using AppStore Connect API.
|
92
|
+
DETAILS
|
93
|
+
end
|
94
|
+
|
95
|
+
# Available options of this plugin.
|
96
|
+
#
|
97
|
+
# @return [Array<FastlaneCore::ConfigItem>]
|
98
|
+
#
|
99
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.available_options Fastlane::Action.available_options
|
100
|
+
# @see https://rubydoc.info/gems/fastlane/FastlaneCore/ConfigItem FastlaneCore::ConfigItem
|
101
|
+
def self.available_options # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
102
|
+
[
|
103
|
+
FastlaneCore::ConfigItem.new(
|
104
|
+
key: :dry_run,
|
105
|
+
env_name: 'FL_SYNC_DEVICES_DRY_RUN',
|
106
|
+
description: 'Do not modify the registered devices but just print what will be done',
|
107
|
+
type: Boolean,
|
108
|
+
default_value: false,
|
109
|
+
optional: true
|
110
|
+
),
|
111
|
+
FastlaneCore::ConfigItem.new(
|
112
|
+
key: :devices_file,
|
113
|
+
env_name: 'FL_SYNC_DEVICES_FILE',
|
114
|
+
description: 'Provide a path to a file with the devices to register. ' \
|
115
|
+
'For the format of the file see the examples',
|
116
|
+
optional: true,
|
117
|
+
verify_block: proc do |value|
|
118
|
+
UI.user_error!("Could not find file '#{value}'") unless File.exist?(value)
|
119
|
+
end
|
120
|
+
),
|
121
|
+
FastlaneCore::ConfigItem.new(
|
122
|
+
key: :api_key_path,
|
123
|
+
env_names: %w[FL_SYNC_DEVICES_API_KEY_PATH APP_STORE_CONNECT_API_KEY_PATH],
|
124
|
+
description: 'Path to your App Store Connect API Key JSON file ' \
|
125
|
+
'(https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file)',
|
126
|
+
optional: true,
|
127
|
+
conflicting_options: [:api_key],
|
128
|
+
verify_block: proc do |value|
|
129
|
+
UI.user_error!("Couldn't find API key JSON file at path '#{value}'") unless File.exist?(value)
|
130
|
+
end
|
131
|
+
),
|
132
|
+
FastlaneCore::ConfigItem.new(
|
133
|
+
key: :api_key,
|
134
|
+
env_names: %w[FL_SYNC_DEVICES_API_KEY APP_STORE_CONNECT_API_KEY],
|
135
|
+
description: 'Your App Store Connect API Key information ' \
|
136
|
+
'(https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option)',
|
137
|
+
type: Hash,
|
138
|
+
default_value: Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::APP_STORE_CONNECT_API_KEY],
|
139
|
+
default_value_dynamic: true,
|
140
|
+
optional: true,
|
141
|
+
sensitive: true,
|
142
|
+
conflicting_options: [:api_key_path]
|
143
|
+
),
|
144
|
+
FastlaneCore::ConfigItem.new(
|
145
|
+
key: :team_id,
|
146
|
+
env_name: 'SYNC_DEVICES_TEAM_ID',
|
147
|
+
code_gen_sensitive: true,
|
148
|
+
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:team_id),
|
149
|
+
default_value_dynamic: true,
|
150
|
+
description: "The ID of your Developer Portal team if you're in multiple teams",
|
151
|
+
optional: true,
|
152
|
+
verify_block: proc do |value|
|
153
|
+
ENV['FASTLANE_TEAM_ID'] = value.to_s
|
154
|
+
end
|
155
|
+
),
|
156
|
+
FastlaneCore::ConfigItem.new(
|
157
|
+
key: :team_name,
|
158
|
+
env_name: 'SYNC_DEVICES_TEAM_NAME',
|
159
|
+
description: "The name of your Developer Portal team if you're in multiple teams",
|
160
|
+
optional: true,
|
161
|
+
code_gen_sensitive: true,
|
162
|
+
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:team_name),
|
163
|
+
default_value_dynamic: true,
|
164
|
+
verify_block: proc do |value|
|
165
|
+
ENV['FASTLANE_TEAM_NAME'] = value.to_s
|
166
|
+
end
|
167
|
+
),
|
168
|
+
FastlaneCore::ConfigItem.new(
|
169
|
+
key: :username,
|
170
|
+
env_name: 'DELIVER_USER',
|
171
|
+
description: 'Optional: Your Apple ID',
|
172
|
+
optional: true,
|
173
|
+
default_value: CredentialsManager::AppfileConfig.try_fetch_value(:apple_id),
|
174
|
+
default_value_dynamic: true
|
175
|
+
)
|
176
|
+
]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Whether if this plugin is supported on the platform.
|
180
|
+
#
|
181
|
+
# @param _platform [Symbol] unused
|
182
|
+
# @return [true]
|
183
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.is_supported%3F Fastlane::Action.is_supported?
|
184
|
+
def self.is_supported?(_platform) # rubocop:disable Naming/PredicateName
|
185
|
+
true
|
186
|
+
end
|
187
|
+
|
188
|
+
# Example usages of this plugin.
|
189
|
+
#
|
190
|
+
# @return [Array<String>]
|
191
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.example_code Fastlane::Action.example_code
|
192
|
+
def self.example_code
|
193
|
+
[
|
194
|
+
<<~EX_1,
|
195
|
+
# Provide TSV file
|
196
|
+
sync_devices(devices_file: '/path/to/devices.txt')
|
197
|
+
EX_1
|
198
|
+
<<~EX_2,
|
199
|
+
# Provide Property List file, with configuring credentials
|
200
|
+
sync_devices(
|
201
|
+
devices_file: '/path/to/devices.deviceids',
|
202
|
+
team_id: 'ABCDEFGHIJ',
|
203
|
+
api_key_path: '/path/to/api_key.json'
|
204
|
+
)
|
205
|
+
EX_2
|
206
|
+
<<~EX_3
|
207
|
+
# Just check what will occur
|
208
|
+
sync_devices(devices_file: '/path/to/devices.txt', dry_run: true)
|
209
|
+
EX_3
|
210
|
+
]
|
211
|
+
end
|
212
|
+
|
213
|
+
# Category of this plugin.
|
214
|
+
#
|
215
|
+
# @return [Symbol]
|
216
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane%2FAction.category Fastlane::Action.category
|
217
|
+
def self.category
|
218
|
+
:code_signing
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spaceship/connect_api'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
module SyncDevicesHelper
|
8
|
+
# Namespace to organize command classes.
|
9
|
+
module Command
|
10
|
+
# Abstract base class of command object.
|
11
|
+
#
|
12
|
+
# @attr device [Spaceship::ConnectAPI::Device]
|
13
|
+
Base = Struct.new(:device) do
|
14
|
+
# Communicate with AppStore Connect and change the remote device.
|
15
|
+
# @abstract
|
16
|
+
# @return [void]
|
17
|
+
def run
|
18
|
+
# :nocov:
|
19
|
+
raise NotImplementedError
|
20
|
+
# :nocov:
|
21
|
+
end
|
22
|
+
|
23
|
+
# Description of this command.
|
24
|
+
# @abstract
|
25
|
+
# @return [String]
|
26
|
+
def description
|
27
|
+
# :nocov:
|
28
|
+
raise NotImplementedError
|
29
|
+
# :nocov:
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Command to do nothing.
|
34
|
+
class Noop < Base
|
35
|
+
# @return (see Base#run)
|
36
|
+
def run
|
37
|
+
# Does nothing.
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return (see Base#description)
|
41
|
+
def description
|
42
|
+
"Skipped #{device.name} (#{device.udid})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Command to disable an existing device.
|
47
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device
|
48
|
+
class Disable < Base
|
49
|
+
# @return (see Base#run)
|
50
|
+
def run
|
51
|
+
Spaceship::ConnectAPI::Device.disable(device.udid)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return (see Base#description)
|
55
|
+
def description
|
56
|
+
"Disabled #{device.name} (#{device.udid})"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Command to enable an existing device.
|
61
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device
|
62
|
+
class Enable < Base
|
63
|
+
# @return (see Base#run)
|
64
|
+
def run
|
65
|
+
Spaceship::ConnectAPI::Device.enable(device.udid)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return (see Base#description)
|
69
|
+
def description
|
70
|
+
"Enabled #{device.name} (#{device.udid})"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Command to rename an existing device.
|
75
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device
|
76
|
+
class Rename < Base
|
77
|
+
# @return [String]
|
78
|
+
attr_reader :name
|
79
|
+
|
80
|
+
# @param device [Spaceship::ConnectAPI::Device]
|
81
|
+
# @param name [String]
|
82
|
+
def initialize(device, name)
|
83
|
+
super(device)
|
84
|
+
@name = name
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return (see Base#run)
|
88
|
+
def run
|
89
|
+
Spaceship::ConnectAPI::Device.rename(device.udid, name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return (see Base#description)
|
93
|
+
def description
|
94
|
+
"Renamed #{device.name} to #{name} (#{device.udid})"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Command to disable and rename an existing device.
|
99
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device
|
100
|
+
class DisableAndRename < Base
|
101
|
+
# @return [String]
|
102
|
+
attr_reader :name
|
103
|
+
|
104
|
+
# @param device [Spaceship::ConnectAPI::Device]
|
105
|
+
# @param name [String]
|
106
|
+
def initialize(device, name)
|
107
|
+
super(device)
|
108
|
+
@name = name
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return (see Base#run)
|
112
|
+
def run
|
113
|
+
Spaceship::ConnectAPI::Device.modify(
|
114
|
+
device.udid,
|
115
|
+
enabled: false,
|
116
|
+
new_name: name
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return (see Base#description)
|
121
|
+
def description
|
122
|
+
"Disabled and renamed #{device.name} to #{name} (#{device.udid})"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Command to enable and rename an existing device.
|
127
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device
|
128
|
+
class EnableAndRename < Base
|
129
|
+
# @return [String] name
|
130
|
+
attr_reader :name
|
131
|
+
|
132
|
+
# @param device [Spaceship::ConnectAPI::Device]
|
133
|
+
# @param name [String]
|
134
|
+
def initialize(device, name)
|
135
|
+
super(device)
|
136
|
+
@name = name
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return (see Base#run)
|
140
|
+
def run
|
141
|
+
Spaceship::ConnectAPI::Device.modify(
|
142
|
+
device.udid,
|
143
|
+
enabled: true,
|
144
|
+
new_name: name
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return (see Base#description)
|
149
|
+
def description
|
150
|
+
"Enabled and renamed #{device.name} to #{name} (#{device.udid})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Command to register a new device.
|
155
|
+
# @see https://developer.apple.com/documentation/appstoreconnectapi/register_a_new_device
|
156
|
+
class Create < Base
|
157
|
+
# @return (see Base#run)
|
158
|
+
def run
|
159
|
+
Spaceship::ConnectAPI::Device.create(
|
160
|
+
name: device.name,
|
161
|
+
platform: device.platform,
|
162
|
+
udid: device.udid
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return (see Base#description)
|
167
|
+
def description
|
168
|
+
"Created #{device.name} (#{device.udid})"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'command'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
module SyncDevicesHelper
|
8
|
+
# Represents differences between 2 devices.
|
9
|
+
class DevicePatch
|
10
|
+
# @return [Spaceship::ConnectAPI::Device]
|
11
|
+
attr_reader :old_device, :new_device
|
12
|
+
|
13
|
+
# @param old_device [Spaceship::ConnectAPI::Device, nil]
|
14
|
+
# @param new_device [Spaceship::ConnectAPI::Device, nil]
|
15
|
+
def initialize(old_device, new_device)
|
16
|
+
@old_device = old_device
|
17
|
+
@new_device = new_device
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean]
|
21
|
+
def renamed?
|
22
|
+
!!old_device && !!new_device && old_device.name != new_device.name
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Boolean]
|
26
|
+
def enabled?
|
27
|
+
!!old_device && !old_device.enabled? && !!new_device&.enabled?
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Boolean]
|
31
|
+
def disabled?
|
32
|
+
!!old_device && old_device.enabled? && !new_device&.enabled?
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Boolean]
|
36
|
+
def created?
|
37
|
+
old_device.nil? && !!new_device&.enabled?
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Boolean]
|
41
|
+
def platform_changed?
|
42
|
+
!!old_device && !!new_device && old_device.platform != new_device.platform
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Command::Base]
|
46
|
+
# @raise [UnsupportedOperation]
|
47
|
+
def command # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
48
|
+
raise UnsupportedOperation.change_platform(old_device, new_device) if platform_changed?
|
49
|
+
|
50
|
+
case [renamed?, enabled?, disabled?, created?]
|
51
|
+
when [false, false, false, false]
|
52
|
+
Command::Noop.new(old_device)
|
53
|
+
when [false, false, false, true]
|
54
|
+
Command::Create.new(new_device)
|
55
|
+
when [false, false, true, false]
|
56
|
+
Command::Disable.new(old_device)
|
57
|
+
when [false, true, false, false]
|
58
|
+
Command::Enable.new(old_device)
|
59
|
+
when [true, false, false, false]
|
60
|
+
Command::Rename.new(old_device, new_device.name)
|
61
|
+
when [true, false, true, false]
|
62
|
+
Command::DisableAndRename.new(old_device, new_device.name)
|
63
|
+
when [true, true, false, false]
|
64
|
+
Command::EnableAndRename.new(old_device, new_device.name)
|
65
|
+
else
|
66
|
+
# :nocov:
|
67
|
+
raise UnsupportedOperation.internal_inconsistency(self, old_device, new_device)
|
68
|
+
# :nocov:
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Generic error that is raised if no operation is defined in AppStore Connect API for diff of devices.
|
74
|
+
class UnsupportedOperation < StandardError
|
75
|
+
# @return [Spaceship::ConnectAPI::Device]
|
76
|
+
attr_reader :old_device, :new_device
|
77
|
+
|
78
|
+
# @param message [String]
|
79
|
+
# @param old_device [Spaceship::ConnectAPI::Device]
|
80
|
+
# @param new_device [Spaceship::ConnectAPI::Device]
|
81
|
+
def initialize(message, old_device, new_device)
|
82
|
+
super(message)
|
83
|
+
@old_device = old_device
|
84
|
+
@new_device = new_device
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param old_device [Spaceship::ConnectAPI::Device]
|
88
|
+
# @param new_device [Spaceship::ConnectAPI::Device]
|
89
|
+
# @return [UnsupportedOperation]
|
90
|
+
def self.change_platform(old_device, new_device)
|
91
|
+
message = "Cannot change platform of the device '#{new_device.udid}' " \
|
92
|
+
"(#{old_device.platform} -> #{new_device.platform})"
|
93
|
+
new(message, old_device, new_device)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param patch [DevicePatch]
|
97
|
+
# @param old_device [Spaceship::ConnectAPI::Device]
|
98
|
+
# @param new_device [Spaceship::ConnectAPI::Device]
|
99
|
+
# @return [UnsupportedOperation]
|
100
|
+
def self.internal_inconsistency(patch, old_device, new_device)
|
101
|
+
info = {
|
102
|
+
renamed?: patch.renamed?,
|
103
|
+
enabled?: patch.enabled?,
|
104
|
+
disabled?: patch.disabled?,
|
105
|
+
created?: patch.created?
|
106
|
+
}
|
107
|
+
new(
|
108
|
+
"Cannot change #{old_device} to #{new_device} because of internal inconsistency. #{info}",
|
109
|
+
old_device,
|
110
|
+
new_device
|
111
|
+
)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module Helper
|
5
|
+
module SyncDevicesHelper
|
6
|
+
# Collection of methods that manipulates TSV or XML devices file.
|
7
|
+
#
|
8
|
+
# @see https://developer.apple.com/account/resources/downloads/Multiple-Upload-Samples.zip
|
9
|
+
module DevicesFile # rubocop:disable Metrics/ModuleLength
|
10
|
+
# Loads a devices file and parse it as an array of devices.
|
11
|
+
# If the file extension is one of +.deviceids+, +.plist+ and +.xml+, this method delegates to {.load_plist},
|
12
|
+
# otherwise to {.load_tsv}.
|
13
|
+
#
|
14
|
+
# @param path [String] path to the output file
|
15
|
+
# @return [Array<Spaceship::ConnectAPI::Device>]
|
16
|
+
def self.load(path)
|
17
|
+
return load_plist(path) if %w[.deviceids .plist .xml].include?(File.extname(path))
|
18
|
+
|
19
|
+
load_tsv(path)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param path [String]
|
23
|
+
# @return [Array<Spaceship::ConnectAPI::Device>]
|
24
|
+
def self.load_tsv(path)
|
25
|
+
require 'csv'
|
26
|
+
require 'spaceship/connect_api'
|
27
|
+
|
28
|
+
table = CSV.read(path, headers: true, col_sep: "\t")
|
29
|
+
validate_headers(table.headers, path)
|
30
|
+
|
31
|
+
devices = table.map.with_index(2) do |row, line_number|
|
32
|
+
validate_row(row, path, line_number)
|
33
|
+
Spaceship::ConnectAPI::Device.new(
|
34
|
+
nil,
|
35
|
+
{
|
36
|
+
name: row['Device Name'],
|
37
|
+
udid: row['Device ID'],
|
38
|
+
platform: parse_platform(row['Device Platform'], path),
|
39
|
+
status: Spaceship::ConnectAPI::Device::Status::ENABLED
|
40
|
+
}
|
41
|
+
)
|
42
|
+
end
|
43
|
+
validate_devices(devices, path)
|
44
|
+
devices
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param path [String]
|
48
|
+
# @return [Array<Spaceship::ConnectAPI::Device>]
|
49
|
+
def self.load_plist(path)
|
50
|
+
require 'cfpropertylist'
|
51
|
+
require 'spaceship/connect_api'
|
52
|
+
|
53
|
+
plist = CFPropertyList::List.new(file: path)
|
54
|
+
items = CFPropertyList.native_types(plist.value)['Device UDIDs']
|
55
|
+
devices = items.map.with_index do |item, index|
|
56
|
+
validate_dict_item(item, index, path)
|
57
|
+
Spaceship::ConnectAPI::Device.new(
|
58
|
+
nil,
|
59
|
+
{
|
60
|
+
name: item['deviceName'],
|
61
|
+
udid: item['deviceIdentifier'],
|
62
|
+
platform: parse_platform(item['devicePlatform'], path),
|
63
|
+
status: Spaceship::ConnectAPI::Device::Status::ENABLED
|
64
|
+
}
|
65
|
+
)
|
66
|
+
end
|
67
|
+
validate_devices(devices, path)
|
68
|
+
devices
|
69
|
+
end
|
70
|
+
|
71
|
+
# Dumps devices to devices file specified by path.
|
72
|
+
# This method delegates to either of {.dump_tsv} or {.dump_plist} depending on +format+.
|
73
|
+
#
|
74
|
+
# @param devices [Array<Spaceship::ConnectAPI::Device>] device objects to dump
|
75
|
+
# @param path [String] path to the output file
|
76
|
+
# @param format [:tsv, :plist] output format
|
77
|
+
# @return [void]
|
78
|
+
def self.dump(devices, path, format: :tsv)
|
79
|
+
case format
|
80
|
+
when :tsv
|
81
|
+
dump_tsv(devices, path)
|
82
|
+
when :plist
|
83
|
+
dump_plist(devices, path)
|
84
|
+
else
|
85
|
+
raise "Unsupported format '#{format}'."
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param devices [Array<Spaceship::ConnectAPI::Device>] device objects to dump
|
90
|
+
# @param path [String] path to the output file
|
91
|
+
# @return [void]
|
92
|
+
def self.dump_tsv(devices, path)
|
93
|
+
require 'csv'
|
94
|
+
|
95
|
+
CSV.open(path, 'w', col_sep: "\t", headers: true, write_headers: true) do |csv|
|
96
|
+
csv << HEADERS
|
97
|
+
devices.each do |device|
|
98
|
+
csv << [device.udid, device.name, device.platform]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param devices [Array<Spaceship::ConnectAPI::Device>] device objects to dump
|
104
|
+
# @param path [String] path to the output file
|
105
|
+
# @return [void]
|
106
|
+
def self.dump_plist(devices, path)
|
107
|
+
require 'cfpropertylist'
|
108
|
+
|
109
|
+
plist = CFPropertyList::List.new
|
110
|
+
plist.value = CFPropertyList.guess(
|
111
|
+
{
|
112
|
+
'Device UDIDs' => devices.map do |device|
|
113
|
+
{
|
114
|
+
deviceIdentifier: device.udid,
|
115
|
+
deviceName: device.name,
|
116
|
+
devicePlatform: device.platform.downcase
|
117
|
+
}
|
118
|
+
end
|
119
|
+
}
|
120
|
+
)
|
121
|
+
plist.save(path, CFPropertyList::List::FORMAT_XML)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Maximum length of a device name that is permitted by Apple Developer Portal.
|
125
|
+
#
|
126
|
+
# @return [Integer]
|
127
|
+
MAX_DEVICE_NAME_LENGTH = 50
|
128
|
+
|
129
|
+
# @param devices [Array<Spaceship::ConnectAPI::Device>] device objects to dump
|
130
|
+
# @param path [String]
|
131
|
+
# @return [void]
|
132
|
+
# @raise [InvalidDevicesFile]
|
133
|
+
def self.validate_devices(devices, path) # rubocop:disable Metrics/AbcSize
|
134
|
+
seen_udids = []
|
135
|
+
devices.each do |device|
|
136
|
+
udid = device.udid&.downcase
|
137
|
+
unless udid.match(udid_regex_for_platform(device.platform))
|
138
|
+
raise InvalidDevicesFile.invalid_udid(device.udid, path)
|
139
|
+
end
|
140
|
+
raise InvalidDevicesFile.udid_not_unique(device.udid, path) if seen_udids.include?(udid)
|
141
|
+
|
142
|
+
if device.name.size > MAX_DEVICE_NAME_LENGTH
|
143
|
+
raise InvalidDevicesFile.device_name_too_long(device.name, path)
|
144
|
+
end
|
145
|
+
|
146
|
+
seen_udids << udid
|
147
|
+
end
|
148
|
+
end
|
149
|
+
private_class_method :validate_devices
|
150
|
+
|
151
|
+
# @param platform_string [String]
|
152
|
+
# @param path [String]
|
153
|
+
# @return [String]
|
154
|
+
def self.parse_platform(platform_string, path)
|
155
|
+
Spaceship::ConnectAPI::BundleIdPlatform.map(platform_string || 'ios')
|
156
|
+
rescue RuntimeError => e
|
157
|
+
if e.message.include?('Cannot find a matching platform')
|
158
|
+
raise InvalidDevicesFile.unknown_platform(platform_string, path)
|
159
|
+
end
|
160
|
+
|
161
|
+
raise
|
162
|
+
end
|
163
|
+
private_class_method :parse_platform
|
164
|
+
|
165
|
+
HEADERS = ['Device ID', 'Device Name', 'Device Platform'].freeze
|
166
|
+
private_constant :HEADERS
|
167
|
+
|
168
|
+
SHORT_HEADERS = HEADERS[0..1].freeze
|
169
|
+
private_constant :SHORT_HEADERS
|
170
|
+
|
171
|
+
# @param [Array<String>] headers
|
172
|
+
# @param [String] path
|
173
|
+
# @raise [InvalidDevicesFile]
|
174
|
+
def self.validate_headers(headers, path)
|
175
|
+
raise InvalidDevicesFile.invalid_headers(path, 1) unless [HEADERS, SHORT_HEADERS].include?(headers.compact)
|
176
|
+
end
|
177
|
+
private_class_method :validate_headers
|
178
|
+
|
179
|
+
# @param row [CSV::Row]
|
180
|
+
# @param path [String]
|
181
|
+
# @param line_number [Integer]
|
182
|
+
# @return [void]
|
183
|
+
# @raise [InvalidDevicesFile]
|
184
|
+
def self.validate_row(row, path, line_number)
|
185
|
+
case row.fields.compact.size
|
186
|
+
when 0, 1
|
187
|
+
raise InvalidDevicesFile.columns_too_short(path, line_number)
|
188
|
+
when 2, 3
|
189
|
+
# Does nothing
|
190
|
+
else
|
191
|
+
raise InvalidDevicesFile.columns_too_long(path, line_number)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
private_class_method :validate_row
|
195
|
+
|
196
|
+
REQUIRED_KEYS = %w[deviceName deviceIdentifier].freeze
|
197
|
+
private_constant :REQUIRED_KEYS
|
198
|
+
|
199
|
+
# @param item [Hash<String, String>]
|
200
|
+
# @param index [Integer]
|
201
|
+
# @param path [String]
|
202
|
+
# @return [void]
|
203
|
+
# @raise [InvalidDevicesFile]
|
204
|
+
def self.validate_dict_item(item, index, path)
|
205
|
+
REQUIRED_KEYS.each do |key|
|
206
|
+
unless item.key?(key)
|
207
|
+
entry = ":Device UDIDs:#{index}:#{key}"
|
208
|
+
raise InvalidDevicesFile.missing_key(entry, path)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
private_class_method :validate_dict_item
|
213
|
+
|
214
|
+
# @param platform [String]
|
215
|
+
# @return [Regexp]
|
216
|
+
# @raise [TypeError] when platform is not in {Spaceship::ConnectAPI::BundleIdPlatform::ALL}.
|
217
|
+
def self.udid_regex_for_platform(platform)
|
218
|
+
case platform
|
219
|
+
when Spaceship::ConnectAPI::BundleIdPlatform::IOS
|
220
|
+
# @see https://www.theiphonewiki.com/wiki/UDID
|
221
|
+
/^(?:[0-9]{8}-[0-9a-f]{16}|[0-9a-f]{40})$/
|
222
|
+
when Spaceship::ConnectAPI::BundleIdPlatform::MAC_OS
|
223
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
224
|
+
else
|
225
|
+
# :nocov:
|
226
|
+
raise TypeError, "Unknown platform '#{platform}' is not in #{Spaceship::ConnectAPI::BundleIdPlatform::ALL}."
|
227
|
+
# :nocov:
|
228
|
+
end
|
229
|
+
end
|
230
|
+
private_class_method :udid_regex_for_platform
|
231
|
+
end
|
232
|
+
|
233
|
+
# Generic error that is raised if device file is not in the valid format.
|
234
|
+
class InvalidDevicesFile < StandardError
|
235
|
+
# URL of example devices files.
|
236
|
+
SAMPLE_FILE_URL = 'https://developer.apple.com/account/resources/downloads/Multiple-Upload-Samples.zip'
|
237
|
+
|
238
|
+
# @return [String]
|
239
|
+
attr_reader :path
|
240
|
+
# @return [Integer]
|
241
|
+
attr_reader :line_number
|
242
|
+
# @return [String, nil]
|
243
|
+
attr_reader :entry
|
244
|
+
|
245
|
+
# @param message [String]
|
246
|
+
# @param path [String]
|
247
|
+
# @param line_number [String, nil]
|
248
|
+
# @param entry [String, nil]
|
249
|
+
def initialize(message, path, line_number: nil, entry: nil)
|
250
|
+
super(format(message, { location: [path, line_number].join(':'), url: SAMPLE_FILE_URL }))
|
251
|
+
@path = path
|
252
|
+
@line_number = line_number
|
253
|
+
@entry = entry
|
254
|
+
end
|
255
|
+
|
256
|
+
# @param path [String]
|
257
|
+
# @param line_number [Integer]
|
258
|
+
# @return [InvalidDevicesFile]
|
259
|
+
def self.invalid_headers(path, line_number)
|
260
|
+
message = 'Invalid header line at %<location>s, please provide a file according to ' \
|
261
|
+
'the Apple Sample UDID file (%<url>s)'
|
262
|
+
new(message, path, line_number: line_number)
|
263
|
+
end
|
264
|
+
|
265
|
+
# @param path [String]
|
266
|
+
# @param line_number [Integer]
|
267
|
+
# @return [InvalidDevicesFile]
|
268
|
+
def self.columns_too_short(path, line_number)
|
269
|
+
message = 'Invalid device line at %<location>s, ensure you are using tabs (NOT spaces). ' \
|
270
|
+
"See Apple's sample/spec here: %<url>s"
|
271
|
+
new(message, path, line_number: line_number)
|
272
|
+
end
|
273
|
+
|
274
|
+
# @param path [String]
|
275
|
+
# @param line_number [Integer]
|
276
|
+
# @return [InvalidDevicesFile]
|
277
|
+
def self.columns_too_long(path, line_number)
|
278
|
+
message = 'Invalid device line at %<location>s, please provide a file according to ' \
|
279
|
+
'the Apple Sample UDID file (%<url>s)'
|
280
|
+
new(message, path, line_number: line_number)
|
281
|
+
end
|
282
|
+
|
283
|
+
# @param entry [String]
|
284
|
+
# @param path [String]
|
285
|
+
# @return [InvalidDevicesFile]
|
286
|
+
def self.missing_key(entry, path)
|
287
|
+
message = "Invalid device file at %<location>s, each item must have a required key '#{entry}', " \
|
288
|
+
"See Apple's sample/spec here: %<url>s"
|
289
|
+
new(message, path, entry: entry)
|
290
|
+
end
|
291
|
+
|
292
|
+
# @param udid [String]
|
293
|
+
# @param path [String]
|
294
|
+
# @return [InvalidDevicesFile]
|
295
|
+
def self.invalid_udid(udid, path)
|
296
|
+
new("Invalid UDID '#{udid}' at %<location>s, the UDID is not in the correct format", path)
|
297
|
+
end
|
298
|
+
|
299
|
+
# @param udid [String]
|
300
|
+
# @param path [String]
|
301
|
+
# @return [InvalidDevicesFile]
|
302
|
+
def self.udid_not_unique(udid, path)
|
303
|
+
message = "Invalid UDID '#{udid}' at %<location>s, there's another device with the same UDID is defined"
|
304
|
+
new(message, path)
|
305
|
+
end
|
306
|
+
|
307
|
+
# @param name [String]
|
308
|
+
# @param path [String]
|
309
|
+
# @return [InvalidDevicesFile]
|
310
|
+
def self.device_name_too_long(name, path)
|
311
|
+
message = "Invalid device name '#{name}' at %<location>s, a device name " \
|
312
|
+
"must be less than or equal to #{DevicesFile::MAX_DEVICE_NAME_LENGTH} characters long"
|
313
|
+
new(message, path)
|
314
|
+
end
|
315
|
+
|
316
|
+
# @param platform [String]
|
317
|
+
# @param path [String]
|
318
|
+
# @return [InvalidDevicesFile]
|
319
|
+
def self.unknown_platform(platform, path)
|
320
|
+
new("Unknown platform '#{platform}' at %<location>s", path)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'device_patch'
|
4
|
+
|
5
|
+
module Fastlane # rubocop:disable Style/Documentation
|
6
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
7
|
+
|
8
|
+
module Helper
|
9
|
+
module SyncDevicesHelper
|
10
|
+
# Represents a collection of {DevicePatch}.
|
11
|
+
class DevicesPatch
|
12
|
+
# @return [Spaceship::ConnectAPI::Device]
|
13
|
+
attr_reader :old_devices, :new_devices
|
14
|
+
# @return [Array<DevicePatch>]
|
15
|
+
attr_reader :commands
|
16
|
+
|
17
|
+
# @param old_devices [Array<Spaceship::ConnectAPI::Device>]
|
18
|
+
# @param new_devices [Array<Spaceship::ConnectAPI::Device>]
|
19
|
+
def initialize(old_devices, new_devices) # rubocop:disable Metrics/AbcSize
|
20
|
+
@old_devices = old_devices
|
21
|
+
@new_devices = new_devices
|
22
|
+
|
23
|
+
old_device_by_udid = old_devices.group_by { |d| d.udid.downcase }.transform_values(&:first)
|
24
|
+
new_device_by_udid = new_devices.group_by { |d| d.udid.downcase }.transform_values(&:first)
|
25
|
+
@commands = (old_device_by_udid.keys + new_device_by_udid.keys)
|
26
|
+
.sort
|
27
|
+
.uniq
|
28
|
+
.map do |udid|
|
29
|
+
old_device = old_device_by_udid[udid]
|
30
|
+
new_device = new_device_by_udid[udid]
|
31
|
+
DevicePatch.new(old_device, new_device).command
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param dry_run [Boolean]
|
36
|
+
# @return [void]
|
37
|
+
def apply!(dry_run: false)
|
38
|
+
@commands.each do |command|
|
39
|
+
if dry_run
|
40
|
+
UI.message("(dry-run) #{command.description}")
|
41
|
+
else
|
42
|
+
command.run
|
43
|
+
UI.message(command.description)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
# @see https://rubydoc.info/gems/fastlane/Fastlane/Helper Fastlane::Helper
|
5
|
+
module Helper
|
6
|
+
# Root namespace of +fastlane-plugin-sync_devices+ helpers.
|
7
|
+
module SyncDevicesHelper
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require_relative 'sync_devices_helper/devices_file'
|
13
|
+
require_relative 'sync_devices_helper/devices_patch'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'sync_devices/version'
|
4
|
+
require_relative 'sync_devices/actions/sync_devices_action'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
# Root namespace of +fastlane-plugin-sync_devices+ plugin.
|
8
|
+
module SyncDevices
|
9
|
+
# @return [Array<String>]
|
10
|
+
def self.all_classes
|
11
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', __dir__)]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Fastlane::SyncDevices.all_classes.each do |current|
|
17
|
+
require current
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fastlane-plugin-sync_devices
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryosuke Ito
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Synchronize your devices with Apple Developer Portal.
|
14
|
+
email: rito.0305@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- CHANGELOG.md
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- lib/fastlane/plugin/sync_devices.rb
|
23
|
+
- lib/fastlane/plugin/sync_devices/actions/sync_devices_action.rb
|
24
|
+
- lib/fastlane/plugin/sync_devices/helper/sync_devices_helper.rb
|
25
|
+
- lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/command.rb
|
26
|
+
- lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/device_patch.rb
|
27
|
+
- lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/devices_file.rb
|
28
|
+
- lib/fastlane/plugin/sync_devices/helper/sync_devices_helper/devices_patch.rb
|
29
|
+
- lib/fastlane/plugin/sync_devices/version.rb
|
30
|
+
homepage: https://github.com/manicmaniac/fastlane-plugin-sync_devices
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
bug_tracker_uri: https://github.com/manicmaniac/fastlane-plugin-sync_devices/issues
|
35
|
+
rubygems_mfa_required: 'true'
|
36
|
+
source_code_uri: https://github.com/manicmaniac/fastlane-plugin-sync_devices
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.7.0
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubygems_version: 3.4.6
|
53
|
+
signing_key:
|
54
|
+
specification_version: 4
|
55
|
+
summary: Synchronize your devices with Apple Developer Portal.
|
56
|
+
test_files: []
|