XcodeCI 0.0.1
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/.gitignore +22 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/bin/xcodeci +28 -0
- data/lib/xcodeci.rb +19 -0
- data/lib/xcodeci/archiveutils.rb +24 -0
- data/lib/xcodeci/buildutils.rb +20 -0
- data/lib/xcodeci/command.rb +218 -0
- data/lib/xcodeci/configuration.rb +21 -0
- data/lib/xcodeci/database.rb +27 -0
- data/lib/xcodeci/gitutils.rb +48 -0
- data/lib/xcodeci/html_reporter.rb +78 -0
- data/lib/xcodeci/logger.rb +12 -0
- data/lib/xcodeci/version.rb +3 -0
- data/readme.md +95 -0
- data/templates/index.html +54 -0
- data/templates/manifest.plist +31 -0
- data/templates/xcodeci.conf.yaml +24 -0
- data/xcodeci.gemspec +27 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3685b018822354139c0f9e88ce48a12c03bda5d9
|
4
|
+
data.tar.gz: e4c4cefe8216c212f72d2f843cc22cec7ad163bf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6baa29afb994b528fe693f47a97d7948faab1b34ee938d4d8186cc0657ca47f23e622ee7951db4b3cb44d1649748e91b85450259e228488e48306d6e919fdea8
|
7
|
+
data.tar.gz: fc1f157cfdab32f8f32d74a6f837932238922e65bbe76cc0b03d91e9540297be1cc2bc529b12361440930482b7b365761a40be25fea341e55fc5ed2fbfe26cee
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Ignazio Calò
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/bin/xcodeci
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'xcodeci'
|
6
|
+
|
7
|
+
if ARGV[0] == 'clear_all'
|
8
|
+
puts "This command will remove all the working copies and any configuration file."
|
9
|
+
print "Are you sure ?[yn] "
|
10
|
+
STDOUT.flush
|
11
|
+
answer = STDIN.gets.chomp
|
12
|
+
if answer.downcase != 'y'
|
13
|
+
puts "Operation cancelled. Exit"
|
14
|
+
exit 0
|
15
|
+
end
|
16
|
+
|
17
|
+
FileUtils.rm_r(Xcodeci::HOME)
|
18
|
+
exit(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
#Xcodeci::Command.run(*ARGV)
|
22
|
+
|
23
|
+
while true
|
24
|
+
Xcodeci::Command.run(*ARGV)
|
25
|
+
sleep 60 * 5
|
26
|
+
end
|
27
|
+
|
28
|
+
|
data/lib/xcodeci.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
require 'colorize'
|
3
|
+
require 'yaml'
|
4
|
+
require 'ipa_reader'
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
autoload :Command, 'xcodeci/command'
|
8
|
+
autoload :Database, 'xcodeci/database'
|
9
|
+
autoload :GitUtils, 'xcodeci/gitutils'
|
10
|
+
autoload :BuildUtils, 'xcodeci/buildutils'
|
11
|
+
autoload :ArchiveUtils, 'xcodeci/archiveUtils'
|
12
|
+
autoload :Logger, 'xcodeci/logger'
|
13
|
+
autoload :HtmlReporter, 'xcodeci/html_reporter'
|
14
|
+
autoload :Configuration, 'xcodeci/configuration'
|
15
|
+
|
16
|
+
HOME = File.join(Dir.home, ".xcodeci")
|
17
|
+
TEMPLATE = File.expand_path("../../templates/", __FILE__)
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class ArchiveUtils
|
3
|
+
|
4
|
+
def archive_ipa(workspace, scheme, path)
|
5
|
+
build_dir = %x(xctool -configuration Release -workspace "#{workspace}" -scheme "#{scheme}" -sdk iphoneos -showBuildSettings | grep -m1 TARGET_BUILD_DIR | cut -d'=' -f2 | xargs | tr -d '\n')
|
6
|
+
app_name = %x(xctool -configuration Release -workspace "#{workspace}" -scheme "#{scheme}" -sdk iphoneos -showBuildSettings | grep -m1 WRAPPER_NAME | cut -d'=' -f2 | xargs | tr -d '\n')
|
7
|
+
|
8
|
+
%x( xcrun -sdk iphoneos PackageApplication -v "#{build_dir}/#{app_name}" -o "#{path}" )#--sign "#{SIGN_COMPANY_NAME}" )#--embed "#{PROVISIONING_PATH}")
|
9
|
+
$?.exitstatus.zero?
|
10
|
+
end
|
11
|
+
|
12
|
+
def save_dsym(workspace, scheme, path)
|
13
|
+
build_dir = %x(xctool -configuration Release -workspace "#{workspace}" -scheme "#{scheme}" -sdk iphoneos -showBuildSettings | grep -m1 TARGET_BUILD_DIR | cut -d'=' -f2 | xargs | tr -d '\n')
|
14
|
+
app_name = %x(xctool -configuration Release -workspace "#{workspace}" -scheme "#{scheme}" -sdk iphoneos -showBuildSettings | grep -m1 WRAPPER_NAME | cut -d'=' -f2 | xargs | tr -d '\n')
|
15
|
+
result = 0
|
16
|
+
begin
|
17
|
+
FileUtils.cp_r "#{build_dir}/#{app_name}.dSYM", path , :verbose => false
|
18
|
+
rescue => e
|
19
|
+
result = 1
|
20
|
+
end
|
21
|
+
result.zero?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class BuildUtils
|
3
|
+
|
4
|
+
def install_pod
|
5
|
+
%x(pod install)
|
6
|
+
$?.exitstatus.zero?
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_build(repository_url, workspace, scheme)
|
10
|
+
%x(xcodebuild -configuration Release -workspace \"#{workspace}\" -scheme \"#{scheme}\" -sdk iphoneos clean build 2>&1)
|
11
|
+
$?.exitstatus.zero?
|
12
|
+
end
|
13
|
+
|
14
|
+
def run_test(repository_name, workspace, scheme)
|
15
|
+
%x(xcodebuild -configuration Release -workspace \"#{workspace}\" -scheme \"#{scheme}\" -sdk iphonesimulator8.0 -arch i386 clean test)
|
16
|
+
# %x(xctool -configuration Release -workspace \"#{workspace}\" -scheme \"#{scheme}\" -sdk iphonesimulator test > /dev/null)
|
17
|
+
$?.exitstatus.zero?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class Command
|
3
|
+
def self.root_output_folder
|
4
|
+
File.join(@@dropbox_folder, "xcodeci")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.workspace_folder repo_url, prj_workspace
|
8
|
+
folder_name = repo_url[repo_url.rindex('/') + 1..-1].sub('.git', '')
|
9
|
+
folder_name = File.join(Xcodeci::HOME, folder_name)
|
10
|
+
path = Dir.glob("#{folder_name}/**/#{prj_workspace}").first
|
11
|
+
File.dirname(path)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.execute_in_workspace_folder (repository_url, workspace ,&block)
|
15
|
+
Dir.chdir workspace_folder repository_url, workspace
|
16
|
+
result = block.call
|
17
|
+
Dir.chdir "#{Xcodeci::HOME}"
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.repository_name repo_url
|
22
|
+
repo_url[repo_url.rindex('/') + 1..-1].sub('.git', '')
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.execute_in_repo_folder (repository_url, &block)
|
26
|
+
Dir.chdir "#{Xcodeci::HOME}/#{repository_name repository_url}"
|
27
|
+
result = block.call
|
28
|
+
Dir.chdir "#{Xcodeci::HOME}"
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def self.run
|
34
|
+
# Create the folder for storing all the projects
|
35
|
+
unless File.exists?(Xcodeci::HOME)
|
36
|
+
Dir.mkdir File.join(Xcodeci::HOME)
|
37
|
+
FileUtils.cp_r File.join(Xcodeci::TEMPLATE, "xcodeci.conf.yaml"), File.join(Xcodeci::HOME, "xcodeci.conf.yaml") , :verbose => false
|
38
|
+
puts "A sample configuration file was created on your ~/.xcodeci folder.".red
|
39
|
+
exit 0
|
40
|
+
end
|
41
|
+
|
42
|
+
configuration = Configuration.new ( File.join(Xcodeci::HOME, "xcodeci.conf.yaml") )
|
43
|
+
unless configuration.is_ok?
|
44
|
+
puts "Your configuration file is doesn't contain any project.".red
|
45
|
+
exit(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
@@dropbox_folder = configuration.app_config[:DROPBOX_FOLDER]
|
49
|
+
@@dropbox_user_id = configuration.app_config[:DROPBOX_USER_ID]
|
50
|
+
|
51
|
+
|
52
|
+
database = Database.new ( File.join(Xcodeci::HOME, "data.yaml") )
|
53
|
+
gitUtils = GitUtils.new
|
54
|
+
buildUtils = BuildUtils.new
|
55
|
+
archiveUtils = ArchiveUtils.new
|
56
|
+
l = Logger.new
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
#TODO check if the configuration is ok
|
61
|
+
puts "=== Start Loop ==="
|
62
|
+
|
63
|
+
configuration.each_project do | project |
|
64
|
+
puts "=== Start #{project[:APP_NAME]} ==="
|
65
|
+
|
66
|
+
# Create the dropbox shared folder
|
67
|
+
output_folder = File.join(root_output_folder, project[:APP_NAME])
|
68
|
+
FileUtils.mkdir_p(output_folder) unless File.exists?(output_folder)
|
69
|
+
|
70
|
+
# Cloning the repository
|
71
|
+
puts "> Cloning the repository #{project[:REPO_URL]}"
|
72
|
+
clone_result = gitUtils.cloning_repo project[:REPO_URL]
|
73
|
+
puts l.log_result clone_result, "Repository clone"
|
74
|
+
|
75
|
+
# Checkout, fetch and pull
|
76
|
+
puts "> Updating the repository"
|
77
|
+
|
78
|
+
last_commits = []
|
79
|
+
execute_in_repo_folder(project[:REPO_URL]) {
|
80
|
+
#Fetch the new commits
|
81
|
+
fetch_result = gitUtils.fetch_repo
|
82
|
+
puts l.log_result fetch_result, "Repository fetch"
|
83
|
+
|
84
|
+
# Checkout the target branch
|
85
|
+
checkout_result = gitUtils.checkout_repo project[:TARGET_BRANCH]
|
86
|
+
puts l.log_result checkout_result, "Repository checkout"
|
87
|
+
|
88
|
+
# Pull the repository
|
89
|
+
pull_result = gitUtils.pull_repo
|
90
|
+
puts l.log_result pull_result, "Repository pull"
|
91
|
+
|
92
|
+
# Get list of the five most recent commits
|
93
|
+
puts "> Getting list of commits"
|
94
|
+
last_commits = gitUtils.get_commit_lists
|
95
|
+
puts l.log_result true, "Found #{last_commits.length} commit(s)"
|
96
|
+
}
|
97
|
+
|
98
|
+
|
99
|
+
# Iterate on each commit to build
|
100
|
+
last_commits.each do |commit_line|
|
101
|
+
commit, email = commit_line.split(' ')
|
102
|
+
|
103
|
+
puts "> Commit #{commit}"
|
104
|
+
unless database.should_build_commit( project[:APP_NAME], commit)
|
105
|
+
puts l.log_result false, "Commit #{commit} skipped"
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
# Checkout del repository al commit specifico
|
110
|
+
puts "> Rollaback repo to #{commit}"
|
111
|
+
execute_in_repo_folder(project[:REPO_URL]) {
|
112
|
+
rollback_result = gitUtils.rollback_repo commit
|
113
|
+
puts l.log_result rollback_result, "Rollback"
|
114
|
+
}
|
115
|
+
|
116
|
+
|
117
|
+
# Run "Pod install" on the repository
|
118
|
+
puts "> Updating dependencies"
|
119
|
+
execute_in_workspace_folder(project[:REPO_URL], project[:WORKSPACE] ) {
|
120
|
+
pod_result = buildUtils.install_pod
|
121
|
+
puts l.log_result pod_result, "Pod installed"
|
122
|
+
}
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
# BUILD
|
128
|
+
puts "> Build"
|
129
|
+
build_result = false
|
130
|
+
execute_in_workspace_folder(project[:REPO_URL], project[:WORKSPACE] ) {
|
131
|
+
build_result = buildUtils.run_build(project[:REPO_URL], project[:WORKSPACE], project[:SCHEME])
|
132
|
+
puts l.log_result build_result, "Build"
|
133
|
+
}
|
134
|
+
|
135
|
+
|
136
|
+
unless build_result
|
137
|
+
test_result = false
|
138
|
+
puts l.log_result false, "Unit tests skipped"
|
139
|
+
|
140
|
+
ipa_result = false
|
141
|
+
puts l.log_result false, "IPA build skipped"
|
142
|
+
|
143
|
+
dsym_result = false
|
144
|
+
puts l.log_result false, "dSym copy skipped"
|
145
|
+
|
146
|
+
else
|
147
|
+
# Create the folder for storing the ipa files and the dSym file.
|
148
|
+
output_folder = File.join(root_output_folder , project[:APP_NAME], commit)
|
149
|
+
FileUtils.mkdir_p(output_folder) unless File.exists?(output_folder)
|
150
|
+
|
151
|
+
# Run unit test
|
152
|
+
puts "> Test"
|
153
|
+
execute_in_workspace_folder(project[:REPO_URL], project[:WORKSPACE] ) {
|
154
|
+
test_result = buildUtils.run_test(project[:REPO_URL], project[:WORKSPACE], project[:SCHEME])
|
155
|
+
puts l.log_result test_result, "Unit Test"
|
156
|
+
}
|
157
|
+
|
158
|
+
# Create ipa file
|
159
|
+
puts "> Archiving"
|
160
|
+
execute_in_workspace_folder(project[:REPO_URL], project[:WORKSPACE] ) {
|
161
|
+
ipa_result = archiveUtils.archive_ipa(project[:WORKSPACE], project[:SCHEME], "#{output_folder}/app.ipa")
|
162
|
+
puts l.log_result ipa_result, "Create Ipa"
|
163
|
+
}
|
164
|
+
|
165
|
+
|
166
|
+
# Copy dSym frile from the "derivedata" folder.
|
167
|
+
execute_in_workspace_folder(project[:REPO_URL], project[:WORKSPACE] ) {
|
168
|
+
dsym_result = archiveUtils.save_dsym(project[:WORKSPACE], project[:SCHEME], "#{output_folder}/")
|
169
|
+
puts l.log_result dsym_result, "Copy dSym"
|
170
|
+
}
|
171
|
+
|
172
|
+
|
173
|
+
# Extract info from the ipa file
|
174
|
+
ipa_file = IpaReader::IpaFile.new("#{output_folder}/app.ipa")
|
175
|
+
bundle_identifier = ipa_file.bundle_identifier
|
176
|
+
bundle_version = ipa_file.version
|
177
|
+
|
178
|
+
# Copy the manifest
|
179
|
+
manifest_template_path = File.join(Xcodeci::TEMPLATE, 'manifest.plist')
|
180
|
+
manifest_template = File.open(manifest_template_path, 'rb') { |f| f.read }
|
181
|
+
link = URI::encode("https://dl.dropboxusercontent.com/u/#{@@dropbox_user_id}/xcodeci/#{project[:APP_NAME]}/#{commit}/app.ipa")
|
182
|
+
manifest_template.gsub!('__IPA_URL_PLACEHOLDER__', link)
|
183
|
+
manifest_template.gsub!('__BUNDLE_IDENTIFIER_PLACEHOLDER__', bundle_identifier)
|
184
|
+
manifest_template.gsub!('__VERSION_PLACEHOLDER__', bundle_version)
|
185
|
+
manifest_template.gsub!("__APP_NAME_PLACEHOLDER__", project[:APP_NAME])
|
186
|
+
|
187
|
+
|
188
|
+
output = File.join(root_output_folder, project[:APP_NAME],commit,'manifest.plist')
|
189
|
+
File.open(output, 'w') { |file| file.write(manifest_template) }
|
190
|
+
|
191
|
+
end
|
192
|
+
commit_report = {
|
193
|
+
build: build_result,
|
194
|
+
test: test_result,
|
195
|
+
ipa: ipa_result,
|
196
|
+
dSym: dsym_result,
|
197
|
+
author: email,
|
198
|
+
date: Time.now
|
199
|
+
}
|
200
|
+
|
201
|
+
database.store_result project[:APP_NAME], commit, commit_report
|
202
|
+
|
203
|
+
end # <- end loop commit
|
204
|
+
puts "=== End #{project[:APP_NAME]} ==="
|
205
|
+
end # <- End single project
|
206
|
+
puts "=== End Loop ==="
|
207
|
+
if (File.exists?(File.join(Xcodeci::HOME, "data.yaml")))
|
208
|
+
# Create html report
|
209
|
+
reporter = HtmlReporter.new File.join(Xcodeci::HOME, "data.yaml") , @@dropbox_user_id
|
210
|
+
output = File.join(root_output_folder, 'index.html')
|
211
|
+
File.open(output, 'w') { |file| file.write(reporter.html_report) }
|
212
|
+
puts "You report it's available at: https://dl.dropboxusercontent.com/u/#{@@dropbox_user_id}/xcodeci/index.html"
|
213
|
+
%x(open -a Safari #{output})
|
214
|
+
end
|
215
|
+
|
216
|
+
end # <- end run method
|
217
|
+
end # <- end class
|
218
|
+
end # <- end module
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class Configuration
|
3
|
+
def initialize path
|
4
|
+
@database = YAML.load_file(path)
|
5
|
+
end
|
6
|
+
|
7
|
+
def each_project
|
8
|
+
@database.each {|key, value|
|
9
|
+
yield(value) unless key == "App_Config"
|
10
|
+
}
|
11
|
+
end
|
12
|
+
def is_ok?
|
13
|
+
not @database.nil? and not @database.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def app_config
|
17
|
+
@database['App_Config']
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Database
|
2
|
+
require "yaml/store"
|
3
|
+
|
4
|
+
def initialize (file_path = "data.yml")
|
5
|
+
@store = YAML::Store.new(file_path)
|
6
|
+
end
|
7
|
+
|
8
|
+
def should_build_commit appname, commit_hash
|
9
|
+
report = @store.transaction {
|
10
|
+
commits = @store[appname]
|
11
|
+
if commits.nil?
|
12
|
+
nil
|
13
|
+
else
|
14
|
+
@store[appname][commit_hash]
|
15
|
+
end
|
16
|
+
}
|
17
|
+
report.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def store_result appname, commit_hash, result
|
21
|
+
@store.transaction do
|
22
|
+
@store[appname] ||= Hash.new
|
23
|
+
@store[appname][commit_hash] = result
|
24
|
+
@store.commit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class GitUtils
|
3
|
+
|
4
|
+
def repository_name repo_url
|
5
|
+
repo_url[repo_url.rindex('/') + 1..-1].sub('.git', '')
|
6
|
+
end
|
7
|
+
# def execute_in_repo_folder (repository_url, &block)
|
8
|
+
# Dir.chdir "#{Xcodeci::HOME}/#{repository_name repository_url}"
|
9
|
+
# result = block.call
|
10
|
+
# Dir.chdir "#{Xcodeci::HOME}"
|
11
|
+
# result
|
12
|
+
# end
|
13
|
+
|
14
|
+
def cloning_repo repository_url
|
15
|
+
#Cloning the repository, it fails if the repository is already cloned
|
16
|
+
dest_folder = File.join(Xcodeci::HOME, (repository_name repository_url))
|
17
|
+
clone_result = %x(git clone #{repository_url} #{dest_folder} 2>&1)
|
18
|
+
$?.exitstatus.zero?
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_repo
|
22
|
+
%x(git fetch 2>&1)
|
23
|
+
$?.exitstatus.zero?
|
24
|
+
end
|
25
|
+
|
26
|
+
def checkout_repo branch
|
27
|
+
%x(git checkout #{branch} 2>&1)
|
28
|
+
$?.exitstatus.zero?
|
29
|
+
end
|
30
|
+
|
31
|
+
def pull_repo
|
32
|
+
%x(git pull 2>&1)
|
33
|
+
$?.exitstatus.zero?
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_commit_lists
|
37
|
+
last_commit = %x(git log -10 --date-order --pretty=format:"%h %ce").split(/\n/)
|
38
|
+
# a single line shoud be in this format "e2d4a86 ignaziocgmail.com"
|
39
|
+
last_commit
|
40
|
+
end
|
41
|
+
|
42
|
+
def rollback_repo commit
|
43
|
+
%x(git checkout "#{commit}" 2>&1)
|
44
|
+
$?.exitstatus.zero?
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Xcodeci
|
2
|
+
class HtmlReporter
|
3
|
+
def initialize db_path, drobox_userid
|
4
|
+
@db_path = db_path
|
5
|
+
@user_id = drobox_userid
|
6
|
+
end
|
7
|
+
|
8
|
+
def table_cell_for_boolean value
|
9
|
+
klass = value ? 'positive' : 'negative'
|
10
|
+
text = value ? "✔︎" : "✘"
|
11
|
+
"<td class=\"#{klass}\";>#{text}</td>"
|
12
|
+
end
|
13
|
+
|
14
|
+
def manifest_link appname, commit
|
15
|
+
"https://dl.dropboxusercontent.com/u/#{@user_id}/xcodeci/#{appname}/#{commit}/manifest.plist"
|
16
|
+
end
|
17
|
+
def ipa_link appname, commit
|
18
|
+
"https://dl.dropboxusercontent.com/u/#{@user_id}/xcodeci/#{appname}/#{commit}/app.ipa"
|
19
|
+
end
|
20
|
+
def itunes_link appname, commit
|
21
|
+
"itms-services://?action=download-manifest&url=#{manifest_link appname, commit}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def html_report
|
25
|
+
database = YAML.load_file(@db_path)
|
26
|
+
html_row = ""
|
27
|
+
|
28
|
+
database.each do |project, commits|
|
29
|
+
html_row << %[
|
30
|
+
<h1>#{project}</h1>
|
31
|
+
<table id="newspaper-a">
|
32
|
+
<thead>
|
33
|
+
<tr>
|
34
|
+
<th scope="col">Date</th>
|
35
|
+
<th scope="col">Commit</th>
|
36
|
+
<th scope="col">Email</th>
|
37
|
+
<th scope="col">Build</th>
|
38
|
+
<th scope="col">Test</th>
|
39
|
+
<th scope="col">IPA</th>
|
40
|
+
<th scope="col">dSym</th>
|
41
|
+
<th scople="col">Manifest</th>
|
42
|
+
<th scople="col">IPA</th>
|
43
|
+
<th scople="col">Install</th>
|
44
|
+
</tr>
|
45
|
+
</thead>
|
46
|
+
<tbody>
|
47
|
+
]
|
48
|
+
sorted_commits = commits.keys.sort {|c1, c2|
|
49
|
+
commits[c1][:date] <=> commits[c2][:date]
|
50
|
+
}
|
51
|
+
sorted_commits.each do | commit |
|
52
|
+
report = commits[commit]
|
53
|
+
html_row << "<tr><td width=\"50px\">#{report[:date].strftime('%D')}</td>" \
|
54
|
+
"<td width=\"50px\">#{commit}</td>" \
|
55
|
+
"<td>#{report[:author]}</td>" \
|
56
|
+
"#{table_cell_for_boolean report[:build]}" \
|
57
|
+
"#{table_cell_for_boolean report[:test]}" \
|
58
|
+
"#{table_cell_for_boolean report[:ipa]}" \
|
59
|
+
"#{table_cell_for_boolean report[:dSym]}" \
|
60
|
+
"<td><a href=\"#{URI::encode(manifest_link project, commit)}\">Manifest</a></td>" \
|
61
|
+
"<td><a href=\"#{URI::encode(ipa_link project, commit)}\">IPA</a></td>" \
|
62
|
+
"<td><a href=\"#{URI::encode(itunes_link project, commit)}\">Install</a></td>" \
|
63
|
+
"</tr>"
|
64
|
+
end #end loop commits
|
65
|
+
html_row << "
|
66
|
+
</tbody>
|
67
|
+
</table>
|
68
|
+
"
|
69
|
+
end # <- End loo projects
|
70
|
+
|
71
|
+
html_template_path = File.join(Xcodeci::TEMPLATE, 'index.html')
|
72
|
+
html_template = File.open(html_template_path, 'rb') { |f| f.read }
|
73
|
+
html_template.gsub!('<!-- __TABLE_PLACEHOLDER__ -->', html_row)
|
74
|
+
html_template
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
data/readme.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
xcodeci
|
2
|
+
=======
|
3
|
+
|
4
|
+
Ruby script for build/test/archive/distribuite iOS application.
|
5
|
+
|
6
|
+
This script can be uses to create a simple continuous integration service for xcode.
|
7
|
+
|
8
|
+
The project is still in development, **any contribution will be appreciate.**
|
9
|
+
|
10
|
+
Basic info
|
11
|
+
==========
|
12
|
+
|
13
|
+
The script runs in a loop and every five minutes it:
|
14
|
+
|
15
|
+
1. Reads the configuration file on `~/.xcodeci/xcodeci.conf.yaml` and iterates on each listed project
|
16
|
+
1. Clones the repository inside the folder `~/.xcodeci/[app_name]`
|
17
|
+
2. Executes `git fetch`, `git pull` and `git checkout` on the specified branch
|
18
|
+
3. For each commit on that branch it:
|
19
|
+
1. Builds the application (only if it's necessarely, otherwise skip to the next commit)
|
20
|
+
2. Runs the unit test
|
21
|
+
3. Save the dSym file on the output folder[^1]
|
22
|
+
4. Generates the ipa file and the manifest.plist on the output folder
|
23
|
+
2. Generates a web page for the OTA distribution
|
24
|
+
|
25
|
+
[^1]: The current output folder is the dropbox folder specified on the configuration file.
|
26
|
+
|
27
|
+
How it's looks like
|
28
|
+
===================
|
29
|
+
This is the output on console
|
30
|
+
|
31
|
+

|
32
|
+
|
33
|
+
And this is the html report
|
34
|
+

|
35
|
+
|
36
|
+
Basic usage
|
37
|
+
===========
|
38
|
+
|
39
|
+
1. Clone the script:
|
40
|
+
|
41
|
+
`git clone https://github.com/ignazioc/xcodeci`
|
42
|
+
|
43
|
+
2. Install the required gems
|
44
|
+
|
45
|
+
`bundle install
|
46
|
+
`
|
47
|
+
3. Install **xctool**
|
48
|
+
|
49
|
+
`brew install xctool`
|
50
|
+
|
51
|
+
for more info about brew command see the [brew website](http://brew.sh)
|
52
|
+
|
53
|
+
4. Run the script
|
54
|
+
|
55
|
+
`./bin/xcodeci`
|
56
|
+
|
57
|
+
The first time you will run the script a sample configuration file will be created on `~/.xcodeci/xcodeci.conf.yaml`
|
58
|
+
|
59
|
+
5. Edit your configuration file (see info below)
|
60
|
+
|
61
|
+
6. Run the script again and wait :)
|
62
|
+
|
63
|
+
|
64
|
+
The configuration file
|
65
|
+
======================
|
66
|
+
|
67
|
+
This is an example of the configuration file
|
68
|
+
|
69
|
+
---
|
70
|
+
App_Config:
|
71
|
+
#The public dropbox folder on your machine
|
72
|
+
:DROPBOX_FOLDER: '/Users/username/Dropbox/Public/'
|
73
|
+
|
74
|
+
#This is your dropbox userid, you can find it when you share a file stored on the
|
75
|
+
#public folder like this one https://dl.dropboxusercontent.com/u/792862/avatar_grumpy.png
|
76
|
+
#In this example the user id is 792862
|
77
|
+
#The user id is used to crete the links on the html report for install the application.
|
78
|
+
|
79
|
+
:DROPBOX_USER_ID: '792862'
|
80
|
+
SampleApp1:
|
81
|
+
:REPO_URL: 'git@github.com:ignazioc/sample_repo_1.git'
|
82
|
+
:APP_NAME: 'SampleApp-1'
|
83
|
+
:TARGET_BRANCH: 'master'
|
84
|
+
:WORKSPACE: 'SampleApp 1.xcworkspace'
|
85
|
+
:SCHEME: 'SampleApp 1'
|
86
|
+
SampleApp2:
|
87
|
+
:REPO_URL: '.....'
|
88
|
+
:APP_NAME: '.....'
|
89
|
+
:TARGET_BRANCH: '.....'
|
90
|
+
:WORKSPACE: '.....'
|
91
|
+
:SCHEME: '.....'
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
5
|
+
<title>Report</title>
|
6
|
+
<style type="text/css">
|
7
|
+
body
|
8
|
+
{
|
9
|
+
line-height: 1.6em;
|
10
|
+
}
|
11
|
+
|
12
|
+
#newspaper-a
|
13
|
+
{
|
14
|
+
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
|
15
|
+
font-size: 14px;
|
16
|
+
margin: 0px;
|
17
|
+
width: 100%;
|
18
|
+
text-align: left;
|
19
|
+
border-collapse: collapse;
|
20
|
+
border: 1px solid #69c;
|
21
|
+
}
|
22
|
+
#newspaper-a th
|
23
|
+
{
|
24
|
+
padding: 12px 17px 12px 17px;
|
25
|
+
font-weight: normal;
|
26
|
+
font-size: 14px;
|
27
|
+
color: #039;
|
28
|
+
border-bottom: 1px dashed #69c;
|
29
|
+
}
|
30
|
+
#newspaper-a td
|
31
|
+
{
|
32
|
+
padding: 7px 17px 7px 17px;
|
33
|
+
font-weight: bold;
|
34
|
+
/*color: #fff;*/
|
35
|
+
}
|
36
|
+
#newspaper-a td.positive {
|
37
|
+
color: #519548;
|
38
|
+
font-size: 18px;
|
39
|
+
}
|
40
|
+
#newspaper-a td.negative {
|
41
|
+
color: #DC0139;
|
42
|
+
font-size: 18px;
|
43
|
+
}
|
44
|
+
#newspaper-a tbody tr:hover td
|
45
|
+
{
|
46
|
+
/*color: #339;*/
|
47
|
+
/*background: #d0dafd;*/
|
48
|
+
}
|
49
|
+
</style>
|
50
|
+
</head>
|
51
|
+
<body>
|
52
|
+
<!-- __TABLE_PLACEHOLDER__ -->
|
53
|
+
</body>
|
54
|
+
</html>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
3
|
+
<plist version="1.0">
|
4
|
+
<dict>
|
5
|
+
<key>items</key>
|
6
|
+
<array>
|
7
|
+
<dict>
|
8
|
+
<key>assets</key>
|
9
|
+
<array>
|
10
|
+
<dict>
|
11
|
+
<key>kind</key>
|
12
|
+
<string>software-package</string>
|
13
|
+
<key>url</key>
|
14
|
+
<string>__IPA_URL_PLACEHOLDER__</string>
|
15
|
+
</dict>
|
16
|
+
</array>
|
17
|
+
<key>metadata</key>
|
18
|
+
<dict>
|
19
|
+
<key>bundle-identifier</key>
|
20
|
+
<string>__BUNDLE_IDENTIFIER_PLACEHOLDER__</string>
|
21
|
+
<key>bundle-version</key>
|
22
|
+
<string>__VERSION_PLACEHOLDER__</string>
|
23
|
+
<key>kind</key>
|
24
|
+
<string>software</string>
|
25
|
+
<key>title</key>
|
26
|
+
<string>__APP_NAME_PLACEHOLDER__</string>
|
27
|
+
</dict>
|
28
|
+
</dict>
|
29
|
+
</array>
|
30
|
+
</dict>
|
31
|
+
</plist>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
---
|
2
|
+
App_Config:
|
3
|
+
#The public dropbox folder on your machine
|
4
|
+
:DROPBOX_FOLDER: '/Users/username/Dropbox/Public/'
|
5
|
+
|
6
|
+
#You can find it when you share a file stored on the
|
7
|
+
#public folder like this one https://dl.dropboxusercontent.com/u/792862/avatar_grumpy.png
|
8
|
+
#In this example the user id is 792862
|
9
|
+
#The user id is used to crete the links on the html report for install the application.
|
10
|
+
:DROPBOX_USER_ID: '792862'
|
11
|
+
|
12
|
+
|
13
|
+
# SampleApp1: # <- This is just a mnemonic name for the project, don't use spaces.
|
14
|
+
# :REPO_URL: 'git@github.com:ignazioc/sample_repo_1.git' # <- This it the git repository
|
15
|
+
# :APP_NAME: 'SampleApp-1' # <- This is used as a name for the output forlder, don't use spaces
|
16
|
+
# :TARGET_BRANCH: 'master' # <- The branch that you want to build
|
17
|
+
# :WORKSPACE: 'SampleApp 1.xcworkspace' # <- The name of the workspace
|
18
|
+
# :SCHEME: 'SampleApp 1' # <- the name of the scheme that you want to build / test
|
19
|
+
# SampleApp2: # <- Add as many projects as you want
|
20
|
+
# :REPO_URL: 'git@github.com:ignazioc/sample_repo_2.git'
|
21
|
+
# :APP_NAME: 'SampleApp-2'
|
22
|
+
# :TARGET_BRANCH: 'master'
|
23
|
+
# :WORKSPACE: 'SampleApp 1.xcworkspace'
|
24
|
+
# :SCHEME: 'SampleApp 1'
|
data/xcodeci.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'xcodeci/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "XcodeCI"
|
8
|
+
spec.version = Xcodeci::VERSION
|
9
|
+
spec.authors = ["Ignazio Calò"]
|
10
|
+
spec.email = ["ignazioc@gmail.com"]
|
11
|
+
spec.summary = %q{This gem it's only a test.}
|
12
|
+
spec.description = %q{This gem it's only a test, please don't use it.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_runtime_dependency "colorize"
|
25
|
+
spec.add_runtime_dependency "cocoapods"
|
26
|
+
spec.add_runtime_dependency "ipa_reader"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: XcodeCI
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ignazio Calò
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: colorize
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: cocoapods
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: ipa_reader
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: This gem it's only a test, please don't use it.
|
98
|
+
email:
|
99
|
+
- ignazioc@gmail.com
|
100
|
+
executables:
|
101
|
+
- xcodeci
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- .gitignore
|
106
|
+
- Gemfile
|
107
|
+
- LICENSE.txt
|
108
|
+
- bin/xcodeci
|
109
|
+
- lib/xcodeci.rb
|
110
|
+
- lib/xcodeci/archiveutils.rb
|
111
|
+
- lib/xcodeci/buildutils.rb
|
112
|
+
- lib/xcodeci/command.rb
|
113
|
+
- lib/xcodeci/configuration.rb
|
114
|
+
- lib/xcodeci/database.rb
|
115
|
+
- lib/xcodeci/gitutils.rb
|
116
|
+
- lib/xcodeci/html_reporter.rb
|
117
|
+
- lib/xcodeci/logger.rb
|
118
|
+
- lib/xcodeci/version.rb
|
119
|
+
- readme.md
|
120
|
+
- templates/index.html
|
121
|
+
- templates/manifest.plist
|
122
|
+
- templates/xcodeci.conf.yaml
|
123
|
+
- xcodeci.gemspec
|
124
|
+
homepage: ''
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
metadata: {}
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.0.14
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: This gem it's only a test.
|
148
|
+
test_files: []
|