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.
@@ -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