Tools for Travis
Travis is an open source and distributed build system for the community. They’re doing continuous integration a lot of great open source projects, including Ruby on Rails recently. Since there’s a bunch of great data in there, this week’s challenge is to do something great with Travis.
You can build anything you like, from CI monitors to command line interfaces showing you data from your projects. You’re probably going to need to fetch data from Travis, so be sure to check out the API documentation. We’ll keep this one open-ended, since the goal is to help the Travis team out a bit. If you’re building a web interface, be sure to use something small, like Sinatra. Remember, your entry should fit into a gist. If you run into any issues along the way, just hop into #travis on Freenode and the team will be happy to help you out.
Remember to keep your entry small, write nice code and include a README explaining what your project does, which problem it solves and how it works. If you’re building a web interface, be sure to include screenshots in there.
We’re still working on a special prize for this week, so you’ll have to wait for some more to get to know what it is.
As always, you have one week to enter, so go build something awesome. Good luck!
Prize
This week’s winner will receive a code for $25 worth of credit for the 6sync hosting platform!
Oh, and this contest description was written by Jeff Kreeftmeijer, on behalf of Sven.
-
Finished in 1st place with a final score of 4.1/5. (View the Gist)Gemfile
source 'http://rubygems.org' gem 'sinatra' gem 'nokogiri' gem 'multi_xml' gem 'slim'
README.mdIs it Rubinius? Ask Travis!
This project was built on Friday, August 5, 2011 as part of the celebration of Rubinius Day!
It utilizes Travis:
- ability test gems against Rubinius
- API
This simple web app borrows graphics and stylesheets from http://isitrubinius.com to create a site with similar functionality, but instead of being crowdsourced, it's powered by Travis: http://isitrbx.herokuapp.com
The app allows you to enter the name of any gem and will tell you whether the latest Rubinius build is passing on Travis.
It could be easily modified to test gems for compatibility with JRuby, MRI 1.9.3, or any other Ruby version/implementation supported by RVM and Travis.
Screenshot

Examples
- Passing: http://isitrbx.herokuapp.com/search?gem=faraday
- Failing: http://isitrbx.herokuapp.com/search?gem=congress
- Unknown: http://isitrbx.herokuapp.com/search?gem=rails
Credits
This project uses the following open-source libraries:
isitrbx.rbrequire 'open-uri' RVM = "rbx" get '/' do slim '' end get '/search' do begin @rubygems_url = "http://rubygems.org/gems/#{params[:gem]}" rubygems = Nokogiri::HTML(open(@rubygems_url)) @authors = rubygems.css('.who .authors p').first.content @description = rubygems.css('#markup p').first.content @github_url = rubygems.css('.meta .links a').map{|link| link['href']}.find{|href| href =~ /github\.com/i} github_array = @github_url.split('/') # ["https:", "", "github.com", "user", "project"] @user, @project = github_array[3], github_array[4] @travis_url = "http://travis-ci.org/#{@user}/#{@project}.xml?rvm=#{RVM}" @status = MultiXml.parse(open(@travis_url))['repository']['last_build_status'] slim :show rescue OpenURI::HTTPError raise Sinatra::NotFound end end
config.rurequire 'bundler' Bundler.require require './isitrbx' run Sinatra::Application
show.slimh1 id="gem-name" = @project dl dt Author(s) dd = @authors dt Summary dd = @description div id="status" - case @status - when "0" span class="passing" passing - when "1" span class="failing" failing - when nil span class="unknown" unknown div Latest Rubinius build status on Travis for #{@project}.layout.slimView full entrydoctype html html head title Is it Rubinius? link href="/css/application.css" media="screen" rel="stylesheet" type="text/css" body div id="head" a href="/" id="branding" span id="tagline" Travis-powered gem compatibility for Rubinius form action="/search" method="get" input type="text" name="gem" value=params[:gem] input type="submit" value="Search" div id="main" == yield -
Finished in 2nd place with a final score of 3.6/5. (View the Gist)README
ABOUT: small build monitor using MacRuby and Growl RUN: macruby tci.rb CONFIG: have a look at the tci.yml right now. user-preferences are coming!
tci.png
tci.rbrequire "yaml" require "json" require "net/http" require 'growl' framework 'AppKit' IMAGE = 'tci.png' @config = File.open("tci.yml") { |file| YAML.load(file) } @interval = @config['interval'].to_f @repos = @config['repos'] @results = Hash.new({}) @queue = Dispatch::Queue.new('de.nofail') @growl = Growl.new('de.nofail.tci', ['notification'], NSImage.alloc.initWithContentsOfFile(IMAGE)) @timer = NSTimer.scheduledTimerWithTimeInterval(@interval, target: self, selector: 'refresh_results:', userInfo: nil, repeats: true) def setupMenu menu = NSMenu.new menu.initWithTitle 'tci' @repos.each do |repo| mi = NSMenuItem.new mi.title = repo mi.action = 'showStatus:' mi.target = self menu.addItem mi end mi = NSMenuItem.new mi.title = 'Reload' mi.action = 'refresh_results:' mi.target = self menu.addItem mi mi = NSMenuItem.new mi.title = 'Quit' mi.action = 'quit:' mi.target = self menu.addItem mi menu end def showStatus(sender) alert = NSAlert.new alert.messageText = "Build result for #{sender.title}" alert.informativeText = @results[sender.title] alert.alertStyle = NSInformationalAlertStyle alert.addButtonWithTitle("close") response = alert.runModal end def quit(sender) NSApplication.sharedApplication.terminate(self) end def refresh_results(timer) @queue.async do @repos.each do |repo| url = "#{@config['remote']}#{repo}.json" res = Net::HTTP.get_response(URI.parse(url)) @results[repo] = result = JSON.parse(res.body) @growl.notify('notification', repo, result['status']) end end end app = NSApplication.sharedApplication status_bar = NSStatusBar.systemStatusBar status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength) status_item.setMenu setupMenu img = NSImage.new.initWithContentsOfFile IMAGE status_item.setImage(img) refresh_results(nil) app.run
growl.rbframework 'Cocoa' framework 'Foundation' class Growl def initialize(app, notifications, icon = nil) @application_name = app @application_icon = icon @notifications = notifications @default_notifications = notifications @center = NSDistributedNotificationCenter.defaultCenter send_registration! end def notify(notification, title, description, options = {}) dict = { :ApplicationName => @application_name, :NotificationName => notification, :NotificationTitle => title, :NotificationDescription => description, :NotificationPriority => options[:priority] || 0, :NotificationIcon => @application_icon.TIFFRepresentation, } dict[:NotificationSticky] = 1 if options[:sticky] @center.postNotificationName(:GrowlNotification, object:nil, userInfo:dict, deliverImmediately:false) end def send_registration! dict = { :ApplicationName => @application_name, :ApplicationIcon => @application_icon.TIFFRepresentation, :AllNotifications => @notifications, :DefaultNotifications => @default_notifications } @center.postNotificationName(:GrowlApplicationRegistrationNotification, object:nil, userInfo:dict, deliverImmediately:true) end end
tci.ymlView full entryinterval: 60 remote: "http://travis-ci.org/" repos: - "phoet/asin"
-
Finished in 3rd place with a final score of 3.5/5. (View the Gist)README.md
Who is to blame?
Do you want to know who broke you tests?
http://whoistoblame.heroku.com/
Type your github name and project name and you will see who is to blame for broken test! (your project must use travis-ci for continuous integration)
For example: http://whoistoblame.heroku.com/?user=travis-ci&project=travis-ci
whoistoblame.rbView full entryrequire 'sinatra' require 'open-uri' require 'json' require 'nokogiri' class Github def initialize owner, project, sha @owner = owner @project = project @sha = sha end def user_info @user_info ||= JSON.parse(open("https://api.github.com/users#{username}").read) end private def commit_page @commit_page ||= Nokogiri::HTML(open("https://github.com/#{@owner}/#{@project}/commit/#{@sha}").read) end def username commit_page.xpath("//div[@class='name']/a/@href").first.value end end class Travis def initialize owner, project @owner = owner @project = project end def status project_info["last_build_status"] end def id project_info["last_build_id"] end def last_commit build_info["commit"] end private def project_info @project_info ||= JSON.parse(open("http://travis-ci.org/#{@owner}/#{@project}.json").read) end def build_info @build_info ||= JSON.parse(open("http://travis-ci.org/#{@owner}/#{@project}/builds/#{id}.json").read) end end get '/' do form = "<div style='text-align:center; margin-top:10%'> <form action='/'> <label for='user'>Owner:</label> <input name='user' /> <label for='project'>Project:</label> <input name='project' /> <input type='submit' value='Blame!' /> </form> </div>" if params[:user] && params[:project] travis = Travis.new(params[:user], params[:project]) if travis.status == 1 github = Github.new(params[:user], params[:project], travis.last_commit) user = github.user_info "<html> <head></head> <body> <header style='text-align:center'> <h2>Who is to blame?</h2> <h3>Build status of #{params[:project]}: <span style='color:red'>Failed!</span></h3> </header> <div style='margin-top:10%;margin-left:40%'> <span style='margin-left:10%'> <img src='#{user['avatar_url']}' /> </span> <p>Name: #{user['name']}</p> <p>Github: <a href='#{user['html_url']}'>#{user['login']}</a></p> <p>Email: #{user['email']}</p> <p>Location: #{user['location']}</p> </div> #{form} </body> </html>" else "<html> <head></head> <body> <header style='text-align:center'> <h2>Who is to blame?</h2> <h3>Build status of #{params[:project]}: <span style='color:green'>Passed!</span></h3> </header> <div style='margin-top:10%;margin-left:40%'> <h1>No blame! <3 </h1> </div> #{form} </body> </html>" end else "<html> <head></head> <body> <header style='text-align:center'> <h2>Who is to blame?</h2> </header> #{form} </body> </html>" end end
-
Finished in 4th place with a final score of 3.3/5. (View the Gist)travis-ci.rb
#TODOS: #* Improve cache on relationships #* Add raw responses #* Add Proxy support #* Rename the base class #* Handle errors/Raise custom exceptions #* RDoc me! require 'net/http' require 'json' #Only json will be used until we add support for a raw response #require 'rexml/document' module TravisCI # Wrapper for the Travis CI API class Client def initialize(options={}) @options = {:format=>DEFAULT_FORMAT}.merge(options) end # Since right now we are not supporting a raw response the format doesn't make any sence # def format(format) # @options[:format] = format.to_s # self # end def self.method_missing(method, *args, &block) return self.new.send(method, *args, &block) if self.client.respond_to?(method) super end def self.respond_to?(method, include_private=false) self.client.respond_to?(method, include_private) || super(method, include_private) end private API_HOST = 'travis-ci.org' DEFAULT_FORMAT = 'json' def results_for(path) @options.each_pair do |key, value| path.gsub!(":#{key}", value) end #TODO Add proxy support response = Net::HTTP.get_response(API_HOST, path) return parse(response.body) end def parse(content) case @options[:format] when 'json' then return JSON.parse(content) when 'xml' return REXML::Document.new(content) end end def self.client self.new end end class Repositories < Client @@repositories = nil @@repositories_cache = [] @@builds = nil @@builds_cache = [] def all @@repositories || self.all! end def all! @@repositories = results_for(REPOSITORIES_PATH).map {|repository_data| Repository.new(nil, nil, repository_data)} end def fetch fetch_from_cache() || self.fetch! end def fetch! repositories_cache().delete_if {|r| r.owner == @options[:owner] && r.name == @options[:name]} repository = Repository.new(@options[:owner], @options[:name], results_for(REPOSITORY_PATH)) repositories_cache() << repository repository end def builds @@builds || self.builds! end def builds! @@builds = results_for(REPOSITORY_BUILDS_PATH).map {|build_data| Build.new(@options[:owner], @options[:name], build_data)} end def build(build_id) build_from_cache(build_id) || self.build!(build_id) end def build!(build_id) @options[:build_id] = build_id.to_s builds_cache().delete_if {|b| b.id == @options[:build_id]} build = Build.new(@options[:owner], @options[:name], results_for(REPOSITORY_BUILD_PATH)) builds_cache() << build build end def owner(owner) @options[:owner] = owner self end def name(name) @options[:name] = name self end def slug(slug) @options[:owner], @options[:name] = slug.split('/') self end def self.name(name) self.new.name(name) end private REPOSITORY_PATH = '/:owner/:name.:format' REPOSITORIES_PATH = '/repositories.:format' REPOSITORY_BUILD_PATH = '/:owner/:name/builds/:build_id.:format' REPOSITORY_BUILDS_PATH = '/:owner/:name/builds.:format' def repositories_cache @@repositories || @@repositories_cache end def builds_cache @@builds || @@builds_cache end def fetch_from_cache repositories_cache().find {|repository| repository.owner == @options[:owner] && repository.name == @options[:name]} end def build_from_cache(build_id) builds_cache().find {|build| build.id == build_id.to_s} end end # I know, it's an awful name, I will rename it, suggestions are welcome! :P class Base def initialize(attributes={}) @attributes = attributes end def method_missing(method, *args, &block) return @attributes[method.to_s] if @attributes.has_key?(method.to_s) super end def respond_to?(method, include_private=false) @attributes.hash_key?(method.to_s) || super(method, include_private) end protected def attributes @attributes end end class Repository < Base #@attributes => [:slug, :id, :status, :last_build_id, :last_build_status, :last_build_number, :last_build_finished_at] attr_reader :name, :owner def initialize(owner, name, attributes={}) @owner = owner || attributes[:slug] && attributes[:slug].split('/').first @name = name || attributes[:slug] && attributes[:slug].split('/').last super(attributes) end def builds @builds ||= Repositories.owner(self.owner).name(self.name).builds! end def last_build @last_build ||= Repositories.owner(self.owner).name(self.name).build!(self.last_build_id) end def reload! @builds = nil @last_build = nil @attributes = Repositories.owner(self.owner).name(self.name).fetch!.attributes end end class Build < Base #@attributes => [:number, :commited_at, :commit, :finished_at, :config, :author_name, :log, :branch, :id, :parent_id, :started_at, :author_email, :status, :repository_id, :message, :compare_url, :matrix] def initialize(repository_owner, repository_name, attributes={}) @repository_owner = repository_owner @repository_name = repository_name super(attributes) end def parent @parent ||= Repositories.owner(@repository_owner).name(@repository_name).build!(self.partner_id) end def repository @repository ||= Repositories.owner(@repository_owner).name(@repository_name).fetch! end def matrix return nil unless @attributes.has_key?('matrix') @matrix ||= @attributes['matrix'].map {|build_data| Build.new(@repository_owner, @repository_name, build_data)} end def reload! @repository = nil @parent = nil @matrix = nil @attributes = Repositories.owner(@repository_owner).name(@repository_name).build!(self.id).attributes end end end
README.textileView full entryTravis CI API Wrapper
Usage
#Repositories TravisCI::Repositories.all #Collection of Repository instances TravisCI::Repositories.all! #Collection of Repository instances bypassing cache TravisCI::Repositories.owner('owner_name_here').name('name_here').fetch #Repository TravisCI::Repositories.owner('owner_name_here').name('name_here').fetch! #Repository bypassing cache TravisCI::Repositories.slug('owner_name_here/name_here').fetch #Repository TravisCI::Repositories.slug('owner_name_here/name_here').fetch! #Repository bypassing cache #Builds TravisCI::Repositories.owner('owner_name_here').name('name_here').builds #Collection of Build instances TravisCI::Repositories.owner('owner_name_here').name('name_here').builds! #Collection of Build instances bypassing cache TravisCI::Repositories.slug('owner_name_here/name_here').builds #Collection of Build instances TravisCI::Repositories.slug('owner_name_here/name_here').builds! #Collection of Build instances bypassing cache TravisCI::Repositories.owner('owner_name_here').name('name_here').build('id_here') #Build TravisCI::Repositories.owner('owner_name_here').name('name_here').build!('id_here') #Build bypassing cache TravisCI::Repositories.slug('owner_name_here/name_here').build('id_here') #Build TravisCI::Repositories.slug('owner_name_here/name_here').build!('id_here') #Build bypassing cacheRepository methods
The main attributes from the API will be accessible via the following methods:
[:slug, :id, :status, :last_build_id, :last_build_status, :last_build_number, :last_build_finished_at]Some relationships can be retrieved using the following methods:
repository.builds #=> Collection of Build instances repository.last_build #=> BuildYou can also update the repository information from the API usign:
repository.reload! #=> Fetch the updated repository data from the API and updates its attributes and relationshipsBuild methods
The main attributes from the API will be accessible via the following methods:
[:number, :commited_at, :commit, :finished_at, :config, :author_name, :log, :branch, :id, :parent_id, :started_at, :author_email, :status, :repository_id, :message, :compare_url, :matrix]Some relationships can be retrieved using the following methods:
build.parent #=> Build or nil build.repository #=> Repository build.matrix #=> Collection of Build instances or nilYou can also update the build information from the API usign:
build.reload! #=> Fetch the updated build data from the API and updates its attributes and relationships -
Finished in 5th place with a final score of 3.3/5. (View the Gist)README.md
A simple Sinatra app that show (# of builds) / (# of worker) message

main.rbView full entry%w{restclient json erb sinatra}.each {|lib|require lib} def parse url, opts={}, &block opts = {only: //, except: /^$/}.merge(opts) JSON.parse(RestClient.get("http://travis-ci.org#{url}", accept: :json)). map(&block).count{|s|opts[:only].match(s) and not opts[:except].match(s)} end def workers(opts) parse('/workers', opts) {|h|h['id']} end def queue(opts) parse('/jobs?queue=builds', opts) {|h|h['repository']['slug']} end get '/' do @workers = workers only: /^main/ @queue = queue except: %r{(^rails/rails$)|travis} @class, @message = if @workers > @queue ['', 'Travis is READY for action!'] elsif @workers < @queue and @queue < 3 * @workers ['minor', 'Travis is in action'] else ['major', 'Travis is chugging along'] end erb :index end __END__ @@ index <!doctype html> <html><head><style> body,article,p,h2,header,div{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;display:block}body{color:#fff;font:1em'Trebuchet MS';overflow: hidden;background:#000;background: -webkit-gradient(radial,50% 0,200,50% -99,900,from(rgba(0,0,0,0)),to(#000)),-webkit-gradient(linear,right top,left top,from(#f14),color-stop(.6,#e1b),to(#0ae)); <% ['-moz-', '-o-', ''].each do |b| %>background: <%= b %>radial-gradient(50% 0,circle closest-corner,rgba(0,0,0,0),#000),<%= b %>linear-gradient(right center,#f14 30%,#e1b 60%,#0ae 99%);<% end %>} article{width:35%;margin: 208px auto 999999px auto;background: rgba(0, 0, 0, .2);padding: 20px 30px;border-radius: 10px;-moz-border-radius: 10px;-webkit-border-radius: 10px;-o-border-radius: 10px}h2{margin:0 1em 1em 24px;font-size:1.5em}#status{box-shadow: 1px 1px 7px #999;width: 15px;height: 15px;border-radius: 10px;background: #64c901;float:left;margin-top:6px}#status.minor{background:#ea7f00}#status.major{background:#c90101} </style></head><body><article><header><div id='status' class='<%= @class %>'></div><h2><%= @message %></h2><p><%= @queue %> builds on <%= @workers %> workers</p></header></article></body></html>