octolab 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/bin/octolab +134 -0
- data/helpers/ssh +2 -0
- data/lib/octolab.rb +311 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ca57dcf29c979b7163dfbafb2d0c52bff63094d3
|
4
|
+
data.tar.gz: 06037f1b1de72dfa6684c56b895cfcfa2b11d436
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e67e12c7b58011040acba8588e1c8e0ddd191520cd0f488862fd98ae36252d4c6b8d635fd9ec4936e7dc74c3fafbd16bfee739ffc36bf2ebe9d9e5f56ffd8f6
|
7
|
+
data.tar.gz: 6d9a5aaa6f974ed4c30822edb7f365e5658abeca8084cfef59b4747b19326fa325328be16fd12f59d1fbde9763e213466cb0c92cf0f21a57439d6ed9652fcfae
|
data/bin/octolab
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
def check_env
|
4
|
+
ret = true
|
5
|
+
env_vars = ['GITHUB_SECRET',
|
6
|
+
'GITHUB_TOKEN',
|
7
|
+
'GITLAB_TOKEN',
|
8
|
+
'GITLAB_CI_URL',
|
9
|
+
'GIT_DATA_PATH',
|
10
|
+
'PROCESS_UID',
|
11
|
+
'PROCESS_GID',
|
12
|
+
'LOG_DIR_PATH',
|
13
|
+
'LOG_WEB_AGE',
|
14
|
+
'LOG_WEB_SIZE',
|
15
|
+
'LOG_GIT_AGE',
|
16
|
+
'LOG_GIT_SIZE']
|
17
|
+
|
18
|
+
puts "-" * 20
|
19
|
+
puts "\e[1;97mCHECKING ENVIRONMENT\e[0m"
|
20
|
+
puts "-" * 20 + "\n\n"
|
21
|
+
|
22
|
+
env_vars.each { |i|
|
23
|
+
if ENV[i].to_s !~ /^$/
|
24
|
+
puts sprintf("\e[1;97m%-15s\e[0m= \e[1;92m" + (i =~ /(secret|token)$/i ? "[ *** SET *** ]" : ENV[i].to_s) + "\e[0m", i)
|
25
|
+
else
|
26
|
+
puts sprintf("\e[1;97m%-15s\e[0m= \e[1;91mmissing\e[0m", i)
|
27
|
+
ret = false
|
28
|
+
end
|
29
|
+
}
|
30
|
+
|
31
|
+
ret
|
32
|
+
end
|
33
|
+
|
34
|
+
case ARGV[0]
|
35
|
+
when 'check'
|
36
|
+
exit 1 if !check_env
|
37
|
+
exit 0
|
38
|
+
when 'start'
|
39
|
+
puts 'Starting OctoLab...'
|
40
|
+
Dir.mkdir(ENV['LOG_DIR_PATH']) unless File.exist?(ENV['LOG_DIR_PATH'])
|
41
|
+
|
42
|
+
#if ENV['RACK_ENV'] == 'production'
|
43
|
+
# require 'octolab'
|
44
|
+
#else
|
45
|
+
load 'lib/octolab.rb'
|
46
|
+
#end
|
47
|
+
|
48
|
+
server = 'thin'
|
49
|
+
host = ENV['OCTOLAB_LISTEN'] || '0.0.0.0'
|
50
|
+
port = ENV['OCTOLAB_PORT'] || '4567'
|
51
|
+
web_app = OctoLab.new
|
52
|
+
|
53
|
+
begin
|
54
|
+
Process.egid = ENV['PROCESS_GID'] if ENV['PROCESS_GID'] and Process.egid == 0
|
55
|
+
Process.euid = ENV['PROCESS_UID'] if ENV['PROCESS_UID'] and Process.euid == 0
|
56
|
+
rescue
|
57
|
+
STDERR.puts 'WARNING! Unable to change UID/GID: ' + $!.message
|
58
|
+
#exit 1
|
59
|
+
end
|
60
|
+
|
61
|
+
if ENV['RACK_ENV'] == 'production'
|
62
|
+
pid = Process.fork
|
63
|
+
if pid.nil? then
|
64
|
+
Dir.mkdir(ENV['LOG_DIR_PATH']) unless File.exist?(ENV['LOG_DIR_PATH'])
|
65
|
+
|
66
|
+
$stdout.reopen(ENV['LOG_DIR_PATH'] + '/web.log', 'a')
|
67
|
+
$stdout.sync = true
|
68
|
+
$stderr.reopen(ENV['LOG_DIR_PATH'] + '/web.log', 'a')
|
69
|
+
$stderr.sync = true
|
70
|
+
|
71
|
+
dispatch = Rack::Builder.app do
|
72
|
+
map '/' do
|
73
|
+
run web_app
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Rack::Server.start({
|
78
|
+
app: dispatch,
|
79
|
+
server: server,
|
80
|
+
Host: host,
|
81
|
+
Port: port
|
82
|
+
})
|
83
|
+
else
|
84
|
+
if File.exists?(ENV['PID_FILE_PATH'])
|
85
|
+
old_pid = File.read(ENV['PID_FILE_PATH'])
|
86
|
+
proc_file = '/proc/' + old_pid + '/cmdline'
|
87
|
+
if File.exists?(proc_file)
|
88
|
+
cmdline = File.read(proc_file).split("\u0000").join(' ')
|
89
|
+
if cmdline =~ /^ruby \S+\/bin\/octolab start$/
|
90
|
+
puts "OctoLab is running with PID " + old_pid + '.'
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
File.open(ENV['PID_FILE_PATH'], 'w') { |f| f.write(pid) }
|
97
|
+
Process.detach(pid)
|
98
|
+
end
|
99
|
+
else
|
100
|
+
dispatch = Rack::Builder.app do
|
101
|
+
map '/' do
|
102
|
+
run web_app
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
Rack::Server.start({
|
107
|
+
app: dispatch,
|
108
|
+
server: server,
|
109
|
+
Host: host,
|
110
|
+
Port: port
|
111
|
+
})
|
112
|
+
end
|
113
|
+
when 'stop'
|
114
|
+
Process.kill(15, File.read(ENV['PID_FILE_PATH']).to_i)
|
115
|
+
File.delete(ENV['PID_FILE_PATH'])
|
116
|
+
puts 'OctoLab has been stopped.'
|
117
|
+
when 'status'
|
118
|
+
if File.exists?(ENV['PID_FILE_PATH'])
|
119
|
+
pid = File.read(ENV['PID_FILE_PATH'])
|
120
|
+
proc_file = '/proc/' + pid + '/cmdline'
|
121
|
+
if File.exists?(proc_file)
|
122
|
+
cmdline = File.read(proc_file).split("\u0000").join(' ')
|
123
|
+
if cmdline =~ /^ruby \S+\/bin\/octolab start$/
|
124
|
+
puts "OctoLab is running with PID " + pid + '.'
|
125
|
+
else
|
126
|
+
puts 'OctoLab is stopped, but PID file exists.'
|
127
|
+
end
|
128
|
+
else
|
129
|
+
puts 'OctoLab is stopped, but PID file exists.'
|
130
|
+
end
|
131
|
+
else
|
132
|
+
puts 'OctoLab is stopped.'
|
133
|
+
end
|
134
|
+
end
|
data/helpers/ssh
ADDED
data/lib/octolab.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
Bundler.require
|
8
|
+
|
9
|
+
module WEBrick
|
10
|
+
class HTTPResponse
|
11
|
+
def create_error_page
|
12
|
+
@body = { :error => HTMLUtils::escape(@reason_phrase) }.to_json
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Logger
|
18
|
+
def internal(error)
|
19
|
+
error '[!] INTERNAL SERVER ERROR - backtrace:'
|
20
|
+
error error.inspect
|
21
|
+
error error.backtrace
|
22
|
+
error '[!] INTERNAL SERVER ERROR - end of backtrace.'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Git
|
27
|
+
class Lib
|
28
|
+
def remote_add(name, url, opts = {})
|
29
|
+
arr_opts = ['add']
|
30
|
+
arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
|
31
|
+
arr_opts << '-t' << opts[:track] if opts[:track]
|
32
|
+
arr_opts << '--mirror=fetch'if opts[:mirror]
|
33
|
+
arr_opts << '--'
|
34
|
+
arr_opts << name
|
35
|
+
arr_opts << url
|
36
|
+
|
37
|
+
command('remote', arr_opts)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class OctoLab < Sinatra::Base
|
43
|
+
|
44
|
+
GITHUB_SECRET = ENV['GITHUB_SECRET'].to_s
|
45
|
+
GITHUB_TOKEN = ENV['GITHUB_TOKEN'].to_s
|
46
|
+
GITLAB_TOKEN = ENV['GITLAB_TOKEN'].to_s
|
47
|
+
GITLAB_CI_URL = ENV['GITLAB_CI_URL'].to_s
|
48
|
+
GIT_DATA_PATH = ENV['GIT_DATA_PATH'].to_s
|
49
|
+
PROCESS_UID = ENV['PROCESS_UID'].to_s
|
50
|
+
PROCESS_GID = ENV['PROCESS_GID'].to_s
|
51
|
+
LOG_DIR_PATH = ENV['LOG_DIR_PATH'].to_s
|
52
|
+
LOG_WEB_AGE = ENV['LOG_WEB_AGE'].to_s
|
53
|
+
LOG_WEB_SIZE = ENV['LOG_WEB_SIZE'].to_s
|
54
|
+
LOG_GIT_AGE = ENV['LOG_GIT_AGE'].to_s
|
55
|
+
LOG_GIT_SIZE = ENV['LOG_GIT_SIZE'].to_s
|
56
|
+
ROOT_DIR = File.realpath(__FILE__.split('/')[0..-3].join('/'))
|
57
|
+
|
58
|
+
class HTTPError < StandardError
|
59
|
+
end
|
60
|
+
|
61
|
+
class TriggerError < StandardError
|
62
|
+
end
|
63
|
+
|
64
|
+
configure do
|
65
|
+
set :root, __FILE__.split('/')[0..-3].join('/')
|
66
|
+
set :server, 'thin'
|
67
|
+
set :run, true
|
68
|
+
set :haml, { :ugly => true }
|
69
|
+
set :clean_trace, true
|
70
|
+
enable :logging
|
71
|
+
|
72
|
+
|
73
|
+
if settings.production?
|
74
|
+
$log_app = Logger.new(LOG_DIR_PATH + '/app.log', LOG_WEB_AGE, LOG_WEB_SIZE)
|
75
|
+
$log_git = Logger.new(LOG_DIR_PATH + '/git.log', LOG_GIT_AGE, LOG_GIT_SIZE)
|
76
|
+
$log_app.level = Logger::DEBUG
|
77
|
+
$log_git.level = Logger::DEBUG
|
78
|
+
else
|
79
|
+
$log_app = Logger.new(STDOUT)
|
80
|
+
$log_git = Logger.new(STDOUT)
|
81
|
+
$log_app.level = Logger::DEBUG
|
82
|
+
$log_git.level = Logger::DEBUG
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
before do
|
87
|
+
@client ||= Octokit::Client.new(:access_token => GITHUB_TOKEN)
|
88
|
+
end
|
89
|
+
|
90
|
+
post '/events' do
|
91
|
+
begin
|
92
|
+
verify_signature(request.body.read)
|
93
|
+
request.body.rewind
|
94
|
+
payload = JSON.parse(request.body.read)
|
95
|
+
$log_app.debug JSON.pretty_generate(payload) if settings.development?
|
96
|
+
$log_app.info '[*] Received event: ' + request.env['HTTP_X_GITHUB_EVENT'] if request.env['HTTP_X_GITHUB_EVENT']
|
97
|
+
|
98
|
+
case request.env['HTTP_X_GITHUB_EVENT']
|
99
|
+
when 'pull_request'
|
100
|
+
git_fetch_github(payload['pull_request']['base']['repo']['full_name'])
|
101
|
+
if payload['action'] =~ /(re)*opened/
|
102
|
+
ENV['OCTOLAB_COMMITS'] = (ENV['OCTOLAB_COMMITS'].to_s.split(',') + [payload['pull_request']['head']['sha']]).join(',')
|
103
|
+
process_pull_request(payload['pull_request'])
|
104
|
+
end
|
105
|
+
when 'push'
|
106
|
+
git_fetch_github(payload['repository']['full_name'])
|
107
|
+
process_push(payload)
|
108
|
+
end
|
109
|
+
rescue
|
110
|
+
$log_app.internal($!)
|
111
|
+
halt 500, { :error => 'Processing failed' }.to_json
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
post '/builds' do
|
116
|
+
begin
|
117
|
+
payload = JSON.parse(request.body.read)
|
118
|
+
$log_app.debug JSON.pretty_generate(payload) if settings.development?
|
119
|
+
|
120
|
+
case request.env['HTTP_X_GITLAB_EVENT']
|
121
|
+
when 'Build Hook'
|
122
|
+
local_name = payload['project_name'].gsub(/\s\/\s/, '/')
|
123
|
+
commit = payload['sha']
|
124
|
+
git_dir = GIT_DATA_PATH + '/' + local_name + '.git'
|
125
|
+
git = Git.open(git_dir, {:log => $log_git, :repository => git_dir, :working_directory => nil })
|
126
|
+
remote_name = git.remotes.select { |i| i.name == 'github' }.first.url.split(':').last.gsub(/\.git$/, '')
|
127
|
+
build_id = payload['build_id'].to_s
|
128
|
+
commit_status = payload['commit']['status']
|
129
|
+
build_status = payload['build_status']
|
130
|
+
build_status = 'failure' if payload['build_status'] == 'canceled' or payload['build_status'] == 'failed'
|
131
|
+
description = 'The build is in progress...'
|
132
|
+
description = 'The build succeeded!' if build_status == 'success'
|
133
|
+
description = 'The build has failed!' if build_status == 'failure'
|
134
|
+
options = {
|
135
|
+
:state => build_status,
|
136
|
+
:target_url => GITLAB_CI_URL + '/' + remote_name + '/builds/' + build_id,
|
137
|
+
:description => description,
|
138
|
+
:context => 'continuous-integration/gitlab'
|
139
|
+
}
|
140
|
+
|
141
|
+
$log_app.info '[*] Build status changed: ' + remote_name + ': ' + commit + ': ' + build_status
|
142
|
+
|
143
|
+
if ENV['OCTOLAB_COMMITS'].to_s.split(',').include?(commit)
|
144
|
+
$log_app.info '[*] Sending status update: ' + remote_name + ': ' + commit + ': ' + build_status
|
145
|
+
if build_status =~ /pending/
|
146
|
+
@client.create_status(remote_name, commit, build_status, options)
|
147
|
+
elsif build_status =~ /failure/ or (build_status =~ /success/ and commit_status =~ /success/)
|
148
|
+
@client.create_status(remote_name, commit, build_status, options)
|
149
|
+
ENV['OCTOLAB_COMMITS'] = ENV['OCTOLAB_COMMITS'].to_s.split(',').delete_if { |i| i == commit }.join(',')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
return
|
154
|
+
end
|
155
|
+
rescue
|
156
|
+
$log_app.internal($!)
|
157
|
+
halt 500, { :error => 'Processing failed' }.to_json
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
helpers do
|
162
|
+
def logger
|
163
|
+
request.logger
|
164
|
+
end
|
165
|
+
|
166
|
+
def verify_signature(body)
|
167
|
+
signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), GITHUB_SECRET, body)
|
168
|
+
return halt 500, { :error => "Signatures didn't match!" }.to_json unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE'])
|
169
|
+
end
|
170
|
+
|
171
|
+
def git_fetch_github(full_name)
|
172
|
+
$log_app.info '[*] Fetching: ' + full_name
|
173
|
+
|
174
|
+
Git.configure do |config|
|
175
|
+
config.git_ssh = ROOT_DIR + '/helpers/ssh'
|
176
|
+
end
|
177
|
+
|
178
|
+
git_dir = GIT_DATA_PATH + '/' + full_name + '.git'
|
179
|
+
git = Git.open(git_dir, { :log => $log_git, :repository => git_dir, :working_directory => nil })
|
180
|
+
|
181
|
+
if git.remotes.length == 0
|
182
|
+
git.add_remote('github', 'git@github.com:' + full_name + '.git', :mirror => true)
|
183
|
+
end
|
184
|
+
|
185
|
+
git.remote('github').fetch
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_project(name)
|
189
|
+
begin
|
190
|
+
url = GITLAB_CI_URL + '/api/v3/projects'
|
191
|
+
uri = URI(url)
|
192
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
193
|
+
http.use_ssl = true
|
194
|
+
headers = { 'PRIVATE-TOKEN' => GITLAB_TOKEN }
|
195
|
+
path = uri.path.empty? ? '/' : uri.path
|
196
|
+
code = http.head(path, headers).code.to_i
|
197
|
+
|
198
|
+
if (code >= 200 && code < 300)
|
199
|
+
response = http.get(uri.path, headers)
|
200
|
+
JSON.parse(response.body).select { |i| i['path_with_namespace'] == name }.first
|
201
|
+
else
|
202
|
+
raise HTTPError, 'Error code: ' + code.to_s + ' URL: ' + url
|
203
|
+
end
|
204
|
+
rescue
|
205
|
+
raise $!
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def get_trigger_token(id)
|
210
|
+
begin
|
211
|
+
uri = URI(GITLAB_CI_URL + '/api/v3/projects/' + id.to_s + '/triggers')
|
212
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
213
|
+
http.use_ssl = true
|
214
|
+
headers = { 'PRIVATE-TOKEN' => GITLAB_TOKEN }
|
215
|
+
path = uri.path.empty? ? '/' : uri.path
|
216
|
+
code = http.head(path, headers).code.to_i
|
217
|
+
|
218
|
+
if (code >= 200 && code < 300)
|
219
|
+
response = http.get(uri.path, headers)
|
220
|
+
res = JSON.parse(response.body).select { |i| i['deleted_at'] == nil }.first
|
221
|
+
if res == nil
|
222
|
+
raise HTTPError, 'Unable to obtain build trigger token for project id: ' + id.to_s
|
223
|
+
end
|
224
|
+
res
|
225
|
+
else
|
226
|
+
raise HTTPError, 'Error code: ' + code.to_s + ' URL: ' + url
|
227
|
+
end
|
228
|
+
rescue
|
229
|
+
raise $!
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def trigger_build(remote_name, ref)
|
234
|
+
begin
|
235
|
+
project = get_project(remote_name)
|
236
|
+
token = get_trigger_token(project['id'])['token']
|
237
|
+
url = GITLAB_CI_URL + '/api/v3/projects/' + project['id'].to_s + '/trigger/builds'
|
238
|
+
uri = URI(url)
|
239
|
+
response = Net::HTTP.post_form(uri, 'token' => token, 'ref' => ref)
|
240
|
+
|
241
|
+
if (response.code.to_i >= 200 && response.code.to_i < 300)
|
242
|
+
JSON.parse(response.body)
|
243
|
+
elsif response.code.to_i == 400
|
244
|
+
raise TriggerError, JSON.parse(response.body)['message']
|
245
|
+
else
|
246
|
+
raise HTTPError, 'Error code: ' + response.code.to_s + ' URL: ' + url
|
247
|
+
end
|
248
|
+
rescue
|
249
|
+
raise $!
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def process_pull_request(pull_request)
|
254
|
+
Thread.new do
|
255
|
+
begin
|
256
|
+
remote_name = pull_request['base']['repo']['full_name']
|
257
|
+
commit = pull_request['head']['sha']
|
258
|
+
ref = pull_request['head']['ref']
|
259
|
+
build_status = 'pending'
|
260
|
+
description = 'The build is in progress...'
|
261
|
+
options = {
|
262
|
+
:state => build_status,
|
263
|
+
:description => description,
|
264
|
+
:context => 'continuous-integration/gitlab'
|
265
|
+
}
|
266
|
+
|
267
|
+
$log_app.info '[*] Processing pull request: ' + remote_name + ': ' + commit
|
268
|
+
@client.create_status(remote_name, commit, build_status, options)
|
269
|
+
trigger_build(remote_name, ref)
|
270
|
+
rescue Octokit::Unauthorized
|
271
|
+
$log_app.error '[!] ' + $!.message
|
272
|
+
rescue HTTPError
|
273
|
+
$log_app.error $!
|
274
|
+
rescue TriggerError
|
275
|
+
$log_app.error $!
|
276
|
+
rescue
|
277
|
+
$log_app.error '[!] Unable to process request for: ' + remote_name + ' ref: ' + ref
|
278
|
+
$log_app.internal($!)
|
279
|
+
ensure
|
280
|
+
build_status = 'failure'
|
281
|
+
description = 'The build has failed!'
|
282
|
+
options = {
|
283
|
+
:state => build_status,
|
284
|
+
:description => description,
|
285
|
+
:context => 'continuous-integration/gitlab'
|
286
|
+
}
|
287
|
+
|
288
|
+
@client.create_status(remote_name, commit, build_status, options)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def process_push(payload)
|
294
|
+
Thread.new do
|
295
|
+
begin
|
296
|
+
remote_name = payload['repository']['full_name']
|
297
|
+
ref = payload['ref'].split("/").last
|
298
|
+
|
299
|
+
trigger_build(remote_name, ref)
|
300
|
+
rescue HTTPError
|
301
|
+
$log_app.error $!
|
302
|
+
rescue TriggerError
|
303
|
+
$log_app.error $!
|
304
|
+
rescue
|
305
|
+
$log_app.error '[!] Unable to process request for: ' + remote_name + ' ref: ' + ref
|
306
|
+
$log_app.internal($!)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: octolab
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jakub Zubielik
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: OctoLab allows to mirror GitHub repositories in GitLab and to trigger
|
14
|
+
build jobs on push requests.
|
15
|
+
email: jzubielik@gmail.com
|
16
|
+
executables:
|
17
|
+
- octolab
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- bin/octolab
|
22
|
+
- helpers/ssh
|
23
|
+
- lib/octolab.rb
|
24
|
+
homepage: http://rubygems.org/gems/octolab
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.5.1
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: GitHub integration for GitLab CI.
|
48
|
+
test_files: []
|