travis 1.4.0 → 1.5.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.
@@ -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
@@ -79,6 +79,13 @@ module Travis
79
79
 
80
80
  private
81
81
 
82
+ def listen(*args)
83
+ super(*args) do |listener|
84
+ on_signal { listener.disconnect }
85
+ yield listener
86
+ end
87
+ end
88
+
82
89
  def detected_endpoint
83
90
  Travis::Client::ORG_URI
84
91
  end
@@ -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?
@@ -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
-
@@ -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
- say log(job)
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
@@ -4,6 +4,7 @@ module Travis
4
4
  module CLI
5
5
  class Restart < RepoCommand
6
6
  def run(number = last_build.number)
7
+ authenticate
7
8
  entity = job(number) || build(number)
8
9
  entity.restart
9
10
 
@@ -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 setup_sauce_lab setup_sauce_connect
63
- alias setup_sauce setup_sauce_connect
76
+ alias setup_sauce_labs setup_sauce_connect
77
+ alias setup_sauce setup_sauce_connect
64
78
 
65
79
  private
66
80
 
@@ -4,7 +4,7 @@ module Travis
4
4
  module CLI
5
5
  class Token < ApiCommand
6
6
  def run
7
- error "not logged in, please run #{command("login#{endpoint_option}")}" if access_token.nil?
7
+ authenticate
8
8
  say access_token, "Your access token is %s"
9
9
  end
10
10
  end
@@ -3,7 +3,7 @@ require 'travis/cli'
3
3
  module Travis
4
4
  module CLI
5
5
  class Whatsup < ApiCommand
6
- on('-m', '--my-repos')
6
+ on('-m', '--my-repos', 'Only display my own repositories')
7
7
 
8
8
  def run
9
9
  recent.each do |repo|
@@ -13,6 +13,8 @@ require 'travis/client/job'
13
13
  require 'travis/client/worker'
14
14
  require 'travis/client/namespace'
15
15
  require 'travis/client/account'
16
+ require 'travis/client/broadcast'
17
+ require 'travis/client/listener'
16
18
 
17
19
  module Travis
18
20
  module Client
@@ -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
- return body unless body.respond_to? :encode
15
- body.encode 'utf-8'
17
+ Tools::SafeString.encoded(body)
16
18
  end
17
19
 
18
20
  def colorized_body
19
- attributes['colorized_body'] ||= encoded_body.gsub(/[^[:print:]\e\n]/, '')
21
+ attributes['colorized_body'] ||= Tools::SafeString.colorized(body)
20
22
  end
21
23
 
22
24
  def clean_body
23
- attributes['clean_body'] ||= colorized_body.gsub(/\e[^m]+m/, '')
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
@@ -34,6 +34,10 @@ module Travis
34
34
  pull_request? ? "Pull Request ##{pr_number}" : commit.branch
35
35
  end
36
36
 
37
+ def pusher_channels
38
+ repository.pusher_channels
39
+ end
40
+
37
41
  def inspect_info
38
42
  "#{repository.slug}##{number}"
39
43
  end
@@ -21,8 +21,8 @@ module Travis
21
21
  MAP.fetch(key)
22
22
  end
23
23
 
24
- def self.aka(name)
25
- MAP[name.to_s] = self
24
+ def self.aka(*names)
25
+ names.each { |n| MAP[n.to_s] = self }
26
26
  end
27
27
 
28
28
  def self.one(key = nil)
@@ -48,6 +48,10 @@ module Travis
48
48
  end
49
49
  end
50
50
 
51
+ def pusher_channels
52
+ build.pusher_channels + ["job-#{id}"]
53
+ end
54
+
51
55
  def inspect_info
52
56
  "#{repository.slug}##{number}"
53
57
  end
@@ -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