match 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +331 -0
- data/bin/match +104 -0
- data/lib/assets/MatchfileTemplate +9 -0
- data/lib/assets/READMETemplate.md +49 -0
- data/lib/match.rb +26 -0
- data/lib/match/encrypt.rb +84 -0
- data/lib/match/generator.rb +67 -0
- data/lib/match/git_helper.rb +66 -0
- data/lib/match/nuke.rb +180 -0
- data/lib/match/options.rb +94 -0
- data/lib/match/runner.rb +93 -0
- data/lib/match/setup.rb +20 -0
- data/lib/match/spaceship_ensure.rb +64 -0
- data/lib/match/table_printer.rb +20 -0
- data/lib/match/utils.rb +24 -0
- data/lib/match/version.rb +4 -0
- metadata +303 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
git_url "[[GIT_URL]]"
|
2
|
+
|
3
|
+
type "development" # The default type, can be: appstore, adhoc or development
|
4
|
+
|
5
|
+
# app_identifier "tools.fastlane.app"
|
6
|
+
# username "user@fastlane.tools" # Your Apple Developer Portal username
|
7
|
+
|
8
|
+
# For all available options run `match --help`
|
9
|
+
# Remove the # in the beginning of the line to enable the other options
|
@@ -0,0 +1,49 @@
|
|
1
|
+
## [fastlane match](https://github.com/fastlane/match)
|
2
|
+
|
3
|
+
This repository contains all your certificates and provisioning profiles needed to build and sign your applications. They are encrypted using OpenSSL via a passphrase.
|
4
|
+
|
5
|
+
**Important:** Make sure this repository is set to private and only your team members have access to this repo.
|
6
|
+
|
7
|
+
Do not modify this file, as it gets overwritten every time you run `match`.
|
8
|
+
|
9
|
+
### Install [fastlane match](https://github.com/fastlane/match)
|
10
|
+
|
11
|
+
```
|
12
|
+
sudo gem install match
|
13
|
+
```
|
14
|
+
|
15
|
+
Make sure you have the latest version of the Xcode command line tools installed:
|
16
|
+
|
17
|
+
```
|
18
|
+
xcode-select --install
|
19
|
+
```
|
20
|
+
|
21
|
+
### Usage
|
22
|
+
|
23
|
+
Navigate to your project folder and run
|
24
|
+
|
25
|
+
```
|
26
|
+
match appstore
|
27
|
+
```
|
28
|
+
```
|
29
|
+
match adhoc
|
30
|
+
```
|
31
|
+
```
|
32
|
+
match development
|
33
|
+
```
|
34
|
+
|
35
|
+
For more information open [fastlane match git repo](https://github.com/fastlane/match)
|
36
|
+
|
37
|
+
### Content
|
38
|
+
|
39
|
+
#### certs
|
40
|
+
|
41
|
+
This directory contains all your certificates with their private keys
|
42
|
+
|
43
|
+
#### profiles
|
44
|
+
|
45
|
+
This directory contains all provisioning profiles
|
46
|
+
|
47
|
+
------------------------------------
|
48
|
+
|
49
|
+
For more information open [fastlane match git repo](https://github.com/fastlane/match)
|
data/lib/match.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'match/version'
|
2
|
+
require 'match/options'
|
3
|
+
require 'match/runner'
|
4
|
+
require 'match/nuke'
|
5
|
+
require 'match/utils'
|
6
|
+
require 'match/table_printer'
|
7
|
+
require 'match/git_helper'
|
8
|
+
require 'match/generator'
|
9
|
+
require 'match/setup'
|
10
|
+
require 'match/encrypt'
|
11
|
+
require 'match/spaceship_ensure'
|
12
|
+
|
13
|
+
require 'fastlane_core'
|
14
|
+
require 'terminal-table'
|
15
|
+
require 'spaceship'
|
16
|
+
|
17
|
+
module Match
|
18
|
+
Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
|
19
|
+
UI = FastlaneCore::UI
|
20
|
+
|
21
|
+
def self.environments
|
22
|
+
envs = %w(appstore adhoc development)
|
23
|
+
envs << "enterprise" if ENV["MATCH_FORCE_ENTERPRISE"]
|
24
|
+
return envs
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Match
|
2
|
+
class Encrypt
|
3
|
+
require 'security'
|
4
|
+
|
5
|
+
def server_name(git_url)
|
6
|
+
["match", git_url].join("_")
|
7
|
+
end
|
8
|
+
|
9
|
+
def password(git_url)
|
10
|
+
password ||= ENV["MATCH_PASSWORD"]
|
11
|
+
unless password
|
12
|
+
item = Security::InternetPassword.find(server: server_name(git_url))
|
13
|
+
password = item.password if item
|
14
|
+
end
|
15
|
+
|
16
|
+
unless password
|
17
|
+
UI.important "Enter the passphrase that should be used to encrypt/decrypt your certificates"
|
18
|
+
UI.important "This passphrase is specific per repository and will be stored in your local keychain"
|
19
|
+
UI.important "Make sure to remember the password, as you'll need it when you run match on a different machine"
|
20
|
+
while password.to_s.length == 0
|
21
|
+
password = ask("Passphrase for Git Repo: ".yellow) { |q| q.echo = "*" }
|
22
|
+
end
|
23
|
+
Security::InternetPassword.add(server_name(git_url), "", password)
|
24
|
+
end
|
25
|
+
|
26
|
+
return @password
|
27
|
+
end
|
28
|
+
|
29
|
+
# removes the password from the keychain again
|
30
|
+
def clear_password(git_url)
|
31
|
+
Security::InternetPassword.delete(server: server_name(git_url))
|
32
|
+
end
|
33
|
+
|
34
|
+
def encrypt_repo(path: nil, git_url: nil)
|
35
|
+
iterate(path) do |current|
|
36
|
+
crypt(path: current,
|
37
|
+
password: password(git_url),
|
38
|
+
encrypt: true)
|
39
|
+
UI.success "🔒 Encrypted '#{File.basename(current)}'" if $verbose
|
40
|
+
end
|
41
|
+
UI.success "🔒 Successfully encrypted certificates repo"
|
42
|
+
end
|
43
|
+
|
44
|
+
def decrypt_repo(path: nil, git_url: nil)
|
45
|
+
iterate(path) do |current|
|
46
|
+
begin
|
47
|
+
crypt(path: current,
|
48
|
+
password: password(git_url),
|
49
|
+
encrypt: false)
|
50
|
+
rescue
|
51
|
+
UI.error "Couldn't decrypt the repo, please make sure you enter the right password!"
|
52
|
+
clear_password(git_url)
|
53
|
+
decrypt_repo(path: path, git_url: git_url)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
UI.success "🔓 Decrypted '#{File.basename(current)}'" if $verbose
|
57
|
+
end
|
58
|
+
UI.success "🔓 Successfully decrypted certificates repo"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def iterate(source_path)
|
64
|
+
Dir[File.join(source_path, "**", "*.{cer,p12,mobileprovision}")].each do |path|
|
65
|
+
next if File.directory?(path)
|
66
|
+
yield(path)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def crypt(path: nil, password: nil, encrypt: true)
|
71
|
+
tmpfile = File.join(Dir.mktmpdir, "temporary")
|
72
|
+
command = ["openssl aes-256-cbc"]
|
73
|
+
command << "-k \"#{password}\""
|
74
|
+
command << "-in \"#{path}\""
|
75
|
+
command << "-out \"#{tmpfile}\""
|
76
|
+
command << "-a"
|
77
|
+
command << "-d" unless encrypt
|
78
|
+
command << "&> /dev/null" unless $verbose # to show show an error message is something goes wrong
|
79
|
+
success = system(command.join(' '))
|
80
|
+
raise "Error decrypting '#{path}'" unless success
|
81
|
+
FileUtils.mv(tmpfile, path)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Match
|
2
|
+
# Generate missing resources
|
3
|
+
class Generator
|
4
|
+
def self.generate_certificate(params, cert_type)
|
5
|
+
require 'cert'
|
6
|
+
output_path = File.join(params[:workspace], "certs", cert_type.to_s)
|
7
|
+
|
8
|
+
arguments = FastlaneCore::Configuration.create(Cert::Options.available_options, {
|
9
|
+
development: params[:type] == "development",
|
10
|
+
output_path: output_path,
|
11
|
+
force: true, # we don't need a certificate without its private key, we only care about a new certificate
|
12
|
+
username: params[:username]
|
13
|
+
})
|
14
|
+
|
15
|
+
Cert.config = arguments
|
16
|
+
|
17
|
+
begin
|
18
|
+
cert_path = Cert::Runner.new.launch
|
19
|
+
rescue => ex
|
20
|
+
if ex.to_s.include?("You already have a current")
|
21
|
+
UI.user_error!("Could not create a new certificate as you reached the maximum number of certificates for this account. You can use the `match nuke` command to revoke your existing certificates. More information https://github.com/fastlane/match")
|
22
|
+
else
|
23
|
+
raise ex
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# We don't care about the signing request
|
28
|
+
Dir[File.join(params[:workspace], "**", "*.certSigningRequest")].each { |path| File.delete(path) }
|
29
|
+
|
30
|
+
# we need to return the path
|
31
|
+
return cert_path
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return (String) The UUID of the newly generated profile
|
35
|
+
def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_id: nil)
|
36
|
+
require 'sigh'
|
37
|
+
|
38
|
+
prov_type = :enterprise if ENV["MATCH_FORCE_ENTERPRISE"] && ENV["SIGH_PROFILE_ENTERPRISE"]
|
39
|
+
profile_name = ["match", profile_type_name(prov_type), params[:app_identifier]].join(" ")
|
40
|
+
|
41
|
+
arguments = FastlaneCore::Configuration.create(Sigh::Options.available_options, {
|
42
|
+
app_identifier: params[:app_identifier],
|
43
|
+
adhoc: prov_type == :adhoc,
|
44
|
+
development: prov_type == :development,
|
45
|
+
output_path: File.join(params[:workspace], "profiles", prov_type.to_s),
|
46
|
+
username: params[:username],
|
47
|
+
force: true,
|
48
|
+
cert_id: certificate_id,
|
49
|
+
provisioning_name: profile_name,
|
50
|
+
ignore_profiles_with_different_name: true
|
51
|
+
})
|
52
|
+
|
53
|
+
Sigh.config = arguments
|
54
|
+
path = Sigh::Manager.start
|
55
|
+
return path
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return the name of the provisioning profile type
|
59
|
+
def self.profile_type_name(type)
|
60
|
+
return "Development" if type == :development
|
61
|
+
return "AdHoc" if type == :adhoc
|
62
|
+
return "AppStore" if type == :appstore
|
63
|
+
return "InHouse" if type == :enterprise
|
64
|
+
return "Unkown"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Match
|
2
|
+
class GitHelper
|
3
|
+
def self.clone(git_url)
|
4
|
+
return @dir if @dir
|
5
|
+
|
6
|
+
@dir = Dir.mktmpdir
|
7
|
+
command = "git clone '#{git_url}' '#{@dir}' --depth 1"
|
8
|
+
UI.message "Cloning remote git repo..."
|
9
|
+
FastlaneCore::CommandExecutor.execute(command: command,
|
10
|
+
print_all: $verbose,
|
11
|
+
print_command: $verbose)
|
12
|
+
|
13
|
+
raise "Error cloning repo, make sure you have access to it '#{git_url}'".red unless File.directory?(@dir)
|
14
|
+
|
15
|
+
copy_readme(@dir)
|
16
|
+
Encrypt.new.decrypt_repo(path: @dir, git_url: git_url)
|
17
|
+
|
18
|
+
return @dir
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.generate_commit_message(params)
|
22
|
+
# 'Automatic commit via fastlane'
|
23
|
+
[
|
24
|
+
"[fastlane]",
|
25
|
+
"Updated",
|
26
|
+
params[:app_identifier],
|
27
|
+
"for",
|
28
|
+
params[:type].to_s
|
29
|
+
].join(" ")
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.commit_changes(path, message, git_url)
|
33
|
+
Dir.chdir(path) do
|
34
|
+
return if `git status`.include?("nothing to commit")
|
35
|
+
|
36
|
+
Encrypt.new.encrypt_repo(path: path, git_url: git_url)
|
37
|
+
|
38
|
+
commands = []
|
39
|
+
commands << "git add -A"
|
40
|
+
commands << "git commit -m '#{message}'"
|
41
|
+
commands << "git push origin master"
|
42
|
+
|
43
|
+
UI.message "Pushing changes to remote git repo..."
|
44
|
+
|
45
|
+
commands.each do |command|
|
46
|
+
FastlaneCore::CommandExecutor.execute(command: command,
|
47
|
+
print_all: $verbose,
|
48
|
+
print_command: $verbose)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
@dir = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.clear_changes
|
55
|
+
FileUtils.rm_rf(@dir) if @dir # @dir might be nil in tests
|
56
|
+
UI.success "🔒 Successfully encrypted certificates repo" # so the user is happy
|
57
|
+
@dir = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Copies the README.md into the git repo
|
61
|
+
def self.copy_readme(directory)
|
62
|
+
template = File.read("#{Helper.gem_path('match')}/lib/assets/READMETemplate.md")
|
63
|
+
File.write(File.join(directory, "README.md"), template)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/match/nuke.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
module Match
|
2
|
+
class Nuke
|
3
|
+
attr_accessor :params
|
4
|
+
attr_accessor :type
|
5
|
+
|
6
|
+
attr_accessor :certs
|
7
|
+
attr_accessor :profiles
|
8
|
+
attr_accessor :files
|
9
|
+
|
10
|
+
def run(params, type: nil)
|
11
|
+
self.params = params
|
12
|
+
self.type = type
|
13
|
+
|
14
|
+
params[:workspace] = GitHelper.clone(params[:git_url])
|
15
|
+
|
16
|
+
had_app_identifier = self.params[:app_identifier]
|
17
|
+
self.params[:app_identifier] = '' # we don't really need a value here
|
18
|
+
FastlaneCore::PrintTable.print_values(config: params,
|
19
|
+
hide_keys: [:app_identifier, :workspace],
|
20
|
+
title: "Summary for match nuke #{Match::VERSION}")
|
21
|
+
|
22
|
+
prepare_list
|
23
|
+
print_tables
|
24
|
+
|
25
|
+
if params[:readonly]
|
26
|
+
UI.user_error!("`match nuke` doesn't delete anything when running with --readonly enabled")
|
27
|
+
end
|
28
|
+
|
29
|
+
if (self.certs + self.profiles + self.files).count > 0
|
30
|
+
UI.error "---"
|
31
|
+
UI.error "Are you sure you want to completely delete and revoke all the"
|
32
|
+
UI.error "certificates and provisioning profiles listed above? (y/n)"
|
33
|
+
UI.error "Warning: By nuking distribution, both App Store and Ad Hoc profiles will be deleted" if type == "distribution"
|
34
|
+
UI.error "Warning: The :app_identifier value will be ignored - this will all delete profiles for all your apps!" if had_app_identifier
|
35
|
+
UI.error "---"
|
36
|
+
if agree("(y/n)", true)
|
37
|
+
nuke_it_now!
|
38
|
+
UI.success "Successfully cleaned your account ♻️"
|
39
|
+
else
|
40
|
+
UI.success "Cancelled nuking #thanks 🏠 👨 👩 👧"
|
41
|
+
end
|
42
|
+
else
|
43
|
+
UI.success "No relevant certificates or provisioning profiles found, nothing to nuke here :)"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Collect all the certs/profiles
|
48
|
+
def prepare_list
|
49
|
+
UI.message "Fetching certificates and profiles..."
|
50
|
+
cert_type = type.to_sym
|
51
|
+
|
52
|
+
prov_types = [:development]
|
53
|
+
prov_types = [:appstore, :adhoc] if cert_type == :distribution
|
54
|
+
|
55
|
+
Spaceship.login(params[:username])
|
56
|
+
Spaceship.select_team
|
57
|
+
|
58
|
+
self.certs = certificate_type(cert_type).all
|
59
|
+
self.profiles = []
|
60
|
+
prov_types.each do |prov_type|
|
61
|
+
self.profiles += profile_type(prov_type).all
|
62
|
+
end
|
63
|
+
|
64
|
+
certs = Dir[File.join(params[:workspace], "**", cert_type.to_s, "*.cer")]
|
65
|
+
keys = Dir[File.join(params[:workspace], "**", cert_type.to_s, "*.p12")]
|
66
|
+
profiles = []
|
67
|
+
prov_types.each do |prov_type|
|
68
|
+
profiles += Dir[File.join(params[:workspace], "**", prov_type.to_s, "*.mobileprovision")]
|
69
|
+
end
|
70
|
+
|
71
|
+
self.files = certs + keys + profiles
|
72
|
+
end
|
73
|
+
|
74
|
+
# Print tables to ask the user
|
75
|
+
def print_tables
|
76
|
+
puts ""
|
77
|
+
if self.certs.count > 0
|
78
|
+
puts Terminal::Table.new({
|
79
|
+
title: "Certificates that are going to be revoked".green,
|
80
|
+
headings: ["Name", "ID", "Type", "Expires"],
|
81
|
+
rows: self.certs.collect { |c| [c.name, c.id, c.class.to_s.split("::").last, c.expires.strftime("%Y-%m-%d")] }
|
82
|
+
})
|
83
|
+
puts ""
|
84
|
+
end
|
85
|
+
|
86
|
+
if self.profiles.count > 0
|
87
|
+
puts Terminal::Table.new({
|
88
|
+
title: "Provisioning Profiles that are going to be revoked".green,
|
89
|
+
headings: ["Name", "ID", "Status", "Type", "Expires"],
|
90
|
+
rows: self.profiles.collect do |p|
|
91
|
+
status = p.status == 'Active' ? p.status.green : p.status.red
|
92
|
+
|
93
|
+
[p.name, p.id, status, p.type, p.expires.strftime("%Y-%m-%d")]
|
94
|
+
end
|
95
|
+
})
|
96
|
+
puts ""
|
97
|
+
end
|
98
|
+
|
99
|
+
if self.files.count > 0
|
100
|
+
puts Terminal::Table.new({
|
101
|
+
title: "Files that are going to be deleted".green,
|
102
|
+
headings: ["Type", "File Name"],
|
103
|
+
rows: self.files.collect do |f|
|
104
|
+
components = f.split(File::SEPARATOR)[-3..-1]
|
105
|
+
|
106
|
+
# from "...1o7xtmh/certs/distribution/8K38XUY3AY.cer" to "distribution cert"
|
107
|
+
file_type = components[0..1].reverse.join(" ")[0..-2]
|
108
|
+
|
109
|
+
[file_type, components[2]]
|
110
|
+
end
|
111
|
+
})
|
112
|
+
puts ""
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def nuke_it_now!
|
117
|
+
UI.header "Deleting #{self.profiles.count} provisioning profiles..." unless self.profiles.count == 0
|
118
|
+
self.profiles.each do |profile|
|
119
|
+
UI.message "Deleting profile '#{profile.name}' (#{profile.id})..."
|
120
|
+
profile.delete!
|
121
|
+
UI.success "Successfully deleted profile"
|
122
|
+
end
|
123
|
+
|
124
|
+
UI.header "Revoking #{self.certs.count} certificates..." unless self.certs.count == 0
|
125
|
+
self.certs.each do |cert|
|
126
|
+
UI.message "Revoking certificate '#{cert.name}' (#{cert.id})..."
|
127
|
+
cert.revoke!
|
128
|
+
UI.success "Successfully deleted certificate"
|
129
|
+
end
|
130
|
+
|
131
|
+
if self.files.count > 0
|
132
|
+
delete_files!
|
133
|
+
end
|
134
|
+
|
135
|
+
# Now we need to commit and push all this too
|
136
|
+
message = ["[fastlane]", "Nuked", "files", "for", type.to_s].join(" ")
|
137
|
+
GitHelper.commit_changes(params[:workspace], message, self.params[:git_url])
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def delete_files!
|
143
|
+
UI.header "Deleting #{self.files.count} files from the git repo..."
|
144
|
+
|
145
|
+
self.files.each do |file|
|
146
|
+
UI.message "Deleting file '#{File.basename(file)}'..."
|
147
|
+
|
148
|
+
# Check if the profile is installed on the local machine
|
149
|
+
if file.end_with?("mobileprovision")
|
150
|
+
parsed = FastlaneCore::ProvisioningProfile.parse(file)
|
151
|
+
uuid = parsed["UUID"]
|
152
|
+
path = Dir[File.join(FastlaneCore::ProvisioningProfile.profiles_path, "#{uuid}.mobileprovision")].last
|
153
|
+
File.delete(path) if path
|
154
|
+
end
|
155
|
+
|
156
|
+
File.delete(file)
|
157
|
+
UI.success "Successfully deleted file"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# The kind of certificate we're interested in
|
162
|
+
def certificate_type(type)
|
163
|
+
cert_type = Spaceship.certificate.production
|
164
|
+
cert_type = Spaceship.certificate.development if type == :development
|
165
|
+
cert_type = Spaceship.certificate.in_house if ENV["MATCH_FORCE_ENTERPRISE"] && Spaceship.client.in_house?
|
166
|
+
|
167
|
+
cert_type
|
168
|
+
end
|
169
|
+
|
170
|
+
# The kind of provisioning profile we're interested in
|
171
|
+
def profile_type(type)
|
172
|
+
profile_type = Spaceship.provisioning_profile.app_store
|
173
|
+
profile_type = Spaceship.provisioning_profile.in_house if ENV["MATCH_FORCE_ENTERPRISE"] && Spaceship.client.in_house?
|
174
|
+
profile_type = Spaceship.provisioning_profile.ad_hoc if type == :adhoc
|
175
|
+
profile_type = Spaceship.provisioning_profile.development if type == :development
|
176
|
+
|
177
|
+
profile_type
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|