github-xcode-bot-builder 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 59137bad110f52349a68bd7159f7bf012bf64524
4
+ data.tar.gz: 70ac9923f4199f55fa8e1369048a4d3cc5a27c33
5
+ SHA512:
6
+ metadata.gz: e00b616e8f1bb4b6faf15604b190a335c5f7f5c2a8b25e47d46bc14e377c838e1ba779f070f3a8989783bf670ab67b2afd6f9261613e1f63e94b7c7712bbfd21
7
+ data.tar.gz: ae108e20b23815b6c2d3c922d4d497cdca8d7868aea4b469909f2d8ae8a46f9556d55f9ba169b86d0b6f74a8c2e02737cbef9d122439f33e44ad76cf7b586fcb
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "octokit", "~> 2.0"
4
+ gem "parseconfig", "~> 1.0.2"
5
+
6
+ group :development do
7
+ gem "rspec", "~> 2.8.0"
8
+ gem "rdoc", "~> 3.12"
9
+ gem "bundler", "~> 1.0"
10
+ gem "jeweler", "~> 1.8.7"
11
+ #gem "rcov", ">= 0"
12
+ end
@@ -0,0 +1,71 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ addressable (2.3.5)
5
+ builder (3.2.2)
6
+ diff-lcs (1.1.3)
7
+ faraday (0.8.8)
8
+ multipart-post (~> 1.2.0)
9
+ git (1.2.6)
10
+ github_api (0.10.1)
11
+ addressable
12
+ faraday (~> 0.8.1)
13
+ hashie (>= 1.2)
14
+ multi_json (~> 1.4)
15
+ nokogiri (~> 1.5.2)
16
+ oauth2
17
+ hashie (2.0.5)
18
+ highline (1.6.20)
19
+ httpauth (0.2.0)
20
+ jeweler (1.8.8)
21
+ builder
22
+ bundler (~> 1.0)
23
+ git (>= 1.2.5)
24
+ github_api (= 0.10.1)
25
+ highline (>= 1.6.15)
26
+ nokogiri (= 1.5.10)
27
+ rake
28
+ rdoc
29
+ json (1.8.1)
30
+ jwt (0.1.8)
31
+ multi_json (>= 1.5)
32
+ multi_json (1.8.2)
33
+ multi_xml (0.5.5)
34
+ multipart-post (1.2.0)
35
+ nokogiri (1.5.10)
36
+ oauth2 (0.9.2)
37
+ faraday (~> 0.8)
38
+ httpauth (~> 0.2)
39
+ jwt (~> 0.1.4)
40
+ multi_json (~> 1.0)
41
+ multi_xml (~> 0.5)
42
+ rack (~> 1.2)
43
+ octokit (2.5.0)
44
+ sawyer (~> 0.5.1)
45
+ parseconfig (1.0.2)
46
+ rack (1.5.2)
47
+ rake (10.1.0)
48
+ rdoc (3.12.2)
49
+ json (~> 1.4)
50
+ rspec (2.8.0)
51
+ rspec-core (~> 2.8.0)
52
+ rspec-expectations (~> 2.8.0)
53
+ rspec-mocks (~> 2.8.0)
54
+ rspec-core (2.8.0)
55
+ rspec-expectations (2.8.0)
56
+ diff-lcs (~> 1.1.2)
57
+ rspec-mocks (2.8.0)
58
+ sawyer (0.5.1)
59
+ addressable (~> 2.3.5)
60
+ faraday (~> 0.8, < 0.10)
61
+
62
+ PLATFORMS
63
+ ruby
64
+
65
+ DEPENDENCIES
66
+ bundler (~> 1.0)
67
+ jeweler (~> 1.8.7)
68
+ octokit (~> 2.0)
69
+ parseconfig (~> 1.0.2)
70
+ rdoc (~> 3.12)
71
+ rspec (~> 2.8.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 ModCloth, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Github Xcode Bot Builder
2
+ ========================
3
+
4
+ A command line tool that creates/manages/deletes Xcode 5 server bots for each Github pull request. When a pull request is opened
5
+ a corresponding Xcode bot is created. When a new commit is pushed the bot is re-run. When the build finishes the github
6
+ pull request status is updated with a comment if there's an error. Users can request that a pull request be retested by
7
+ adding a comment that includes the word "retest" (case insensitive). When a pull request is closed the corresponding
8
+ bot is deleted.
9
+
10
+ Setup
11
+ =====
12
+ Make sure your Xcode server is correctly setup to allow ANYONE to create a build (without a username or password, see suggested features below).
13
+ Then make sure you can manually create and execute a build and run it.
14
+
15
+ Create a ~/.bot-sync-github.cfg
16
+
17
+ Go to your [Github Account Settings](https://github.com/settings/applications) and create a personal access token which
18
+ you will use as your *github_access_token* so that the **bot-sync-github** script can access your github repo
19
+
20
+ ```
21
+ github_access_token = 57244a72a7ca33931a40eb4ec21621505ab9f6b3
22
+ github_url = https://github.com/someuser/Some-Repo.git
23
+ github_repo = someuser/Some-Repo
24
+ xcode_server = 192.168.10.123
25
+ xcode_devices = iphonesimulator iPhone Retina (4-inch) 7.0|iphonesimulator iPhone Retina (4-inch) 6.1
26
+ xcode_scheme = Some-Scheme-Name-app
27
+ xcode_project_or_workspace = SomeProject.xcworkspace # or SomeProject.xcproject
28
+ ```
29
+
30
+ Note that *xcode_devices* need to be pipe delimited. To get the list of available devices run the bot-devices command.
31
+ The *xcode_server* can either be an ip address or a hostname.
32
+
33
+ Manually run **bot-sync-github** from the command line to make sure it works
34
+
35
+ Schedule **bot-sync-github** to run in cron every couple of minutes. For example if you're using RVM:
36
+
37
+ ```
38
+ */2 * * * * $HOME/.rvm/bin/ruby-2.0.0-p247 $HOME/.rvm/gems/ruby-2.0.0-p247/bin/bot-sync-github >> /tmp/bot-sync-github.log 2>&1
39
+ ```
40
+
41
+ Troubleshooting
42
+ ===============
43
+ Send us a pull request with your troubleshooting tips here!
44
+
45
+ Contributing
46
+ ============
47
+
48
+ * Github Xcode Bot Builder uses [Jeweler](https://github.com/technicalpickles/jeweler) for managing the Gem, versioning,
49
+ generating the Gemspec, etc. so do not manually edit the gemspec since it is auto generated from the Rakefile.
50
+ * Check out the latest **master** to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
51
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
52
+ * Fork the project.
53
+ * Start a feature/bugfix branch.
54
+ * Commit and push until you are happy with your contribution.
55
+ * Don't forget to add yourself to the contributors section below
56
+
57
+ Suggested features to contribute
58
+ ================================
59
+ * Support for configuring username and password to use with your Xcode server
60
+ * Add specs that use VCR to help us add test coverage
61
+ * Add support for multiple repositories
62
+ * Add better error handling
63
+ * Update this README.md to make it easier for new users to get started and troubleshoot
64
+
65
+ Contributors
66
+ ============
67
+ - [ModCloth](http://www.modcloth.com/)
68
+ - [Geoffery Nix](http://github.com/geoffnix)
69
+ - [Two Bit Labs](http://twobitlabs.com/)
70
+ - [Todd Huss](http://github.com/thuss)
71
+
72
+ Copyright
73
+ =========
74
+
75
+ Copyright (c) 2013 ModCloth. See LICENSE for further details.
76
+
77
+
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "github-xcode-bot-builder"
18
+ gem.homepage = "http://github.com/ModCloth/github-xcode-bot-builder"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Create Xcode bots to run when github pull requests are created or updated}
21
+ gem.description = %Q{A command line tool that can be run via cron that configures and manages Xcode server bots for each pull request}
22
+ gem.email = ""
23
+ gem.authors = ["ModCloth", "Two Bit Labs", "Geoffery Nix", "Todd Huss"]
24
+ gem.executables = ['bot-sync-github', 'bot-devices', 'bot-status', 'bot-delete']
25
+ # dependencies defined in Gemfile
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'rdoc/task'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "github-xcode-bot-builder #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2013-10-23.
4
+ # Copyright (c) 2013. All rights reserved.
5
+
6
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
7
+ require "bot_cli"
8
+
9
+ BotCli.new.delete(ARGV)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2013-10-23.
4
+ # Copyright (c) 2013. All rights reserved.
5
+
6
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
7
+ require "bot_cli"
8
+
9
+ BotCli.new.devices(ARGV)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2013-10-23.
4
+ # Copyright (c) 2013. All rights reserved.
5
+
6
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
7
+ require "bot_cli"
8
+
9
+ BotCli.new.status(ARGV)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2013-10-23.
4
+ # Copyright (c) 2013. All rights reserved.
5
+
6
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
7
+ require "bot_cli"
8
+
9
+ BotCli.new.sync_github(ARGV)
@@ -0,0 +1,215 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'cgi/cookie'
4
+ require 'SecureRandom'
5
+ require 'json'
6
+ require 'pp'
7
+ require 'bot_config'
8
+ require 'singleton'
9
+ require 'ostruct'
10
+
11
+ class BotBuilder
12
+ include Singleton
13
+
14
+ def delete_bot(guid)
15
+ success = false
16
+ service_requests = [ service_request('deleteBotWithGUID:', [guid]) ]
17
+ delete_info = batch_service_request(service_requests)
18
+ if (delete_info['responses'][0]['responseStatus'] == 'succeeded')
19
+ puts "BOT Deleted #{guid}"
20
+ success = true
21
+ else
22
+ puts "Error deleting BOT #{guid}"
23
+ end
24
+ end
25
+
26
+ def create_bot(short_name, long_name, branch, scm_url, project_path, scheme_name, devices = [])
27
+ device_guids = find_guids_for_devices(devices)
28
+ if (device_guids.count != devices.count)
29
+ puts "Some of the following devices could not be found on the server: #{devices}"
30
+ exit 1
31
+ end
32
+
33
+ scm_guid = find_guid_for_scm_url(scm_url)
34
+ if (scm_guid.nil? || scm_guid.empty?)
35
+ puts "Could not find repository on the server #{scm_url}"
36
+ exit 1
37
+ end
38
+
39
+ # Create the bot
40
+ buildSchemeKey = (project_path =~ /xcworkspace/) ? :buildWorkspacePath : :buildProjectPath
41
+
42
+ service_requests = [
43
+ service_request('createBotWithProperties:', [
44
+ {
45
+ shortName: short_name,
46
+ longName: long_name,
47
+ extendedAttributes: {
48
+ scmInfo: {
49
+ "/" => {
50
+ scmBranch: branch,
51
+ }
52
+ },
53
+ scmInfoGUIDMap: {
54
+ "/" => scm_guid
55
+ },
56
+ buildSchemeKey => project_path,
57
+ buildSchemeName: scheme_name,
58
+ pollForSCMChanges: false,
59
+ buildOnTrigger: false,
60
+ buildFromClean: true,
61
+ integratePerformsAnalyze: true,
62
+ integratePerformsTest: true,
63
+ integratePerformsArchive: false,
64
+ deviceSpecification: "specificDevices",
65
+ deviceInfo: device_guids
66
+ },
67
+ notifyCommitterOnSuccess: false,
68
+ notifyCommitterOnFailure: false,
69
+ type: "com.apple.entity.Bot"
70
+ }
71
+
72
+
73
+ ])
74
+ ]
75
+ bot_info = batch_service_request(service_requests)
76
+ bot_guid = bot_info['responses'][0]['response']['guid']
77
+ puts "BOT Created #{bot_guid} #{short_name}"
78
+
79
+ # Start the bot
80
+ start_bot bot_guid
81
+
82
+ bot_guid
83
+ end
84
+
85
+ def start_bot(bot_guid)
86
+ service_requests = [ service_request('startBotRunForBotGUID:', [bot_guid]) ]
87
+ bot_start_info = batch_service_request(service_requests)
88
+ puts "BOT Started #{bot_guid}"
89
+ end
90
+
91
+ def status_of_all_bots
92
+ # After immediately creating: latest_run_status "" run_sub_status ""
93
+ # While running: latest_run_status "running" run_sub_status ""
94
+ # After completion: latest_run_status "completed" run_sub_status "build-failed|build-errors|test-failures|warnings|analysis-issues|succeeded"
95
+ service_requests = [ service_request('query:', [
96
+ {
97
+ fields: ['guid','tinyID','latestRunStatus','latestRunSubStatus'],
98
+ entityTypes: ["com.apple.entity.Bot"]
99
+ }
100
+ ], 'SearchService') ]
101
+ status_info = batch_service_request(service_requests)
102
+ results = status_info['responses'][0]['response']['results']
103
+ statuses = {}
104
+ results.each do |result|
105
+ bot = OpenStruct.new result['entity']
106
+ bot.status_url = "http://#{BotConfig.instance.xcode_server_hostname}/xcode/bots/#{bot.tinyID}"
107
+ bot.latest_run_status = (bot.latestRunStatus.nil? || bot.latestRunStatus.empty?) ? :unknown : bot.latestRunStatus.to_sym
108
+ bot.latest_run_sub_status = (bot.latestRunSubStatus.nil? || bot.latestRunSubStatus.empty?) ? :unknown : bot.latestRunSubStatus.to_sym
109
+ bot.short_name = bot.tinyID
110
+ bot.short_name_without_version = bot.short_name.sub(/_v\d*$/, '_v')
111
+ statuses[bot.short_name_without_version] = bot
112
+ end
113
+ statuses
114
+ end
115
+
116
+ def status
117
+ status_of_all_bots.values.each do |bot|
118
+ puts "#{bot.status_url} #{bot.latest_run_status} #{bot.latest_run_sub_status}"
119
+ end
120
+ end
121
+
122
+ def devices
123
+ device_info = get_device_info
124
+ device_info.each do |device|
125
+ puts device_string_for_device(device)
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def find_guid_for_scm_url(scm_url)
132
+ scm_info = get_scm_info
133
+ scm_guid = nil
134
+ scm_info.each do |scm|
135
+ if (scm['scmRepoPath'] == scm_url)
136
+ scm_guid = scm['scmGUID']
137
+ end
138
+ end
139
+ scm_guid
140
+ end
141
+
142
+ def find_guids_for_devices(devices)
143
+ device_info = get_device_info
144
+ device_guids = []
145
+ device_info.each do |device|
146
+ device_string = device_string_for_device device
147
+ if (devices.include? device_string)
148
+ device_guids << device['guid']
149
+ end
150
+ end
151
+ device_guids
152
+ end
153
+
154
+ def device_string_for_device(device)
155
+ "#{device['adcDevicePlatform']} #{device['adcDeviceName']} #{device['adcDeviceSoftwareVersion']}"
156
+ end
157
+
158
+ def get_device_info
159
+ # Put to get device and Device Info
160
+ service_requests = [
161
+ service_request('allDevices', [])
162
+ ]
163
+ device_info = batch_service_request(service_requests)['responses'][0]['response']
164
+ device_info
165
+ end
166
+
167
+ def get_scm_info
168
+ # Put to get device and Device Info
169
+ service_requests = [
170
+ service_request('findAllSCMInfos', [])
171
+ ]
172
+ scm_info = batch_service_request(service_requests)['responses'][0]['response']
173
+ scm_info
174
+ end
175
+
176
+ def get_session_guid
177
+ # Get the guid
178
+ if (@session_guid == nil)
179
+ response = Net::HTTP.get_response(URI.parse("http://#{BotConfig.instance.xcode_server_hostname}/xcode"))
180
+ cookies = CGI::Cookie::parse(response['set-cookie'])
181
+ @session_guid = cookies['cc.collabd_session_guid']
182
+ end
183
+ @session_guid
184
+ end
185
+
186
+ def batch_service_request(service_requests)
187
+ payload = {
188
+ type: 'com.apple.BatchServiceRequest' ,
189
+ requests: service_requests
190
+ }
191
+ http = Net::HTTP.new(BotConfig.instance.xcode_server_hostname)
192
+ request = Net::HTTP::Put.new('/collabdproxy')
193
+ request['Content-Type'] = 'application/json; charset=UTF-8'
194
+ request['Cookie'] = "cc.collabd_session_guid=#{@session_guid}"
195
+ request.body = payload.to_json
196
+ response = http.request(request)
197
+ json = JSON.parse(response.body)
198
+ # response_status = json['responses'][0]['responseStatus']
199
+ # puts "Result status #{response_status}"
200
+ json
201
+ end
202
+
203
+ def service_request(name, arguments, service = 'XCBotService')
204
+ get_session_guid
205
+ {
206
+ type: 'com.apple.ServiceRequest',
207
+ arguments: arguments,
208
+ sessionGUID: @session_guid,
209
+ serviceName: service,
210
+ methodName: name,
211
+ expandReferencedObjects: false
212
+ }
213
+ end
214
+
215
+ end
@@ -0,0 +1,39 @@
1
+ require 'bot_builder'
2
+ require 'bot_github'
3
+
4
+ # Patch net/http to set a reasonable open_timeout to prevent hanging
5
+ module Net
6
+ class HTTP
7
+ alias old_initialize initialize
8
+
9
+ def initialize(*args)
10
+ old_initialize(*args)
11
+ @open_timeout = 60
12
+ end
13
+ end
14
+ end
15
+
16
+ class BotCli
17
+
18
+ def delete(args)
19
+ guid = args[0]
20
+ if (guid == nil || guid.empty?)
21
+ $stderr.puts "Missing guid of bot to delete"
22
+ exit 1
23
+ end
24
+ BotBuilder.instance.delete_bot guid
25
+ end
26
+
27
+ def status(args)
28
+ BotBuilder.instance.status
29
+ end
30
+
31
+ def devices(args)
32
+ BotBuilder.instance.devices
33
+ end
34
+
35
+ def sync_github(args)
36
+ BotGithub.instance.sync
37
+ end
38
+
39
+ end
@@ -0,0 +1,58 @@
1
+ require 'singleton'
2
+ require 'parseconfig'
3
+
4
+ class BotConfig
5
+ include Singleton
6
+
7
+ def initialize
8
+ @filename = File.expand_path('~/.bot-sync-github.cfg')
9
+ if (!File.exists? @filename)
10
+ $stderr.puts "Missing configuration file #{@filename}"
11
+ exit 1
12
+ end
13
+
14
+ @config = ParseConfig.new(@filename)
15
+
16
+ # Make sure every param is configured properly since param will throw an error for a missing key
17
+ [:xcode_server, :github_url, :github_repo, :github_access_token, :xcode_devices, :xcode_scheme, :xcode_project_or_workspace].each do |key|
18
+ param key
19
+ end
20
+ end
21
+
22
+ def xcode_server_hostname
23
+ param :xcode_server
24
+ end
25
+
26
+ def github_access_token
27
+ param :github_access_token
28
+ end
29
+
30
+ def scm_path
31
+ param :github_url
32
+ end
33
+
34
+ def github_repo
35
+ param :github_repo
36
+ end
37
+
38
+ def xcode_devices
39
+ param(:xcode_devices).split('|')
40
+ end
41
+
42
+ def xcode_scheme
43
+ param :xcode_scheme
44
+ end
45
+
46
+ def xcode_project_or_workspace
47
+ param :xcode_project_or_workspace
48
+ end
49
+
50
+ def param(key)
51
+ value = @config[key.to_s]
52
+ if (value.nil?)
53
+ $stderr.puts "Missing configuration key #{key} in #{@filename}"
54
+ exit 1
55
+ end
56
+ value
57
+ end
58
+ end
@@ -0,0 +1,208 @@
1
+ require 'octokit'
2
+ require 'singleton'
3
+ require 'bot_config'
4
+ require 'bot_builder'
5
+ require 'ostruct'
6
+
7
+ class BotGithub
8
+ include Singleton
9
+
10
+ def initialize
11
+ @client = Octokit::Client.new :access_token => BotConfig.instance.github_access_token
12
+ @client.login
13
+ end
14
+
15
+ def sync
16
+ puts "\nStarting Github Xcode Bot Builder #{Time.now}\n-----------------------------------------------------------"
17
+ # Check to see if we're already running and skip this run if so
18
+ running_instances = `ps aux | grep [b]ot-sync-github | grep -v bin/sh`.split("\n")
19
+ if (running_instances.count > 1)
20
+ $stderr.puts "Skipping run since bot-sync-github is already running"
21
+ PP.pp(running_instances, STDERR)
22
+ exit 1
23
+ end
24
+
25
+ bot_statuses = BotBuilder.instance.status_of_all_bots
26
+ bots_processed = []
27
+ pull_requests.each do |pr|
28
+ # Check if a bot exists for this PR
29
+ bot = bot_statuses[pr.bot_short_name_without_version]
30
+ bots_processed << pr.bot_short_name
31
+ if (bot.nil?)
32
+ # Create a new bot
33
+ BotBuilder.instance.create_bot(pr.bot_short_name, pr.bot_long_name, pr.branch,
34
+ BotConfig.instance.scm_path,
35
+ BotConfig.instance.xcode_project_or_workspace,
36
+ BotConfig.instance.xcode_scheme,
37
+ BotConfig.instance.xcode_devices)
38
+ create_status_new_build(pr)
39
+ else
40
+ github_state_cur = latest_github_state(pr).state # :unknown :pending :success :error :failure
41
+ github_state_new = convert_bot_status_to_github_state(bot)
42
+ if (github_state_new == :pending && github_state_cur != github_state_new)
43
+ # User triggered a new build by clicking Integrate on the Xcode server interface
44
+ create_status(pr, github_state_new, convert_bot_status_to_github_description(bot), bot.status_url)
45
+ elsif (github_state_new != :unknown && github_state_cur != github_state_new)
46
+ # Build has passed or failed so update status and comment on the issue
47
+ create_comment_for_bot_status(pr, bot)
48
+ create_status(pr, github_state_new, convert_bot_status_to_github_description(bot), bot.status_url)
49
+ elsif (github_state_cur == :unknown)
50
+ # Unknown state occurs when there's a new commit so trigger a new build
51
+ BotBuilder.instance.start_bot(bot.guid)
52
+ create_status_new_build(pr)
53
+ elsif (user_requested_retest(pr, bot))
54
+ BotBuilder.instance.start_bot(bot.guid)
55
+ else
56
+ puts "PR #{pr.number} (#{github_state_cur}) is up to date for bot #{bot.short_name}"
57
+ end
58
+ end
59
+ end
60
+
61
+ # Delete bots that no longer have open pull requests
62
+ bots_unprocessed = bot_statuses.keys - bots_processed
63
+ bots_unprocessed.each do |bot_short_name|
64
+ bot = bot_statuses[bot_short_name]
65
+ BotBuilder.instance.delete_bot(bot.guid) unless !is_managed_bot(bot)
66
+ end
67
+
68
+ puts "-----------------------------------------------------------\nFinished Github Xcode Bot Builder #{Time.now}\n"
69
+ end
70
+
71
+ private
72
+
73
+ def convert_bot_status_to_github_description(bot)
74
+ bot_run_status = bot.latest_run_status # :unknown :running :completed
75
+ bot_run_sub_status = bot.latest_run_sub_status # :unknown :build-failed :build-errors :test-failures :warnings :analysis-issues :succeeded
76
+ github_description = bot_run_status == :running ? "Build Triggered." : ""
77
+ if (bot_run_status == :completed || bot_run_status == :failed)
78
+ github_description = bot_run_sub_status.to_s.split('-').map(&:capitalize).join(' ') + "."
79
+ end
80
+ github_description
81
+ end
82
+
83
+ def convert_bot_status_to_github_state(bot)
84
+ bot_run_status = bot.latest_run_status # :unknown :running :completed
85
+ bot_run_sub_status = bot.latest_run_sub_status # :unknown :build-failed :build-errors :test-failures :warnings :analysis-issues :succeeded
86
+ github_state = bot_run_status == :running ? :pending : :unknown
87
+ if (bot_run_status == :completed || bot_run_status == :failed)
88
+ github_state = case bot_run_sub_status
89
+ when :"test-failures", :"warnings", :"analysis-issues"
90
+ :failure
91
+ when :"succeeded"
92
+ :success
93
+ else
94
+ :error
95
+ end
96
+ end
97
+ github_state
98
+ end
99
+
100
+ def create_comment_for_bot_status(pr, bot)
101
+ message = "Build " + convert_bot_status_to_github_state(bot).to_s.capitalize + ": " + convert_bot_status_to_github_description(bot)
102
+ message += "\n#{bot.status_url}"
103
+ @client.add_comment(BotConfig.instance.github_repo, pr.number, message)
104
+ puts "PR #{pr.number} added comment \"#{message}\""
105
+ end
106
+
107
+ def create_status_new_build(pr)
108
+ create_status(pr, :pending, "Build Triggered.")
109
+ end
110
+
111
+ def create_status(pr, github_state, description = nil, target_url = nil)
112
+ options = {}
113
+ if (!description.nil?)
114
+ options['description'] = description
115
+ end
116
+ if (!target_url.nil?)
117
+ options['target_url'] = target_url
118
+ end
119
+ @client.create_status(BotConfig.instance.github_repo, pr.sha, github_state.to_s, options)
120
+ puts "PR #{pr.number} status updated to \"#{github_state}\" with description \"#{description}\""
121
+ end
122
+
123
+ def latest_github_state(pr)
124
+ statuses = @client.statuses(BotConfig.instance.github_repo, pr.sha)
125
+ status = OpenStruct.new
126
+ status.state = statuses[0].state.to_sym
127
+ status.updated_at = statuses[0].updated_at
128
+ status
129
+ end
130
+
131
+ def pull_requests
132
+ github_repo = BotConfig.instance.github_repo
133
+ responses = @client.pull_requests(github_repo)
134
+ prs = []
135
+ responses.each do |response|
136
+ pr = OpenStruct.new
137
+ pr.sha = response.head.sha
138
+ pr.branch = response.head.ref
139
+ pr.title = response.title
140
+ pr.state = response.state
141
+ pr.number = response.number
142
+ pr.updated_at = response.updated_at
143
+ pr.bot_short_name = bot_short_name(pr)
144
+ pr.bot_short_name_without_version = bot_short_name_without_version(pr)
145
+ pr.bot_long_name = bot_long_name(pr)
146
+ prs << pr
147
+ end
148
+ prs
149
+ end
150
+
151
+ def user_requested_retest(pr, bot)
152
+ should_retest = false
153
+ github_repo = BotConfig.instance.github_repo
154
+
155
+ # Check for a user retest request comment
156
+ comments = @client.issue_comments(github_repo, pr.number)
157
+ latest_retest_time = Time.at(0)
158
+ found_retest_comment = false
159
+ comments.each do |comment|
160
+ if (comment.body =~ /retest/i)
161
+ latest_retest_time = comment.updated_at
162
+ found_retest_comment = true
163
+ end
164
+ end
165
+
166
+ return should_retest unless found_retest_comment
167
+
168
+ # Get the latest status time
169
+ latest_status_time = latest_github_state(pr)
170
+ if (latest_status_time.nil? || latest_status_time.updated_at.nil?)
171
+ latest_status_time = Time.at(0)
172
+ end
173
+
174
+ if (latest_retest_time > latest_status_time.updated_at)
175
+ should_retest = true
176
+ puts "PR #{pr.number} user requested a retest"
177
+ end
178
+
179
+ should_retest
180
+ end
181
+
182
+ def bot_long_name(pr)
183
+ github_repo = BotConfig.instance.github_repo
184
+ "PR #{pr.number} #{pr.title} #{github_repo}"
185
+ end
186
+
187
+ def bot_short_name(pr)
188
+ short_name = "#{pr.number}-#{pr.branch}".gsub(/[^[:alnum:]]/, '_') + bot_short_name_suffix
189
+ short_name
190
+ end
191
+
192
+ # For duplicate bot names xcode server appends a version
193
+ # bot_short_name_v, bot_short_name_v1, bot_short_name_v2. This method converts bot_short_name_v2 to bot_short_name_v
194
+ def bot_short_name_without_version(pr)
195
+ bot_short_name(pr).sub(/_v\d*$/, '_v')
196
+ end
197
+
198
+ def is_managed_bot(bot)
199
+ # Check the suffix of the bot to see if it matches the bot_short_name_suffix
200
+ bot.short_name =~ /#{bot_short_name_suffix}\d*$/
201
+ end
202
+
203
+ def bot_short_name_suffix
204
+ github_repo = BotConfig.instance.github_repo.downcase
205
+ ('_' + github_repo + '_v').gsub(/[^[:alnum:]]/, '_')
206
+ end
207
+
208
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "GithubXcodeBotBuilder" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'github-xcode-bot-builder'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github-xcode-bot-builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - ModCloth
8
+ - Two Bit Labs
9
+ - Geoffery Nix
10
+ - Todd Huss
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-11-25 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: octokit
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '2.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: parseconfig
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ~>
35
+ - !ruby/object:Gem::Version
36
+ version: 1.0.2
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.0.2
44
+ - !ruby/object:Gem::Dependency
45
+ name: rspec
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ~>
49
+ - !ruby/object:Gem::Version
50
+ version: 2.8.0
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ version: 2.8.0
58
+ - !ruby/object:Gem::Dependency
59
+ name: rdoc
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ~>
63
+ - !ruby/object:Gem::Version
64
+ version: '3.12'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ~>
70
+ - !ruby/object:Gem::Version
71
+ version: '3.12'
72
+ - !ruby/object:Gem::Dependency
73
+ name: bundler
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: '1.0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '1.0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: jeweler
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ~>
91
+ - !ruby/object:Gem::Version
92
+ version: 1.8.7
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ~>
98
+ - !ruby/object:Gem::Version
99
+ version: 1.8.7
100
+ description: A command line tool that can be run via cron that configures and manages
101
+ Xcode server bots for each pull request
102
+ email: ''
103
+ executables:
104
+ - bot-sync-github
105
+ - bot-devices
106
+ - bot-status
107
+ - bot-delete
108
+ extensions: []
109
+ extra_rdoc_files:
110
+ - LICENSE
111
+ - README.md
112
+ files:
113
+ - .document
114
+ - .rspec
115
+ - Gemfile
116
+ - Gemfile.lock
117
+ - LICENSE
118
+ - README.md
119
+ - Rakefile
120
+ - VERSION
121
+ - bin/bot-delete
122
+ - bin/bot-devices
123
+ - bin/bot-status
124
+ - bin/bot-sync-github
125
+ - lib/bot_builder.rb
126
+ - lib/bot_cli.rb
127
+ - lib/bot_config.rb
128
+ - lib/bot_github.rb
129
+ - spec/github_xcode_bot_builder_spec.rb
130
+ - spec/spec_helper.rb
131
+ homepage: http://github.com/ModCloth/github-xcode-bot-builder
132
+ licenses:
133
+ - MIT
134
+ metadata: {}
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 2.0.3
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: Create Xcode bots to run when github pull requests are created or updated
155
+ test_files: []