travis 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +85 -4
- data/Rakefile +76 -2
- data/bin/travis +7 -0
- data/completion/extconf.rb +17 -0
- data/completion/travis.sh +1320 -0
- data/lib/travis/cli.rb +2 -0
- data/lib/travis/cli/accounts.rb +21 -0
- data/lib/travis/cli/api_command.rb +7 -0
- data/lib/travis/cli/command.rb +15 -0
- data/lib/travis/cli/encrypt.rb +1 -2
- data/lib/travis/cli/logs.rb +3 -7
- data/lib/travis/cli/monitor.rb +48 -0
- data/lib/travis/cli/restart.rb +1 -0
- data/lib/travis/cli/setup.rb +16 -2
- data/lib/travis/cli/token.rb +1 -1
- data/lib/travis/cli/whatsup.rb +1 -1
- data/lib/travis/client.rb +2 -0
- data/lib/travis/client/account.rb +5 -1
- data/lib/travis/client/artifact.rb +40 -4
- data/lib/travis/client/broadcast.rb +14 -0
- data/lib/travis/client/build.rb +4 -0
- data/lib/travis/client/entity.rb +2 -2
- data/lib/travis/client/job.rb +4 -0
- data/lib/travis/client/listener.rb +149 -0
- data/lib/travis/client/methods.rb +10 -0
- data/lib/travis/client/repository.rb +9 -1
- data/lib/travis/client/session.rb +5 -0
- data/lib/travis/client/user.rb +10 -1
- data/lib/travis/tools/safe_string.rb +20 -0
- data/lib/travis/version.rb +1 -1
- data/spec/cli/encrypt_spec.rb +5 -0
- data/spec/cli/restart_spec.rb +2 -2
- data/spec/client/broadcast_spec.rb +10 -0
- data/spec/support/fake_api.rb +7 -0
- data/travis.gemspec +27 -12
- metadata +61 -14
data/lib/travis/cli.rb
CHANGED
@@ -16,6 +16,7 @@ module Travis
|
|
16
16
|
module CLI
|
17
17
|
autoload :Token, 'travis/cli/token'
|
18
18
|
autoload :ApiCommand, 'travis/cli/api_command'
|
19
|
+
autoload :Accounts, 'travis/cli/accounts'
|
19
20
|
autoload :Branches, 'travis/cli/branches'
|
20
21
|
autoload :Command, 'travis/cli/command'
|
21
22
|
autoload :Console, 'travis/cli/console'
|
@@ -28,6 +29,7 @@ module Travis
|
|
28
29
|
autoload :Init, 'travis/cli/init'
|
29
30
|
autoload :Login, 'travis/cli/login'
|
30
31
|
autoload :Logs, 'travis/cli/logs'
|
32
|
+
autoload :Monitor, 'travis/cli/monitor'
|
31
33
|
autoload :Open, 'travis/cli/open'
|
32
34
|
autoload :Parser, 'travis/cli/parser'
|
33
35
|
autoload :Pubkey, 'travis/cli/pubkey'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'travis/cli'
|
2
|
+
|
3
|
+
module Travis
|
4
|
+
module CLI
|
5
|
+
class Accounts < ApiCommand
|
6
|
+
def run
|
7
|
+
authenticate
|
8
|
+
accounts.each do |account|
|
9
|
+
color = account.subscribed? ? :green : :info
|
10
|
+
say [
|
11
|
+
color(account.login, [color, :bold]),
|
12
|
+
color("(#{account.name}):", color),
|
13
|
+
account.subscribed? ? "subscribed," : "not subscribed,",
|
14
|
+
account.repos_count == 1 ? "1 repository" : "#{account.repos_count} repositories"
|
15
|
+
].join(" ")
|
16
|
+
end
|
17
|
+
say session.config['host'], "To set up a subscription, please visit %s." unless accounts.all?(&:subscribed?)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/travis/cli/command.rb
CHANGED
@@ -58,6 +58,7 @@ module Travis
|
|
58
58
|
attr_reader :input, :output
|
59
59
|
|
60
60
|
def initialize(options = {})
|
61
|
+
@on_signal = []
|
61
62
|
@formatter = Travis::Tools::Formatter.new
|
62
63
|
self.output = $stdout
|
63
64
|
self.input = $stdin
|
@@ -124,6 +125,7 @@ module Travis
|
|
124
125
|
end
|
125
126
|
|
126
127
|
def execute
|
128
|
+
setup_trap
|
127
129
|
check_ruby
|
128
130
|
check_arity(method(:run), *arguments)
|
129
131
|
load_config
|
@@ -173,8 +175,21 @@ module Travis
|
|
173
175
|
end
|
174
176
|
end
|
175
177
|
|
178
|
+
def on_signal(&block)
|
179
|
+
@on_signal << block
|
180
|
+
end
|
181
|
+
|
176
182
|
private
|
177
183
|
|
184
|
+
def setup_trap
|
185
|
+
[:INT, :TERM].each do |signal|
|
186
|
+
trap signal do
|
187
|
+
@on_signal.each { |c| c.call }
|
188
|
+
exit 1
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
178
193
|
def format(data, format = nil, style = nil)
|
179
194
|
style ||= :important
|
180
195
|
data = format % color(data, style) if format and interactive?
|
data/lib/travis/cli/encrypt.rb
CHANGED
@@ -20,7 +20,7 @@ module Travis
|
|
20
20
|
error "--override without --add makes no sense" if override? and not add?
|
21
21
|
self.override |= !config_key.start_with?('env.') if add? and not append?
|
22
22
|
|
23
|
-
if args.first =~ %r{\w+/\w+}
|
23
|
+
if args.first =~ %r{\w+/\w+} && !args.first.include?("=")
|
24
24
|
warn "WARNING: The name of the repository is now passed to the command with the -r option:"
|
25
25
|
warn " #{command("encrypt [...] -r #{args.first}")}"
|
26
26
|
warn " If you tried to pass the name of the repository as the first argument, you"
|
@@ -100,4 +100,3 @@ Please add the following to your <[[ color('.travis.yml', :info) ]]> file:
|
|
100
100
|
%s
|
101
101
|
|
102
102
|
Pro Tip: You can add it automatically by running with <[[ color('--add', :info) ]]>.
|
103
|
-
|
data/lib/travis/cli/logs.rb
CHANGED
@@ -1,18 +1,14 @@
|
|
1
1
|
require 'travis/cli'
|
2
|
+
require 'travis/tools/safe_string'
|
2
3
|
|
3
4
|
module Travis
|
4
5
|
module CLI
|
5
6
|
class Logs < RepoCommand
|
7
|
+
include Tools::SafeString
|
6
8
|
def run(number = last_build.number)
|
7
9
|
error "##{number} is not a job, try #{number}.1" unless job = job(number)
|
8
|
-
|
10
|
+
job.log.body { |part| print interactive? ? encoded(part) : clean(part) }
|
9
11
|
end
|
10
|
-
|
11
|
-
private
|
12
|
-
|
13
|
-
def log(job)
|
14
|
-
interactive? ? job.log.colorized_body : job.log.clean_body
|
15
|
-
end
|
16
12
|
end
|
17
13
|
end
|
18
14
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'travis/cli'
|
2
|
+
|
3
|
+
module Travis
|
4
|
+
module CLI
|
5
|
+
class Monitor < ApiCommand
|
6
|
+
on('-m', '--my-repos', 'Only monitor my own repositories')
|
7
|
+
on('-r', '--repo SLUG', 'monitor given repository (can be used more than once)') do |c, slug|
|
8
|
+
c.repos << slug
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :repos
|
12
|
+
|
13
|
+
def initialize(*)
|
14
|
+
@repos = []
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
super
|
20
|
+
repos.map! { |r| repo(r) }
|
21
|
+
repos.concat(user.repositories) if my_repos?
|
22
|
+
end
|
23
|
+
|
24
|
+
def description
|
25
|
+
case repos.size
|
26
|
+
when 0 then session.config['host']
|
27
|
+
when 1 then repos.first.slug
|
28
|
+
else "#{repos.size} repositories"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
listen(*repos) do |listener|
|
34
|
+
listener.on_connect { say description, 'Monitoring %s:' }
|
35
|
+
listener.on 'build:started', 'job:started', 'build:finished', 'job:finished' do |event|
|
36
|
+
entity = event.job || event.build
|
37
|
+
time = entity.finished_at || entity.started_at
|
38
|
+
say [
|
39
|
+
color(formatter.time(time), entity.color),
|
40
|
+
color(entity.inspect_info, [entity.color, :bold]),
|
41
|
+
color(entity.state, entity.color)
|
42
|
+
].join(" ")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/travis/cli/restart.rb
CHANGED
data/lib/travis/cli/setup.rb
CHANGED
@@ -33,6 +33,20 @@ module Travis
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
def setup_rubygems
|
37
|
+
configure 'deploy', 'provider' => 'rubygems' do |config|
|
38
|
+
rubygems_file = File.expand_path('.rubygems/authorization', ENV['HOME'])
|
39
|
+
|
40
|
+
if File.exist? rubygems_file
|
41
|
+
config['api_key'] = File.read(rubygems_file)
|
42
|
+
end
|
43
|
+
|
44
|
+
config['api_key'] ||= ask("RubyGems API token: ") { |q| q.echo = "*" }.to_s
|
45
|
+
config['on'] = { 'repo' => repository.slug } if agree("Deploy only from #{repository.slug}? ") { |q| q.default = 'yes' }
|
46
|
+
encrypt(config, 'api_key') if agree("Encrypt API key? ") { |q| q.default = 'yes' }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
36
50
|
def setup_nodejitsu
|
37
51
|
configure 'deploy', 'provider' => 'nodejitsu' do |config|
|
38
52
|
jitsu_file = File.expand_path('.jitsuconf', ENV['HOME'])
|
@@ -59,8 +73,8 @@ module Travis
|
|
59
73
|
end
|
60
74
|
end
|
61
75
|
|
62
|
-
alias
|
63
|
-
alias setup_sauce
|
76
|
+
alias setup_sauce_labs setup_sauce_connect
|
77
|
+
alias setup_sauce setup_sauce_connect
|
64
78
|
|
65
79
|
private
|
66
80
|
|
data/lib/travis/cli/token.rb
CHANGED
data/lib/travis/cli/whatsup.rb
CHANGED
data/lib/travis/client.rb
CHANGED
@@ -3,12 +3,16 @@ require 'travis/client'
|
|
3
3
|
module Travis
|
4
4
|
module Client
|
5
5
|
class Account < Entity
|
6
|
-
attributes :name, :login, :type, :repos_count
|
6
|
+
attributes :name, :login, :type, :repos_count, :subscribed
|
7
7
|
|
8
8
|
one :account
|
9
9
|
many :accounts
|
10
10
|
|
11
11
|
inspect_info :login
|
12
|
+
|
13
|
+
def subscribed
|
14
|
+
attributes.fetch('subscribed') { true }
|
15
|
+
end
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'travis/client'
|
3
|
+
require 'travis/tools/safe_string'
|
3
4
|
|
4
5
|
module Travis
|
5
6
|
module Client
|
6
7
|
class Artifact < Entity
|
8
|
+
CHUNKED = "application/json; chunked=true; version=2, application/json; version=2"
|
9
|
+
|
7
10
|
# @!parse attr_reader :job_id, :type, :body
|
8
11
|
attributes :job_id, :type, :body
|
9
12
|
|
@@ -11,16 +14,49 @@ module Travis
|
|
11
14
|
has :job
|
12
15
|
|
13
16
|
def encoded_body
|
14
|
-
|
15
|
-
body.encode 'utf-8'
|
17
|
+
Tools::SafeString.encoded(body)
|
16
18
|
end
|
17
19
|
|
18
20
|
def colorized_body
|
19
|
-
attributes['colorized_body'] ||=
|
21
|
+
attributes['colorized_body'] ||= Tools::SafeString.colorized(body)
|
20
22
|
end
|
21
23
|
|
22
24
|
def clean_body
|
23
|
-
attributes['clean_body'] ||=
|
25
|
+
attributes['clean_body'] ||= Tools::SafeString.clean(body)
|
26
|
+
end
|
27
|
+
|
28
|
+
def body(stream = block_given?)
|
29
|
+
return load_attribute('body') unless block_given? or stream
|
30
|
+
return yield(load_attribute('body')) unless stream and job.pending?
|
31
|
+
number = 0
|
32
|
+
|
33
|
+
session.listen(self) do |listener|
|
34
|
+
listener.on 'job:log' do |event|
|
35
|
+
next unless event.payload['number'] > number
|
36
|
+
number = event.payload['number']
|
37
|
+
yield event.payload['_log']
|
38
|
+
listener.disconnect if event.payload['final']
|
39
|
+
end
|
40
|
+
|
41
|
+
listener.on 'job:finished' do |event|
|
42
|
+
listener.disconnect
|
43
|
+
end
|
44
|
+
|
45
|
+
listener.on_connect do
|
46
|
+
data = session.get_raw("/artifacts/#{id}", nil, "Accept" => CHUNKED)['log']
|
47
|
+
if data['parts']
|
48
|
+
data['parts'].each { |p| yield p['content'] }
|
49
|
+
number = data['parts'].last['number'] if data['parts'].any?
|
50
|
+
else
|
51
|
+
yield data['body']
|
52
|
+
listener.disconnect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def pusher_entity
|
59
|
+
job
|
24
60
|
end
|
25
61
|
|
26
62
|
one :log
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'travis/client'
|
2
|
+
|
3
|
+
module Travis
|
4
|
+
module Client
|
5
|
+
class Broadcast < Entity
|
6
|
+
attributes :recipient_id, :recipient_type, :kind, :message, :expired, :created_at, :updated_at
|
7
|
+
|
8
|
+
one :broadcast
|
9
|
+
many :broadcasts
|
10
|
+
|
11
|
+
inspect_info :message
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/travis/client/build.rb
CHANGED
data/lib/travis/client/entity.rb
CHANGED
data/lib/travis/client/job.rb
CHANGED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'travis/client'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
if require 'pusher-client'
|
6
|
+
# it's us that has been loading pusher-client
|
7
|
+
# so let's assume we can mess with it - yay for global state
|
8
|
+
PusherClient.logger.level = 2
|
9
|
+
end
|
10
|
+
|
11
|
+
module Travis
|
12
|
+
module Client
|
13
|
+
class Listener
|
14
|
+
class Socket < PusherClient::Socket
|
15
|
+
attr_accessor :session, :signatures
|
16
|
+
def initialize(application_key, options = {})
|
17
|
+
@session = options.fetch(:session)
|
18
|
+
@signatures = {}
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def subscribe_all
|
23
|
+
# bulk auth on connect
|
24
|
+
fetch_auth(*channels.channels.keys)
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch_auth(*channels)
|
29
|
+
channels.select! { |c| signatures[c].nil? if c.start_with? 'private-' }
|
30
|
+
signatures.merge! session.post_raw('/pusher/auth', :channels => channels, :socket_id => socket_id)['channels'] if channels.any?
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_private_auth(channel)
|
34
|
+
fetch_auth(channel.name)
|
35
|
+
signatures[channel.name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
EVENTS = %w[
|
40
|
+
build:created build:started build:finished
|
41
|
+
job:created job:started job:log job:finished
|
42
|
+
]
|
43
|
+
|
44
|
+
Event = Struct.new(:type, :repository, :build, :job, :payload)
|
45
|
+
|
46
|
+
class EntityListener
|
47
|
+
attr_reader :listener, :entities
|
48
|
+
|
49
|
+
extend Forwardable
|
50
|
+
def_delegators :listener, :disconnect, :on_connect, :subscribe
|
51
|
+
|
52
|
+
def initialize(listener, entities)
|
53
|
+
@listener, @entities = listener, Array(entities)
|
54
|
+
end
|
55
|
+
|
56
|
+
def on(*events)
|
57
|
+
listener.on(*events) { |e| yield(e) if dispatch?(e) }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def dispatch?(event)
|
63
|
+
entities.include? event.repository or
|
64
|
+
entities.include? event.build or
|
65
|
+
entities.include? event.job
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_reader :session, :socket
|
70
|
+
|
71
|
+
def initialize(session)
|
72
|
+
@session = session
|
73
|
+
@socket = Socket.new(pusher_key, :encrypted => true, :session => session)
|
74
|
+
@channels = []
|
75
|
+
@callbacks = []
|
76
|
+
end
|
77
|
+
|
78
|
+
def subscribe(*entities)
|
79
|
+
entities = entities.map do |entity|
|
80
|
+
entity = entity.pusher_entity while entity.respond_to? :pusher_entity
|
81
|
+
@channels.concat(entity.pusher_channels)
|
82
|
+
entity
|
83
|
+
end
|
84
|
+
|
85
|
+
yield entities.any? ? EntityListener.new(self, entities) : self if block_given?
|
86
|
+
end
|
87
|
+
|
88
|
+
def on(*events, &block)
|
89
|
+
events = events.flat_map { |e| e.respond_to?(:to_str) ? e.to_str : EVENTS.grep(e) }.uniq
|
90
|
+
events.each { |e| @callbacks << [e, block] }
|
91
|
+
end
|
92
|
+
|
93
|
+
def on_connect
|
94
|
+
socket.bind('pusher:connection_established') { yield }
|
95
|
+
end
|
96
|
+
|
97
|
+
def listen
|
98
|
+
@channels = default_channels if @channels.empty?
|
99
|
+
@channels.map! { |c| c.start_with?('private-') ? c : "private-#{c}" } if session.private_channels?
|
100
|
+
@channels.uniq.each { |c| socket.subscribe(c) }
|
101
|
+
@callbacks.each { |e,b| socket.bind(e) { |d| dispatch(e, d, &b) } }
|
102
|
+
socket.connect
|
103
|
+
end
|
104
|
+
|
105
|
+
def disconnect
|
106
|
+
socket.disconnect
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def dispatch(type, json)
|
112
|
+
payload = JSON.parse(json)
|
113
|
+
entities = session.load format_payload(type, payload)
|
114
|
+
yield Event.new(type, entities['repository'], entities['build'], entities['job'], payload)
|
115
|
+
end
|
116
|
+
|
117
|
+
def format_payload(type, payload)
|
118
|
+
case type
|
119
|
+
when "job:log" then format_log(payload)
|
120
|
+
when /job:/ then format_job(payload)
|
121
|
+
else payload
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def format_job(payload)
|
126
|
+
build = { "id" => payload["build_id"], "repository_id" => payload["repository_id"] }
|
127
|
+
repo = { "id" => payload["repository_id"], "slug" => payload["repository_slug"] }
|
128
|
+
build["number"] = payload["number"][/^[^\.]+/] if payload["number"]
|
129
|
+
{ "job" => payload, "build" => build, "repository" => repo }
|
130
|
+
end
|
131
|
+
|
132
|
+
def format_log(payload)
|
133
|
+
job = session.job(payload['id'])
|
134
|
+
{ "job" => { "id" => job.id }, "build" => { "id" => job.build.id }, "repository" => { "id" => job.repository.id } }
|
135
|
+
end
|
136
|
+
|
137
|
+
def default_channels
|
138
|
+
return ['common'] if session.access_token.nil?
|
139
|
+
session.user.channels
|
140
|
+
end
|
141
|
+
|
142
|
+
def pusher_key
|
143
|
+
session.config.fetch('pusher').fetch('key')
|
144
|
+
rescue IndexError
|
145
|
+
raise Travis::Client::Error, "#{session.api_endpoint} is missing pusher key"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|