diff --git a/.travis.yml b/.travis.yml index 67e6504c..27dc76a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: ruby rvm: - 1.9.3 - rbx-19mode + - jruby-19mode before_script: - 'RAILS_ENV=test rake db:create db:schema:load --trace' notifications: @@ -9,3 +10,4 @@ notifications: matrix: allow_failures: - rvm: rbx-19mode + - rvm: jruby-19mode diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0545a308 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing to Travis-CI +Issues for any Travis-CI repo should be submitted to https://github.com/travis-ci/travis-ci/issues + +## Security Issues +***Any security issues should be submitted directly to [security@travis-ci.org](mailto:security@travis-ci.org)*** + +## Reporting Issues +- Explain what you expected to happen vs the actual results +- Include a screenshot if it helps illustrate the issue. https://github.com/blog/1347-issue-attachments +- What steps are required to reproduce the issue +- An example build that shows the issue + +## Submitting a PR to Travis-API + +See testing and setup notes in the base [README](https://github.com/travis-ci/travis-api) diff --git a/Gemfile b/Gemfile index c0555e45..ace4c1c0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,18 +1,18 @@ ruby '1.9.3' rescue nil -source :rubygems +source 'https://rubygems.org' gemspec -gem 'travis-support', github: 'travis-ci/travis-support' gem 'travis-core', github: 'travis-ci/travis-core' +gem 'travis-support', github: 'travis-ci/travis-support' gem 'travis-sidekiqs', github: 'travis-ci/travis-sidekiqs', require: nil, ref: 'cde9741' -gem 'sinatra', github: 'sinatra/sinatra' -gem 'sinatra-contrib', github: 'sinatra/sinatra-contrib', require: nil +gem 'sinatra' #github: 'sinatra/sinatra' +gem 'sinatra-contrib', require: nil #github: 'sinatra/sinatra-contrib', require: nil # TODO need to release the gem as soon i'm certain this change makes sense gem 'simple_states', github: 'svenfuchs/simple_states', branch: 'sf-set-state-early' -gem 'unicorn' +gem 'puma', '1.6.3' gem "sentry-raven", github: 'getsentry/raven-ruby' gem 'yard-sinatra', github: 'rkh/yard-sinatra' gem 'rack-contrib', github: 'rack/rack-contrib' @@ -21,7 +21,9 @@ gem 'gh', github: 'rkh/gh' gem 'bunny' gem 'dalli' gem 'pry' -gem 'metriks', '0.9.9.2' +gem 'metriks', '0.9.9.2' + +gem 'ar-octopus', github: 'travis-ci/octopus', require: nil group :test do gem 'rspec', '~> 2.11' @@ -34,9 +36,10 @@ group :development do gem 'foreman' gem 'rerun' # gem 'debugger' + gem 'rb-fsevent', '~> 0.9.1' end group :development, :test do gem 'rake', '~> 0.9.2' - gem 'micro_migrations', git: 'http://gist.github.com/4269321.git' + gem 'micro_migrations', git: 'https://gist.github.com/4269321.git' end diff --git a/Gemfile.lock b/Gemfile.lock index e0bba5fe..1bfbf9ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ GIT remote: git://github.com/getsentry/raven-ruby.git - revision: a0f59d3974034b38891fb1d4a70ff45fa996e7c9 + revision: 267b33417a3ed43f552911cf353561a641ba9fb2 specs: - sentry-raven (0.4.0) + sentry-raven (0.4.6) faraday (>= 0.7.6) - hashie + hashie (>= 1.1.0) multi_json (~> 1.0) uuidtools @@ -17,11 +17,11 @@ GIT GIT remote: git://github.com/rkh/gh.git - revision: 1dece05c588c63e714520aae686589de1b3bcbd5 + revision: ff93c759591a66c9d5250cada5234d2adde95dd3 specs: - gh (0.9.1) + gh (0.11.1) addressable - backports (~> 2.3) + backports faraday (~> 0.8) multi_json (~> 1.0) net-http-persistent (>= 2.7) @@ -29,32 +29,11 @@ GIT GIT remote: git://github.com/rkh/yard-sinatra.git - revision: 3b1064eef407d2d288a5b96d258178a1e67b3b80 + revision: e61831bca0431b35eaa62fdd18acbc65f81322af specs: yard-sinatra (1.0.0) yard (~> 0.7) -GIT - remote: git://github.com/sinatra/sinatra-contrib.git - revision: 86c85007860bbaf596092547e7902ff5e0a07698 - specs: - sinatra-contrib (1.4.0) - backports (>= 2.0) - eventmachine - rack-protection - rack-test - sinatra (~> 1.4.0) - tilt (~> 1.3) - -GIT - remote: git://github.com/sinatra/sinatra.git - revision: 459369eb66224836f72e21bbece58c007f3422fa - specs: - sinatra (1.4.0) - rack (~> 1.4) - rack-protection (~> 1.3) - tilt (~> 1.3, >= 1.3.3) - GIT remote: git://github.com/svenfuchs/simple_states.git revision: b1d45144e6a758220d7b21f83b08dc92de0d3196 @@ -64,23 +43,30 @@ GIT activesupport hashr (~> 0.0.10) +GIT + remote: git://github.com/travis-ci/octopus.git + revision: 2d4cca475479516f47c3144971205f50c335ad35 + specs: + ar-octopus (0.5.0beta) + activerecord (>= 2.3.0) + activesupport (>= 2.3.0) + GIT remote: git://github.com/travis-ci/travis-core.git - revision: 259e48ffc68a67eff32848334025ef17ab58a3b3 + revision: 9ecc95058bd63f9f70799c7f9f8ad41cbd39b16c specs: travis-core (0.0.1) - actionmailer (~> 3.2.11) - activerecord (~> 3.2.11) + actionmailer (~> 3.2.12) + activerecord (~> 3.2.12) coder (~> 0.3.0) data_migrations (~> 0.0.1) gh hashr (~> 0.0.19) metriks (~> 0.9.7) multi_json - postmark-rails (~> 0.4.1) pusher (~> 0.11.0) - railties (~> 3.2.11) - rake (~> 0.9.2.2) + railties (~> 3.2.12) + rake redis (~> 3.0) rollout (~> 1.1.0) simple_states (~> 0.1.1) @@ -96,12 +82,12 @@ GIT GIT remote: git://github.com/travis-ci/travis-support.git - revision: cf916e10949db43ce6f2b6f86082b367f04acfcd + revision: 5463f10e58563e79b950da8c6c392d1e80ec3013 specs: travis-support (0.0.1) GIT - remote: http://gist.github.com/4269321.git + remote: https://gist.github.com/4269321.git revision: 8e2d21b924a69dd48191df6a18e51769f5a88614 specs: micro_migrations (0.0.1) @@ -112,10 +98,10 @@ PATH travis-api (0.0.1) backports (~> 2.5) hubble (~> 0.1) - newrelic_rpm (~> 3.5.0) + newrelic_rpm (~> 3.6.1.88) pg (~> 0.13.2) rack-contrib (~> 1.1) - rack-ssl (~> 1.3) + rack-ssl (~> 1.3, >= 1.3.3) redcarpet (~> 2.1) sinatra (~> 1.3) sinatra-contrib (~> 1.3) @@ -124,73 +110,78 @@ PATH travis-support GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - actionmailer (3.2.11) - actionpack (= 3.2.11) - mail (~> 2.4.4) - actionpack (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) + actionmailer (3.2.13) + actionpack (= 3.2.13) + mail (~> 2.5.3) + actionpack (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) builder (~> 3.0.0) erubis (~> 2.7.0) journey (~> 1.0.4) - rack (~> 1.4.0) + rack (~> 1.4.5) rack-cache (~> 1.2) rack-test (~> 0.6.1) sprockets (~> 2.2.1) - activemodel (3.2.11) - activesupport (= 3.2.11) + activemodel (3.2.13) + activesupport (= 3.2.13) builder (~> 3.0.0) - activerecord (3.2.11) - activemodel (= 3.2.11) - activesupport (= 3.2.11) + activerecord (3.2.13) + activemodel (= 3.2.13) + activesupport (= 3.2.13) arel (~> 3.0.2) tzinfo (~> 0.3.29) - activesupport (3.2.11) - i18n (~> 0.6) + activesupport (3.2.13) + i18n (= 0.6.1) multi_json (~> 1.0) - addressable (2.3.2) + addressable (2.3.4) arel (3.0.2) - atomic (1.0.1) + atomic (1.1.9) avl_tree (1.1.3) - backports (2.7.1) + backports (2.8.2) builder (3.0.4) bunny (0.8.0) celluloid (0.12.4) facter (>= 1.6.12) timers (>= 1.0.0) coder (0.3.0) - coderay (1.0.8) + coderay (1.0.9) connection_pool (0.9.3) daemons (1.1.9) - dalli (2.6.0) + dalli (2.6.3) data_migrations (0.0.1) activerecord rake database_cleaner (0.8.0) - diff-lcs (1.1.3) + diff-lcs (1.2.4) + dotenv (0.7.0) erubis (2.7.0) - eventmachine (1.0.0) - facter (1.6.17) + eventmachine (1.0.3) + facter (1.7.0) factory_girl (2.4.2) activesupport - faraday (0.8.4) + faraday (0.8.7) multipart-post (~> 1.1) - foreman (0.61.0) + ffi (1.8.1) + foreman (0.63.0) + dotenv (>= 0.7) thor (>= 0.13.6) - hashie (1.2.0) + hashie (2.0.4) hashr (0.0.22) - hike (1.2.1) - hitimes (1.1.1) + hike (1.2.2) + hitimes (1.2.1) hubble (0.1.2) yajl-ruby (~> 1.1) i18n (0.6.1) journey (1.0.4) - json (1.7.6) - kgio (2.8.0) - listen (0.7.2) - mail (2.4.4) + json (1.7.7) + listen (1.0.3) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + mail (2.5.3) i18n (>= 0.4.0) mime-types (~> 1.16) treetop (~> 1.4.8) @@ -200,101 +191,108 @@ GEM atomic (~> 1.0) avl_tree (~> 1.1.2) hitimes (~> 1.1) - mime-types (1.19) - mocha (0.13.1) + mime-types (1.23) + mocha (0.13.3) metaclass (~> 0.0.1) - multi_json (1.5.0) - multipart-post (1.1.5) + multi_json (1.7.3) + multipart-post (1.2.0) net-http-persistent (2.8) net-http-pipeline (1.0.1) - newrelic_rpm (3.5.5.38) + newrelic_rpm (3.6.1.88) pg (0.13.2) polyglot (0.3.3) - postmark (0.9.18) - json - rake - postmark-rails (0.4.1) - actionmailer - postmark (>= 0.9.0) - rake - pry (0.9.11.4) + pry (0.9.12.1) coderay (~> 1.0.5) method_source (~> 0.8) slop (~> 3.4) - pusher (0.11.2) + puma (1.6.3) + rack (~> 1.2) + pusher (0.11.3) multi_json (~> 1.0) signature (~> 0.1.6) - rack (1.4.4) + rack (1.4.5) rack-cache (1.2) rack (>= 0.4) - rack-protection (1.3.2) + rack-protection (1.5.0) rack - rack-ssl (1.3.2) + rack-ssl (1.3.3) rack rack-test (0.6.2) rack (>= 1.0) - railties (3.2.11) - actionpack (= 3.2.11) - activesupport (= 3.2.11) + railties (3.2.13) + actionpack (= 3.2.13) + activesupport (= 3.2.13) rack-ssl (~> 1.3.2) rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.10.0) - rake (0.9.2.2) - rdoc (3.12) + rake (0.9.6) + rb-fsevent (0.9.3) + rb-inotify (0.9.0) + ffi (>= 0.5.0) + rb-kqueue (0.2.0) + ffi (>= 0.5.0) + rdoc (3.12.2) json (~> 1.4) redcarpet (2.2.2) - redis (3.0.2) - redis-namespace (1.2.1) + redis (3.0.4) + redis-namespace (1.3.0) redis (~> 3.0.0) - rerun (0.7.1) - listen + rerun (0.8.1) + listen (>= 1.0.3) rollout (1.1.0) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.1) + rspec (2.13.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.1) sidekiq (2.5.4) celluloid (~> 0.12.0) connection_pool (~> 0.9.2) multi_json (~> 1) redis (~> 3) redis-namespace - signature (0.1.6) - slop (3.4.3) + signature (0.1.7) + sinatra (1.3.6) + rack (~> 1.4) + rack-protection (~> 1.3) + tilt (~> 1.3, >= 1.3.3) + sinatra-contrib (1.3.2) + backports (>= 2.0) + eventmachine + rack-protection + rack-test + sinatra (~> 1.3.0) + tilt (~> 1.3) + slop (3.4.4) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - thin (1.5.0) + thin (1.5.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) thor (0.14.6) - tilt (1.3.3) + tilt (1.4.1) timers (1.1.0) treetop (1.4.12) polyglot polyglot (>= 0.3.1) - tzinfo (0.3.35) - unicorn (4.5.0) - kgio (~> 2.6) - rack - raindrops (~> 0.7) - uuidtools (2.1.3) + tzinfo (0.3.37) + uuidtools (2.1.4) yajl-ruby (1.1.0) - yard (0.8.3) + yard (0.8.6.1) PLATFORMS ruby DEPENDENCIES + ar-octopus! bunny dalli database_cleaner (~> 0.8.0) @@ -305,18 +303,19 @@ DEPENDENCIES micro_migrations! mocha (~> 0.12) pry + puma (= 1.6.3) rack-cache (~> 1.2) rack-contrib! rake (~> 0.9.2) + rb-fsevent (~> 0.9.1) rerun rspec (~> 2.11) sentry-raven! simple_states! - sinatra! - sinatra-contrib! + sinatra + sinatra-contrib travis-api! travis-core! travis-sidekiqs! travis-support! - unicorn yard-sinatra! diff --git a/Rakefile b/Rakefile index 5c5bce13..29502405 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,11 @@ require 'bundler/setup' ENV['SCHEMA'] = "#{Gem.loaded_specs['travis-core'].full_gem_path}/db/schema.rb" -require 'micro_migrations' +begin + require 'micro_migrations' +rescue LoadError + # we can't load micro migrations on production +end require 'travis' begin @@ -33,3 +37,6 @@ task 'travis-api.gemspec' do end task default: 'travis-api.gemspec' + +tasks_path = File.expand_path('../lib/tasks/*.rake', __FILE__) +Dir.glob(tasks_path).each { |r| import r } diff --git a/config.ru b/config.ru index 7806e127..8fb5947a 100644 --- a/config.ru +++ b/config.ru @@ -7,10 +7,12 @@ $stdout.sync = true require 'travis/api/app' require 'core_ext/module/load_constants' -# models = Travis::Model.constants.map(&:to_s) -# only = [/^(ActiveRecord|ActiveModel|Travis|GH|#{models.join('|')})/] -# [Travis::Api, Travis, GH].each do |target| -# target.load_constants! :only => only, :skip => ['Travis::Memory', 'GH::ResponseWrapper'], :debug => false -# end +models = Travis::Model.constants.map(&:to_s) +only = [/^(ActiveRecord|ActiveModel|Travis|GH|#{models.join('|')})/] +skip = ['Travis::Memory', 'GH::ResponseWrapper', 'Travis::NewRelic'] + +[Travis::Api, Travis, GH].each do |target| + target.load_constants! :only => only, :skip => skip, :debug => false +end run Travis::Api::App.new diff --git a/lib/tasks/build_update_branch.rake b/lib/tasks/build_update_branch.rake new file mode 100644 index 00000000..55a4575b --- /dev/null +++ b/lib/tasks/build_update_branch.rake @@ -0,0 +1,23 @@ +namespace :build do + namespace :migrate do + task :branch do + require 'travis' + Travis::Database.connect + + + Build.select(['id', 'commit_id']).pushes.includes(:commit).find_in_batches do |builds| + branches = Hash.new { |h, k| h[k] = [] } + + builds.each do |build| + #next if build.branch + branches[build.commit.branch] << build.id + end + + branches.each do |branch, ids| + Build.where(id: ids).update_all(branch: branch) + end + end; nil + + end + end +end diff --git a/lib/tasks/build_update_pull_request_data.rake b/lib/tasks/build_update_pull_request_data.rake new file mode 100644 index 00000000..0fbcd681 --- /dev/null +++ b/lib/tasks/build_update_pull_request_data.rake @@ -0,0 +1,22 @@ +namespace :build do + namespace :migrate do + task :pull_request_data do + require 'travis' + Travis::Database.connect + + Build.pull_requests.includes(:request).order('id DESC').find_in_batches do |builds| + Build.transaction do + builds.each do |build| + next if build.pull_request_number && build.pull_request_title + attrs = { + :pull_request_number => build.request.pull_request_number, + :pull_request_title => build.request.pull_request_title + } + + Build.where(id: build.id).update_all(attrs) + end + end + end + end + end +end diff --git a/lib/travis/api/app.rb b/lib/travis/api/app.rb index bedd863f..68ed7981 100644 --- a/lib/travis/api/app.rb +++ b/lib/travis/api/app.rb @@ -52,7 +52,11 @@ module Travis::Api end def self.deploy_sha - @deploy_sha ||= File.exist?('.deploy_sha') ? File.read('.deploy-sha')[0..7] : 'deploy-sha' + @deploy_sha ||= File.exist?(deploy_sha_path) ? File.read(deploy_sha_path)[0..7] : 'deploy-sha' + end + + def self.deploy_sha_path + File.expand_path('../../../../.deploy-sha', __FILE__) end attr_accessor :app @@ -70,8 +74,8 @@ module Travis::Api if Travis::Features.feature_active?(:use_rack_cache) && memcache_server use Rack::Cache, verbose: true, - metastore: "memcached://#{memcache_servers}/#{self.class.deploy_sha}", - entitystore: "memcached://#{memcache_servers}/#{self.class.deploy_sha}" + metastore: "memcached://#{memcache_servers}/#{Travis::Api::App.deploy_sha}", + entitystore: "memcached://#{memcache_servers}/#{Travis::Api::App.deploy_sha}" end use Rack::Deflater @@ -100,6 +104,10 @@ module Travis::Api private + def self.console? + defined? Travis::Console + end + def self.setup! setup_travis load_endpoints @@ -109,18 +117,50 @@ module Travis::Api def self.setup_travis Travis::Amqp.config = Travis.config.amqp - Travis::Database.connect + + setup_database_connections + Travis::Features.start - Sidekiq.configure_client do |config| - config.redis = Travis.config.redis.merge(size: 1, namespace: Travis.config.sidekiq.namespace) + + if Travis.env == 'production' || Travis.env == 'staging' + Sidekiq.configure_client do |config| + config.redis = Travis.config.redis.merge(size: 1, namespace: Travis.config.sidekiq.namespace) + end end - Raven.configure do |config| - config.dsn = Travis.config.sentry.dsn - end if Travis.config.sentry + if Travis.env == 'production' and not console? + Raven.configure do |config| + config.dsn = Travis.config.sentry.dsn + end if Travis.config.sentry - Travis::LogSubscriber::ActiveRecordMetrics.attach - $metriks_reporter = Metriks::Reporter::Logger.new + Travis::LogSubscriber::ActiveRecordMetrics.attach + Travis::Notification.setup + end + end + + def self.setup_database_connections + Travis::Database.connect + + return unless Travis.config.use_database_follower? + require 'octopus' + + if Travis.env == 'production' || Travis.env == 'staging' + puts "Setting up the DB follower as a read slave" + + # Octopus checks for Rails.env, just hardcode enabled? + Octopus.instance_eval do + def enabled? + true + end + end + + ActiveRecord::Base.custom_octopus_connection = false + + ::Octopus.setup do |config| + config.shards = { :follower => Travis.config.database_follower } + config.environments = ['production', 'staging'] + end + end end def self.load_endpoints diff --git a/lib/travis/api/app/access_token.rb b/lib/travis/api/app/access_token.rb index a8e324b9..99a70ad5 100644 --- a/lib/travis/api/app/access_token.rb +++ b/lib/travis/api/app/access_token.rb @@ -4,7 +4,7 @@ require 'securerandom' class Travis::Api::App class AccessToken DEFAULT_SCOPES = [:public, :private] - attr_reader :token, :scopes, :user_id, :app_id + attr_reader :token, :scopes, :user_id, :app_id, :expires_in, :extra def self.create(options = {}) new(options).tap(&:save) @@ -18,25 +18,40 @@ class Travis::Api::App def self.find_by_token(token) return token if token.is_a? self user_id, app_id, *scopes = redis.lrange(key(token), 0, -1) - new(token: token, scopes: scopes, user_id: user_id, app_id: app_id) if user_id + extra = decode_json(scopes.pop) if scopes.last && scopes.last =~ /^json:/ + new(token: token, scopes: scopes, user_id: user_id, app_id: app_id, extra: extra) if user_id end def initialize(options = {}) raise ArgumentError, 'must supply either user_id or user' unless options.key?(:user) ^ options.key?(:user_id) raise ArgumentError, 'must supply app_id' unless options.key?(:app_id) + begin + @expires_in = Integer(options[:expires_in]) if options[:expires_in] + rescue ArgumentError + raise ArgumentError, 'expires_in must be of integer type' + end + @app_id = Integer(options[:app_id]) @scopes = Array(options[:scopes] || options[:scope] || DEFAULT_SCOPES).map(&:to_sym) @user = options[:user] @user_id = Integer(options[:user_id] || @user.id) @token = options[:token] || reuse_token || SecureRandom.urlsafe_base64(16) + @extra = options[:extra] end def save key = key(token) redis.del(key) - redis.rpush(key, [user_id, app_id, *scopes].map(&:to_s)) + data = [user_id, app_id, *scopes] + data << encode_json(extra) if extra + redis.rpush(key, data.map(&:to_s)) redis.set(reuse_key, token) + + if expires_in + redis.expire(reuse_key, expires_in) + redis.expire(key, expires_in) + end end def user @@ -60,6 +75,14 @@ class Travis::Api::App def key(token) "t:#{token}" end + + def encode_json(hash) + 'json:' + Base64.encode64(hash.to_json) + end + + def decode_json(json) + JSON.parse(Base64.decode64(json.gsub(/^json:/, ''))) + end end include Helpers @@ -68,7 +91,7 @@ class Travis::Api::App private def reuse_token - redis.get(reuse_key) + redis.get(reuse_key) unless expires_in end def reuse_key diff --git a/lib/travis/api/app/base.rb b/lib/travis/api/app/base.rb index 4f61f6a6..4fbf40c8 100644 --- a/lib/travis/api/app/base.rb +++ b/lib/travis/api/app/base.rb @@ -1,6 +1,5 @@ require 'travis/api/app' require 'sinatra/base' -require 'new_relic/agent/instrumentation/rack' class Travis::Api::App # Superclass for any endpoint and middleware. @@ -18,6 +17,17 @@ class Travis::Api::App "This feature has not yet been implemented. Sorry :(\n\nPull Requests welcome!" end + # hotfix?? + def route_missing + @app ? forward : halt(404) + end + + def call(env) + super + rescue Sinatra::NotFound + [404, {'Content-Type' => 'text/plain'}, ['Tell Konstantin to fix this!']] + end + configure do # We pull in certain protection middleware in App. # Being token based makes us invulnerable to common diff --git a/lib/travis/api/app/endpoint.rb b/lib/travis/api/app/endpoint.rb index 2716ff13..11c82afc 100644 --- a/lib/travis/api/app/endpoint.rb +++ b/lib/travis/api/app/endpoint.rb @@ -10,7 +10,7 @@ class Travis::Api::App set(:prefix) { "/" << name[/[^:]+$/].underscore } set disable_root_endpoint: false register :scoping - helpers :current_user, :flash + helpers :current_user, :flash, :db_follower # TODO hmmm? before { flash.clear } diff --git a/lib/travis/api/app/endpoint/artifacts.rb b/lib/travis/api/app/endpoint/artifacts.rb index 43fb9040..cd35a570 100644 --- a/lib/travis/api/app/endpoint/artifacts.rb +++ b/lib/travis/api/app/endpoint/artifacts.rb @@ -4,10 +4,12 @@ class Travis::Api::App class Endpoint # Artifacts are generated by builds. Currently we only expose logs as # artifacts + # + # **DEPRECATED** will be removed as soon as the client uses /logs/:id class Artifacts < Endpoint # Fetches an artifact by it's *id*. get '/:id' do |id| - respond_with service(:find_artifact, params) + respond_with service(:find_log, params) end end end diff --git a/lib/travis/api/app/endpoint/authorization.rb b/lib/travis/api/app/endpoint/authorization.rb index eb5a1e57..f433cfc1 100644 --- a/lib/travis/api/app/endpoint/authorization.rb +++ b/lib/travis/api/app/endpoint/authorization.rb @@ -218,7 +218,7 @@ class Travis::Api::App end def user_for_github_token(token, drop_token = false) - data = GH.with(token: token.to_s) { GH['user'] } + data = GH.with(token: token.to_s, client_id: nil) { GH['user'] } scopes = parse_scopes data.headers['x-oauth-scopes'] halt 403, 'insufficient access: %p' unless acceptable? scopes diff --git a/lib/travis/api/app/endpoint/builds.rb b/lib/travis/api/app/endpoint/builds.rb index fe7b21c4..3ef22141 100644 --- a/lib/travis/api/app/endpoint/builds.rb +++ b/lib/travis/api/app/endpoint/builds.rb @@ -4,7 +4,8 @@ class Travis::Api::App class Endpoint class Builds < Endpoint get '/' do - respond_with service(:find_builds, params) + name = params[:branches] ? :find_branches : :find_builds + respond_with service(name, params) end get '/:id' do diff --git a/lib/travis/api/app/endpoint/documentation.rb b/lib/travis/api/app/endpoint/documentation.rb index f7feaaf6..7d054b1c 100644 --- a/lib/travis/api/app/endpoint/documentation.rb +++ b/lib/travis/api/app/endpoint/documentation.rb @@ -1,4 +1,5 @@ require 'travis/api/app' +require 'travis/api/app/endpoint/documentation/resources' class Travis::Api::App class Endpoint @@ -44,6 +45,7 @@ class Travis::Api::App def with_code_highlighting(str) str. + gsub(/json\(:([^)]+)\)/) { "
" + Resources::Helpers.json($1) + "
" }. gsub('/, ''). gsub(/TODO:?/, 'TODO') @@ -81,154 +83,116 @@ __END__ - + Travis API documentation - - - - - - - + + + + + + - - - Fork me on GitHub - - -
-
-
-

The Travis API

-

All the routes, just waiting for you to build something awesome.

-
+ -
+ - - -
- - <% general_docs.each do |doc| %> - <%= erb :entry, locals: doc %> - <% end %> - - <% endpoints.each do |endpoint| %> - <%= erb :entry, {}, - id: endpoint['name'], - title: endpoint['name'], - content: erb(:endpoint_content, {}, endpoint: endpoint) %> - <% end %> - -
+
+
+
+ + @@ endpoint_content <% unless endpoint['doc'].to_s.empty? %> <%= docs_for endpoint %> -
<% end %> <% endpoint['routes'].each do |route| %>
-

<%= route['verb'] %> <%= route['uri'] %>

+

<%= route['verb'] %> <%= route['uri'] %>

<% if route['scope'] %>

-

Required autorization scope: <%= route['scope'] %>
+
Required authorization scope: <%= route['scope'] %>

<% end %> <%= docs_for route %> @@ -237,11 +201,7 @@ __END__ @@ entry
- +

<%= title %> #

<%= content %>
diff --git a/lib/travis/api/app/endpoint/documentation/css/style.css b/lib/travis/api/app/endpoint/documentation/css/style.css new file mode 100644 index 00000000..ee4513a8 --- /dev/null +++ b/lib/travis/api/app/endpoint/documentation/css/style.css @@ -0,0 +1,594 @@ +/* ---( = begin global reset thanks to eric meyer elements )------------------------------- */ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, font, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; +} + +/* remember to define focus styles! */ +:focus { + outline: 0; +} + +body { + line-height: 1; + color: black; + background: white; +} + +ol, ul { + list-style: none; +} + +/* tables still need 'cellspacing="0"' in the markup */ +table { + border-collapse: separate; + border-spacing: 0; +} + +caption, th, td { + text-align: left; + font-weight: normal; +} + +blockquote:before, blockquote:after, q:before, q:after { + content: ""; +} + +blockquote, q { + quotes: "" ""; +} + +/* travis-ci styles */ + +body { + margin: 0 0 1em 0; + font-size: 14px; + line-height: 1.4286; + color: #555; + background: #fff; + font-family: "Helvetica Neue", Arial, Verdana, sans-serif; +} + +a { + color: #3366cc; + outline: none; + text-decoration: underline; +} + +a:visited { + color: #666; + +} + +a:hover { + color: #66cc33; + text-decoration: none; +} + +p, ul, blockquote, pre, td, th, label { + margin: 1.4286em 0; + font-size: 1em; + line-height: 1.4286; +} + +blockquote { + font-style: italic; + margin-left: 1em; +} + +blockquote small.author { + font-style: normal; + margin-top: 10px; + text-align: right; +} + +blockquote small.author:after { + content: ":"; +} + +p small.author { + margin: 0; +} + +ul, ol { + margin: 1.4286em 0; + text-align: left; +} + +li { + line-height: 1.4286; +} + +table { + border-collapse: collapse; + margin-bottom: 1.5em; +} + +strong { + font-family: Helvetica, Arial; + color: #8e7a2b; + font-weight: bold; +} + +em { + font-style: italic; +} +span.help { + font-style: italic; + background-color: #ffff99; + font-family: Georgia, Times, Serif; +} + +pre { + margin-top: 1em; + padding: 1em 1.5em; + line-height: 1.5em; + border: 1px solid #ddd; + background: #fafafa; + border-bottom-left-radius: 8px 8px; + border-bottom-right-radius: 8px 8px; + border-top-left-radius: 8px 8px; + border-top-right-radius: 8px 8px; + font-family: monospace; + font-size: 13px; + overflow-x: scroll; +} + +p > code, div > code, li > code { + background-color: #fafafa; + border: 1px solid #e0e0e0; + color: #333; + padding: 0px 0.2em; + font-family: monospace; + font-size: 13.3px; +} + +.wrapper { + width: 960px; + margin: 0 auto; + text-align: left; + overflow: hidden; + position: relative; +} + +div#navigation { + clear: both; + margin: 0 0 0 0; + padding: .8em 0 .6em 0; + overflow: hidden; + background: #efefef; + color: #888; + border-bottom: 1px solid #ccc; +} + +div#header { + clear: both; + margin: 1em auto 0 auto; + padding-top: 10px; + text-align: center; + overflow: hidden; +} + +div#navigation .wrapper { + width: 940px; +} + +div#navigation .wrapper a#logo { + color: #828282; + display:block; + float: left; + font-weight: bold; + font-size: 1.1em; + line-height: 1.2em; + margin: 0.15em 1em 0 0; + padding: 0 1em 0 0; + text-decoration: none; +} + +div#navigation .wrapper a#logo span { + color: #bbbaba; + font-size: .85em; +} + +div#navigation ul { + margin:0; + padding:0; +} + +div#navigation ul li { + margin: 0; + padding: 0; + float: left; + border-left: 1px solid #ccc; +} + +div#navigation ul li.right { + float: right; + border-left: none; + font-size: .83em; +} + +div#navigation ul li a, +div#navigation ul li a:visited { + color: #999; + display:block; + padding: 0em 1em 0 1em ; + text-decoration:none; +} + +div#navigation ul li a:hover, +div#navigation ul li a:visited { + text-decoration:underline; +} + +div#navigation ul li.selected a, +div#navigation ul li.selected a:visited { + text-decoration: none; + color: #6c3; +} + +div#navigation ul li.active a, +div#navigation ul li.selected a:visited { + text-decoration: none; + color: #444; +} + +div#content { + clear: both; + margin: 0 auto 3em auto; + padding: 0 0 0; + text-align:center; + overflow: hidden; +} + +div#content div.pad { + margin-left: 20px; + margin-right: 20px; +} + +div#main { + float:left; + width: 580px; + overflow: hidden; + padding-top: 2em; +} + +#main ul, +#main ul li { + list-style-type: disc; +} + +#main ol, +#main ol li { + list-style-type: decimal; +} + +#main li { + margin-left: 1.4286em; +} + +.clear { + clear: both; +} + +#main figure { + clear: both !important; + width: 578px; + border: 1px solid #efefef; + text-align: center; + margin: 0px; + border-radius: 4px; +} + +#main figure figcaption { + background-color: #efefef; + text-align: left; + font-size: 80%; + padding: 5px 10px; +} + +#main figure img { + max-width: 570px; + margin: 4px; +} + +#main figure.left { + margin-bottom: 1.4286em; + margin-right: 1.4286em; + float: left; +} + +#main figure.right { + margin-bottom: 1.4286em; + margin-left: 1.4286em; + float: right; +} + +#main figure.smallest { + width: 138px; +} + +#main figure.smallest img { + width: 130px; +} + + +#main figure.smaller { + width: 208px; +} + +#main figure.smaller img { + width: 200px; +} + +#main figure.small { + width: 258px; +} + +#main figure.small img { + max-width: 250px; +} + +div#sidebar { + float:right; + width: 325px; + overflow: hidden; + padding-top: 0; +} + +#sidebar ul { + margin:0 0 1.5em 0; + padding:0; +} + +#sidebar p { + margin: 0; + padding: 0; +} + +#sidebar p, +#sidebar ul li p, +#sidebar ul li a, +#sidebar ul li a:visited { + display:block; + text-decoration:none; + padding:4px 0 3px 20px; + border-top: 1px solid #efefef; +} + +#sidebar ul li a:hover, +#sidebar ul li a:visited:hover { + text-decoration:underline; +} + +#sidebar ul li.selected a, +#sidebar ul li.selected a:visited { + color: #6c3; + background: #efefef; + margin-left: 0; + padding-left: 20px; +} + +#sidebar ul li ul { + border-bottom: 0; + list-style-type: disc; + margin-bottom: 0.5em; +} + +#sidebar ul li ul li { + border-bottom: 0; + list-style-type: disc; + margin-left: 1.5em; +} + +#sidebar ul li ul li a, +#sidebar ul li ul li a:visited, +#sidebar ul li.selected ul li a, +#sidebar ul li.selected ul li a:visited { + display:block; + text-decoration:none; + padding:0; + border-top: 0; + font-weight: normal; + color: #195190; +} + +#sidebar ul li ul li.selected a, +#sidebar ul li ul li.selected a:visited { + color: #444; + font-weight: normal; +} + +div#footer { + clear: both; + margin: 0 auto; + padding-top: 10px; + text-align: center; + overflow: hidden; + background: #efefef; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd +} + +div#footer .wrapper { + background: transparent; + width: 920px; +} + +div#footer div.box { + float: left; + width: 280px; + margin-right: 30px; + color: #888; +} + +div#footer div.last { + margin-right: 0; + margin-left: 15px; +} + +div#footer div.box ul li a, +div#footer div.box ul li a:visited { + color: #7d8997; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + font-weight: bold; + color: #40454F; +} + +h1 { + font-size: 3em; + line-height: 1em; + margin-bottom: 0.5em; + clear: both; +} + +h2 { + font-size: 1.6em; + margin-bottom: 1em; + line-height: 1em; + letter-spacing: -0.5px; + clear: both; +} + +.meta { + margin-top: -1.7em; + font-size: 80%; +} + +h3 { + font-size: 1.15em; + line-height: 1em; + margin-bottom: 0.5em; +} + +h4 { + font-size: 0.95em; + line-height: 1.25; + margin-bottom: 1.25em; + height: 1.25em; +} + +h1 img, h2 img, h3 img, h4 img, h5 img, h6 img { + margin: 0; +} + +h1 a, +h1 a:visited, +h2 a, +h2 a:visited, +h3 a, +h3 a:visited, +h4 a, +h4 a:visited, +h5 a, +h5 a:visited { + color: #464755; +} + +div#header h1 { + float: left; + width: 350px; + text-indent: -20000em; + background: transparent; + font-size:1px; + margin: 20px 0 0 20px; +} + +div#header h1 a, +div#header h1 a:visited, +div#header h1 a:hover, +div#header h1 a:visited:hover { + display: block; + width: 350px; + height: 75px; +} + +div#header h1.riddle { + float: none; + width: 100%; + text-indent: 0; + background: #fff; + color: #000; + font-size:4em; + margin: 20px 0 0 0; + padding:0; +} + +div#header h1.riddle a, +div#header h1.riddle a:visited, +div#header h1.riddle a:hover, +div#header h1.riddle a:visited:hover { + display: block; + width: 100%; + height: auto; + color: #000; + text-decoration: none; +} + +div#header p { + color: #777; + display: block; + font: italic 1.25em Georgia, Times, Serif; + line-height: 1.67em; + margin: 0.935em 0 1.87em 0; + padding: 0 0 1.25em 0; + border-bottom: 1px solid #ccc; +} + +div#sidebar h2 { + font-size: .9em; + letter-spacing: .5px; + text-transform:uppercase; + margin: 40px 20px 5px 20px; +} + +.highlight pre { + font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace; + padding: 0 1.5em; +} + +.highlight br { + display: none; +} + +.highlight { + background-color: #f8f8f8; + border: 1px solid silver; + font-family: 'Courier New', 'Terminal', monospace; + color: #100; + margin: 1.5em 0; +} + +div#content div.pad { + margin-left: 0; + margin-right: 0; +} + +div#navigation ul li.lang { + float: right; + border-left: none; + font-size: .83em; +} + +#dsq-global-toolbar ul li { + list-style-type: none; +} diff --git a/lib/travis/api/app/endpoint/documentation/resources.rb b/lib/travis/api/app/endpoint/documentation/resources.rb new file mode 100644 index 00000000..aefc1392 --- /dev/null +++ b/lib/travis/api/app/endpoint/documentation/resources.rb @@ -0,0 +1,141 @@ +require 'json' + +class Travis::Api::App::Endpoint + module Resources + module Helpers + def self.json(key) + JSON.pretty_generate(Resources.const_get(key.to_s.upcase)) + end + end + + REPOSITORY_KEY = { + 'public_key' => '-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAOcx131amMqIzm5+FbZz+DhIgSDbFzjKKpzaN5UWVCrLSc57z64xxTV6\nkaOTZmjCWz6WpaPkFZY+czfL7lmuZ/Y6UNm0vupvdZ6t27SytFFGd1/RJlAe89tu\nGcIrC1vtEvQu2frMLvHqFylnGd5Gy64qkQT4KRhMsfZctX4z5VzTAgMBAAE=\n-----END RSA PUBLIC KEY-----\n', + } + + REPOSITORY = { + 'repo' => { + 'id' => 119756, + 'slug' => 'travis-ci/travis-api', + 'description' => 'The public Travis API', + 'last_build_id' => 6347735, + 'last_build_number' => '468', + 'last_build_state' => 'started', + 'last_build_duration' => nil, + 'last_build_language' => nil, + 'last_build_started_at' => '2013-04-15T09:45:29Z', + 'last_build_finished_at' => nil, + } + } + + REPOSITORIES = { 'repos' => [ REPOSITORY['repo'] ] } + + SHORT_BUILD = { + 'id' => 6347735, + 'repository_id' => 119756, + 'commit_id' => 1873023, + 'number' => '468', + 'pull_request' => false, + 'pull_request_title' => nil, + 'pull_request_number' => nil, + 'config' => { + 'language' => 'ruby', + 'rvm' => [ + '1.9.3', + 'rbx-19mode', + 'jruby-19mode', + ], + 'before_script' => [ + 'RAILS_ENV=test rake db:create db:schema:load --trace', + ], + 'notifications' => { + 'irc' => 'irc.freenode.org#travis', + }, + 'matrix' => { + 'allow_failures' => [ + { + 'rvm' => 'rbx-19mode', + }, + { + 'rvm' => 'jruby-19mode', + }, + ], + }, + '.result' => 'configured', + }, + 'state' => 'passed', + 'started_at' => '2013-04-15T09:45:29Z', + 'finished_at' => '2013-04-15T09:49:42Z', + 'duration' => 489, + 'job_ids' => [ + 6347736, + 6347737, + 6347738, + ], + } + + COMMIT = { + 'id' => 1873023, + 'sha' => 'a18f211f6f921affd1ecd8c18691b40d9948aae5', + 'branch' => 'master', + 'message' => "Merge pull request #25 from henrikhodne/add-responses-to-documentation\n\nAdd responses to documentation", + 'committed_at' => '2013-04-15T09:44:31Z', + 'author_name' => 'Henrik Hodne', + 'author_email' => 'me@henrikhodne.com', + 'committer_name' => 'Henrik Hodne', + 'committer_email' => 'me@henrikhodne.com', + 'compare_url' => 'https://github.com/travis-ci/travis-api/compare/0f31ff4fb6aa...a18f211f6f92', + 'pull_request_number' => nil, + } + + BUILDS = { + 'builds' => [ + SHORT_BUILD + ], + 'commits' => [ + COMMIT + ] + } + + JOB = { + 'id' => 6347736, + 'repository_id' => 119756, + 'build_id' => 6347735, + 'commit_id' => 1873023, + 'log_id' => 1219815, + 'state' => 'passed', + 'number' => '468.1', + 'config' => { + 'language' => 'ruby', + 'rvm' => '1.9.3', + 'before_script' => [ + 'RAILS_ENV=test rake db:create db:schema:load --trace', + ], + 'notifications' => { + 'irc' => 'irc.freenode.org#travis', + }, + 'matrix' => { + 'allow_failures' => [ + { + 'rvm' => 'rbx-19mode', + }, + { + 'rvm' => 'jruby-19mode', + } + ] + }, + '.result' => 'configured' + }, + 'started_at' => '2013-04-15T09:45:29Z', + 'finished_at' => '2013-04-15T09:48:14Z', + 'queue' => 'builds.linux', + 'allow_failure' => false, + 'tags' => '', + } + + BUILD = { + 'build' => SHORT_BUILD, + 'commit' => COMMIT, + 'jobs' => [ JOB ] + } + end +end diff --git a/lib/travis/api/app/endpoint/jobs.rb b/lib/travis/api/app/endpoint/jobs.rb index de02c291..966ef7cd 100644 --- a/lib/travis/api/app/endpoint/jobs.rb +++ b/lib/travis/api/app/endpoint/jobs.rb @@ -10,6 +10,31 @@ class Travis::Api::App get '/:id' do respond_with service(:find_job, params) end + + get '/:job_id/log' do + resource = service(:find_log, params).run + if !resource || resource.archived? + archived_log_path = archive_url("/jobs/#{params[:job_id]}/log.txt") + + if params[:cors_hax] + status 204 + headers['Access-Control-Expose-Headers'] = 'Location' + headers['Location'] = archived_log_path + else + redirect archived_log_path, 307 + end + else + respond_with resource + end + end + + def archive_url(path) + "https://s3.amazonaws.com/#{hostname('archive')}#{path}" + end + + def hostname(name) + "#{name}#{'-staging' if Travis.env == 'staging'}.#{Travis.config.host.split('.')[-2, 2].join('.')}" + end end end end diff --git a/lib/travis/api/app/endpoint/logs.rb b/lib/travis/api/app/endpoint/logs.rb new file mode 100644 index 00000000..83692be5 --- /dev/null +++ b/lib/travis/api/app/endpoint/logs.rb @@ -0,0 +1,21 @@ +require 'travis/api/app' + +class Travis::Api::App + class Endpoint + # Logs are generated by builds. + class Logs < Endpoint + # Fetches a log by it's *id*. + get '/:id' do |id| + respond_with service(:find_log, params) + end + + put '/:id' do |id| + # TODO @rkh ... rather lost in the auth/scopes code. + token = env['HTTP_TOKEN'] + halt 403, 'no token' unless token + halt 403, 'internal' unless token == Travis.config.tokens.internal + respond_with service(:update_log, params) + end + end + end +end diff --git a/lib/travis/api/app/endpoint/repos.rb b/lib/travis/api/app/endpoint/repos.rb index 7e9eb3fe..e1db9bb5 100644 --- a/lib/travis/api/app/endpoint/repos.rb +++ b/lib/travis/api/app/endpoint/repos.rb @@ -8,10 +8,19 @@ class Travis::Api::App # You can filter the repositories by adding parameters to the request. For example, you can get all repositories # owned by johndoe by adding `owner_name=johndoe`, or all repositories that johndoe has access to by adding # `member=johndoe`. The parameter names correspond to the keys of the response hash. + # + # ### Response + # + # json(:repositories) get '/' do respond_with service(:find_repos, params) end + # Gets the repository with the given id. + # + # ### Response + # + # json(:repository) get '/:id' do respond_with service(:find_repo, params) end @@ -20,6 +29,15 @@ class Travis::Api::App respond_with service(:find_repo, params.merge(schema: 'cc')) end + # Get the public key for the repository with the given id. + # + # This can be used to encrypt secure variables in the build configuration. See + # [the encryption keys](http://about.travis-ci.org/docs/user/encryption-keys/) documentation page for more + # information. + # + # ### Response + # + # json(:repository_key) get '/:id/key' do respond_with service(:find_repo_key, params), version: :v2 end @@ -28,14 +46,31 @@ class Travis::Api::App respond_with service(:regenerate_repo_key, params), version: :v2 end + # Gets the repository with the given name. + # + # ### Response + # + # json(:repository) get '/:owner_name/:name' do - respond_with service(:find_repo, params) + prefer_follower do + respond_with service(:find_repo, params) + end end + # Gets the builds for the repository with the given name. + # + # ### Response + # + # json(:builds) get '/:owner_name/:name/builds' do respond_with service(:find_builds, params) end + # Get a build with the given id in the repository with the given name. + # + # ### Response + # + # json(:build) get '/:owner_name/:name/builds/:id' do respond_with service(:find_build, params) end @@ -44,6 +79,15 @@ class Travis::Api::App respond_with service(:find_repo, params.merge(schema: 'cc')) end + # Get the public key for a given repository. + # + # This can be used to encrypt secure variables in the build configuration. See + # [the encryption keys](http://about.travis-ci.org/docs/user/encryption-keys/) documentation page for more + # information. + # + # ### Response + # + # json(:repository_key) get '/:owner_name/:name/key' do respond_with service(:find_repo_key, params), version: :v2 end diff --git a/lib/travis/api/app/endpoint/workers.rb b/lib/travis/api/app/endpoint/workers.rb index bb84d038..bd648785 100644 --- a/lib/travis/api/app/endpoint/workers.rb +++ b/lib/travis/api/app/endpoint/workers.rb @@ -4,11 +4,11 @@ class Travis::Api::App class Endpoint class Workers < Endpoint get '/' do - respond_with service(:find_workers, params) + respond_with service(:find_workers, params), type: :workers end get '/:id' do - respond_with service(:find_worker, params) + respond_with service(:find_worker, params), type: :worker end end end diff --git a/lib/travis/api/app/extensions/scoping.rb b/lib/travis/api/app/extensions/scoping.rb index 626bdbb2..5945804e 100644 --- a/lib/travis/api/app/extensions/scoping.rb +++ b/lib/travis/api/app/extensions/scoping.rb @@ -11,6 +11,16 @@ class Travis::Api::App def public? scope == :public end + + def required_params_match? + return true unless token = env['travis.access_token'] + + if token.extra && (required_params = token.extra['required_params']) + required_params.all? { |name, value| params[name] == value } + else + true + end + end end def self.registered(app) @@ -18,23 +28,35 @@ class Travis::Api::App app.helpers(Helpers) end - def scope(name) + def scope(*names) condition do - name = settings.default_scope if name == :default + names = [settings.default_scope] if names == [:default] scopes = env['travis.access_token'].try(:scopes) || settings.anonymous_scopes - headers['X-OAuth-Scopes'] = scopes.map(&:to_s).join(',') - headers['X-Accepted-OAuth-Scopes'] = name.to_s - if scopes.include? name - env['travis.scope'] = name - headers['Vary'] = 'Accept' - headers['Vary'] << ', Authorization' unless public? - true - elsif env['travis.access_token'] - pass { halt 403, "insufficient access" } - else - pass { halt 401, "no access token supplied" } + result = names.any? do |name| + if scopes.include?(name) && required_params_match? + headers['X-OAuth-Scopes'] = scopes.map(&:to_s).join(',') + headers['X-Accepted-OAuth-Scopes'] = name.to_s + + env['travis.scope'] = name + headers['Vary'] = 'Accept' + headers['Vary'] << ', Authorization' unless public? + true + end end + + if !result + headers['X-OAuth-Scopes'] = scopes.map(&:to_s).join(',') + headers['X-Accepted-OAuth-Scopes'] = names.first.to_s + + if env['travis.access_token'] + pass { halt 403, "insufficient access" } + else + pass { halt 401, "no access token supplied" } + end + end + + result end end diff --git a/lib/travis/api/app/helpers/accept.rb b/lib/travis/api/app/helpers/accept.rb index ed29d328..a57dfa8e 100644 --- a/lib/travis/api/app/helpers/accept.rb +++ b/lib/travis/api/app/helpers/accept.rb @@ -7,6 +7,76 @@ class Travis::Api::App DEFAULT_VERSION = 'v1' DEFAULT_FORMAT = 'json' + class Entry + SEPARATORS = Regexp.escape("()<>@,;:\/[]?={}\t ") + TOKEN = /[^#{SEPARATORS}]+/ + attr_reader :type, :subtype, :quality, :version, :params + def initialize(accept_string) + @type, @subtype, @quality, @version, @params = parse(accept_string) + end + + def <=>(other) + [1 - quality, mime_type.count('*'), 1 - params.size] <=> + [1 - other.quality, other.mime_type.count('*'), 1 - other.params.size] + end + + def mime_type + "#{type}/#{subtype}" + end + + def version + version = @version || params['version'] + version ? "v#{version}" : nil + end + + def accepts?(mime_type) + return true if self.mime_type == '*/*' + + type, subtype = mime_type.scan(%r{(#{TOKEN})/(#{TOKEN})}).flatten + type == self.type && (self.subtype == '*' || subtype == self.subtype) + end + + def to_s + str = "#{mime_type}; q=#{quality}" + str << "; #{params.map { |k,v| "#{k}=#{v}" }.join('; ')}" if params.length > 0 + str + end + + private + def parse(str) + # this handles only subset of what Accept header can + # contain, only the simplest cases, no quoted strings etc. + type, subtype, params = str.scan(%r{(#{TOKEN})/(#{TOKEN})(.*)}).flatten + quality = 1 + + version = nil + if params + params = Hash[*params.split(';').map { |p| p.scan /(#{TOKEN})=(#{TOKEN})/ }.flatten] + quality = params.delete('q').to_f if params['q'] + end + + if subtype =~ HEADER_FORMAT + subtype = $2 + version = $1 + end + + [type, subtype, quality, version, params] + end + end + + def accept_entries + entries = env['HTTP_ACCEPT'].to_s.delete(' ').to_s.split(',').map { |e| Entry.new(e) } + entries.empty? ? [Entry.new('*/*')] : entries.sort + end + + def acceptable_formats + if format = env['travis.format_from_path'] + [Entry.new(Rack::Mime.mime_type(".#{format}"))] + else + accept_entries + end + end + def accept_version @accept_version ||= request.accept.join =~ HEADER_FORMAT && "v#{$1}" || DEFAULT_VERSION end diff --git a/lib/travis/api/app/helpers/db_follower.rb b/lib/travis/api/app/helpers/db_follower.rb new file mode 100644 index 00000000..1bd654e4 --- /dev/null +++ b/lib/travis/api/app/helpers/db_follower.rb @@ -0,0 +1,17 @@ +require 'travis/api/app' + +class Travis::Api::App + module Helpers + module DbFollower + def prefer_follower + if Travis.config.use_database_follower? + Octopus.using(:follower) do + yield + end + else + yield + end + end + end + end +end diff --git a/lib/travis/api/app/helpers/respond_with.rb b/lib/travis/api/app/helpers/respond_with.rb index cfb1cc44..19f4c4fd 100644 --- a/lib/travis/api/app/helpers/respond_with.rb +++ b/lib/travis/api/app/helpers/respond_with.rb @@ -6,11 +6,12 @@ class Travis::Api::App # convert (in addition to the return values supported by Sinatra, of # course). These values will be encoded in JSON. module RespondWith + include Accept + def respond_with(resource, options = {}) - options[:format] ||= env['travis.format'] result = respond(resource, options) - result = result ? result.to_json : 404 - halt result + result = result.to_json if result && response.content_type =~ /application\/json/ + halt result || 404 end def body(value = nil, options = {}, &block) @@ -21,15 +22,34 @@ class Travis::Api::App private def respond(resource, options) - responders(resource, options).each do |const| - responder = const.new(self, resource, options) - resource = responder.apply if responder.apply? + resource = apply_service_responder(resource, options) + + response = nil + acceptable_formats.find do |accept| + responders(resource, options).find do |const| + responder = const.new(self, resource, options.dup.merge(accept: accept)) + response = responder.apply if responder.apply? + end end + + if responders = options[:responders] + responders.each do |klass| + responder = klass.new(self, response, options) + response = responder.apply if responder.apply? + end + end + + response || (resource ? error(406) : error(404)) + end + + def apply_service_responder(resource, options) + responder = Responders::Service.new(self, resource, options) + resource = responder.apply if responder.apply? resource end def responders(resource, options) - [:Service, :Json, :Image, :Xml, :Plain].map do |name| + [:Json, :Image, :Xml, :Plain].map do |name| Responders.const_get(name) end end diff --git a/lib/travis/api/app/middleware/rewrite.rb b/lib/travis/api/app/middleware/rewrite.rb index 3215602b..8ac8b9f2 100644 --- a/lib/travis/api/app/middleware/rewrite.rb +++ b/lib/travis/api/app/middleware/rewrite.rb @@ -24,6 +24,7 @@ class Travis::Api::App def extract_format env['PATH_INFO'].sub!(FORMAT, '') + env['travis.format_from_path'] = $1 env['travis.format'] = $1 || accept_format end @@ -40,6 +41,7 @@ class Travis::Api::App end def force_redirect(path) + path += "?#{request.query_string}" unless request.query_string.empty? response.body = '' response['Content-Length'] = '0' response['Content-Type'] = '' diff --git a/lib/travis/api/app/responders/base.rb b/lib/travis/api/app/responders/base.rb index 14f5df38..5f463d9c 100644 --- a/lib/travis/api/app/responders/base.rb +++ b/lib/travis/api/app/responders/base.rb @@ -31,5 +31,23 @@ module Travis::Api::App::Responders def headers endpoint.headers end + + def apply + endpoint.content_type content_type + end + + def apply? + resource && acceptable_format? + end + + def format + self.class.name.split('::').last.downcase + end + + def acceptable_format? + if accept = options[:accept] + accept.accepts?(Rack::Mime.mime_type(".#{format}")) + end + end end end diff --git a/lib/travis/api/app/responders/image.rb b/lib/travis/api/app/responders/image.rb index 2c4c4f69..c6c6a17d 100644 --- a/lib/travis/api/app/responders/image.rb +++ b/lib/travis/api/app/responders/image.rb @@ -1,18 +1,22 @@ module Travis::Api::App::Responders class Image < Base - def apply? - options[:format] == 'png' + def format + 'png' end def apply headers['Pragma'] = "no-cache" headers['Expires'] = Time.now.utc.httpdate headers['Content-Disposition'] = %(inline; filename="#{File.basename(filename)}") - halt send_file(filename, type: :png) + send_file(filename, type: :png, last_modified: last_modified) end private + def content_type + 'image/png' + end + def filename "#{root}/public/images/result/#{result}.png" end @@ -24,5 +28,10 @@ module Travis::Api::App::Responders def root File.expand_path('.') # TODO wat. end + + def last_modified + resource ? resource.last_build_finished_at : nil + end + end end diff --git a/lib/travis/api/app/responders/json.rb b/lib/travis/api/app/responders/json.rb index 8ad49337..012b2be8 100644 --- a/lib/travis/api/app/responders/json.rb +++ b/lib/travis/api/app/responders/json.rb @@ -4,21 +4,54 @@ class Travis::Api::App include Helpers::Accept def apply? - options[:format] == 'json' && !resource.is_a?(String) && !resource.nil? + super && !resource.is_a?(String) && !resource.nil? && accepts_log? end def apply - halt result.to_json + super + + result end private + def content_type + 'application/json;charset=utf-8' + end + + def accepts_log? + return true unless resource.is_a?(Log) + + chunked = accept_params[:chunked] + chunked ? !resource.aggregated_at : true + end + def result - builder ? builder.new(resource, request.params).data : resource + builder ? builder.new(resource, params).data : basic_type_resource end def builder - @builder ||= Travis::Api.builder(resource, { :version => accept_version }.merge(options)) + if defined?(@builder) + @builder + else + @builder = Travis::Api.builder(resource, { :version => version }.merge(options)) + end + end + + def accept_params + (options[:accept].params || {}).symbolize_keys + end + + def version + options[:accept].version || Travis::Api::App::Helpers::Accept::DEFAULT_VERSION + end + + def params + (request.params || {}).merge(accept_params) + end + + def basic_type_resource + resource if resource.is_a?(Hash) end end end diff --git a/lib/travis/api/app/responders/plain.rb b/lib/travis/api/app/responders/plain.rb index 89328c96..63d3d223 100644 --- a/lib/travis/api/app/responders/plain.rb +++ b/lib/travis/api/app/responders/plain.rb @@ -1,26 +1,35 @@ module Travis::Api::App::Responders class Plain < Base + def format + 'txt' + end + def apply? - # make sure that we don't leak anything by processing only Artifact::Log + # make sure that we don't leak anything by processing only Log # instances here. I don't want to create entire new API builder just # for log's content for now. # # TODO: think how to handle other formats correctly - options[:format] == 'txt' && resource.is_a?(Artifact::Log) + super && resource.is_a?(Log) end def apply + super + filename = resource.id disposition = params[:attachment] ? 'attachment' : 'inline' headers['Content-Disposition'] = %(#{disposition}; filename="#{filename}") - endpoint.content_type 'text/plain' - halt(params[:deansi] ? clear_ansi(resource.content) : resource.content) + params[:deansi] ? clear_ansi(resource.content) : resource.content end private + def content_type + 'text/plain' + end + def clear_ansi(content) content.gsub(/\r\r/, "\r") .gsub(/^.*\r(?!$)/, '') diff --git a/lib/travis/api/app/responders/xml.rb b/lib/travis/api/app/responders/xml.rb index b6a075a6..836d5f20 100644 --- a/lib/travis/api/app/responders/xml.rb +++ b/lib/travis/api/app/responders/xml.rb @@ -16,19 +16,25 @@ module Travis::Api::App::Responders } def apply? - options[:format] == 'xml' + super && resource.is_a?(Repository) end def apply - halt TEMPLATE % data + super + + TEMPLATE % data end private + def content_type + 'application/xml;charset=utf-8' + end + def data { name: resource.slug, - url: [Travis.config.domain, resource.slug].join('/'), + url: File.join("https://", Travis.config.client_domain, resource.slug), activity: activity, label: last_build.try(:number), status: status, diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..cf16e253 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/result/error.png b/public/images/result/error.png new file mode 100644 index 00000000..5e8f4b4c Binary files /dev/null and b/public/images/result/error.png differ diff --git a/public/images/result/failing.png b/public/images/result/failing.png index 46eda84e..ed9087ed 100644 Binary files a/public/images/result/failing.png and b/public/images/result/failing.png differ diff --git a/public/images/result/passing.png b/public/images/result/passing.png index 1d1bbe39..2d1cc758 100644 Binary files a/public/images/result/passing.png and b/public/images/result/passing.png differ diff --git a/public/images/result/pending.png b/public/images/result/pending.png new file mode 100644 index 00000000..82620d7f Binary files /dev/null and b/public/images/result/pending.png differ diff --git a/public/images/result/unknown.png b/public/images/result/unknown.png index cb7abbda..6f3e3e87 100644 Binary files a/public/images/result/unknown.png and b/public/images/result/unknown.png differ diff --git a/script/server b/script/server index cf218d5f..a17c4245 100755 --- a/script/server +++ b/script/server @@ -3,6 +3,6 @@ cd "$(dirname "$0")/.." [ $PORT ] || PORT=3000 [ $RACK_ENV ] || RACK_ENV=development -cmd="ruby -I lib -S bundle exec ruby -I lib -S unicorn config.ru -c config/unicorn.rb -p $PORT -E $RACK_ENV" -[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -l 127.0.0.1:$PORT" +cmd="ruby -I lib -S bundle exec ruby -I lib -S puma config.ru -p $PORT -e $RACK_ENV --threads 0:16" +[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -b tcp://127.0.0.1:$PORT" exec $cmd diff --git a/spec/integration/formats_handling_spec.rb b/spec/integration/formats_handling_spec.rb new file mode 100644 index 00000000..efda70b5 --- /dev/null +++ b/spec/integration/formats_handling_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'App' do + before do + FactoryGirl.create(:test, :number => '3.1', :queue => 'builds.common') + + add_endpoint '/foo' do + get '/' do + respond_with(Log.first) + end + + get '/hash' do + respond_with foo: 'bar' + end + end + end + + it 'gives priority to format given the url' do + response = get '/foo.txt', {}, 'HTTP_ACCEPT' => 'application/json' + response.content_type.should =~ /^text\/plain/ + end + + it 'responds with first available type' do + response = get '/foo', {}, 'HTTP_ACCEPT' => 'image/jpeg, application/json' + response.content_type.should =~ /^application\/json/ + end + + it 'responds with 406 if server can\'t use any mime type' do + response = get '/foo/hash', {}, 'HTTP_ACCEPT' => 'text/plain, image/jpeg' + response.status.should == 406 + end +end diff --git a/spec/integration/responders_spec.rb b/spec/integration/responders_spec.rb new file mode 100644 index 00000000..2dc2cb78 --- /dev/null +++ b/spec/integration/responders_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe 'App' do + before do + FactoryGirl.create(:test, :number => '3.1', :queue => 'builds.common') + + responder = Class.new(Travis::Api::App::Responders::Base) do + def apply? + true + end + + def apply + resource[:extra] = 'moar!' + + resource + end + end + + add_endpoint '/foo' do + get '/hash' do + respond_with({ foo: 'bar' }, responders: [responder]) + end + end + end + + it 'runs responder when rendering the response with respond_with' do + response = get '/foo/hash', {}, 'HTTP_ACCEPT' => 'application/json' + JSON.parse(response.body).should == { 'foo' => 'bar', 'extra' => 'moar!' } + end +end diff --git a/spec/integration/scopes_spec.rb b/spec/integration/scopes_spec.rb new file mode 100644 index 00000000..f06fd4db --- /dev/null +++ b/spec/integration/scopes_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'App' do + before do + FactoryGirl.create(:test, :number => '3.1', :queue => 'builds.common') + + add_endpoint '/foo' do + get '/:id/bar', scope: [:foo, :bar] do + respond_with foo: 'bar' + end + + get '/:job_id/log' do + respond_with job_id: params[:job_id] + end + end + end + + it 'checks if token has one of the required scopes' do + token = Travis::Api::App::AccessToken.new(app_id: 1, user_id: 2, scopes: [:foo]).tap(&:save) + + response = get '/foo/1/bar', {}, 'HTTP_ACCEPT' => 'application/json; version=2', 'HTTP_AUTHORIZATION' => "token #{token.token}" + response.should be_successful + response.headers['X-Accepted-OAuth-Scopes'].should == 'foo' + + token = Travis::Api::App::AccessToken.new(app_id: 1, user_id: 2, scopes: [:bar]).tap(&:save) + + response = get '/foo/1/bar', {}, 'HTTP_ACCEPT' => 'application/json; version=2', 'HTTP_AUTHORIZATION' => "token #{token.token}" + response.should be_successful + response.headers['X-Accepted-OAuth-Scopes'].should == 'bar' + + token = Travis::Api::App::AccessToken.new(app_id: 1, user_id: 2, scopes: [:baz]).tap(&:save) + + response = get '/foo/1/bar', {}, 'HTTP_ACCEPT' => 'application/json; version=2', 'HTTP_AUTHORIZATION' => "token #{token.token}" + response.status.should == 404 + end + + it 'checks if required_params match the from the request' do + extra = { + required_params: { job_id: '10' } + } + token = Travis::Api::App::AccessToken.new(app_id: 1, user_id: 2, extra: extra).tap(&:save) + + response = get '/foo/10/log', {}, 'HTTP_ACCEPT' => 'application/json', 'HTTP_AUTHORIZATION' => "token #{token.token}" + response.should be_successful + + response = get '/foo/11/log', {}, 'HTTP_ACCEPT' => 'application/json', 'HTTP_AUTHORIZATION' => "token #{token.token}" + response.status.should == 403 + end +end diff --git a/spec/integration/v1/branches_spec.rb b/spec/integration/v1/branches_spec.rb index 07beef62..6e5c8735 100644 --- a/spec/integration/v1/branches_spec.rb +++ b/spec/integration/v1/branches_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Branches' do - before { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } diff --git a/spec/integration/v1/builds_spec.rb b/spec/integration/v1/builds_spec.rb index 3c1bca96..73d2fe85 100644 --- a/spec/integration/v1/builds_spec.rb +++ b/spec/integration/v1/builds_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Builds' do - before { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:build) { repo.builds.first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } diff --git a/spec/integration/v1/hooks_spec.rb b/spec/integration/v1/hooks_spec.rb index 35160330..ccf76a00 100644 --- a/spec/integration/v1/hooks_spec.rb +++ b/spec/integration/v1/hooks_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe 'Hooks' do before(:each) do - Scenario.default user.permissions.create repository: repo, admin: true end diff --git a/spec/integration/v1/repositories_spec.rb b/spec/integration/v1/repositories_spec.rb index 9d4da90b..b1b35fd1 100644 --- a/spec/integration/v1/repositories_spec.rb +++ b/spec/integration/v1/repositories_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'v1 repos' do - before(:each) { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } @@ -42,10 +40,6 @@ describe 'v1 repos' do end describe 'GET /svenfuchs/minimal.png' do - it '"unknown" when the repository does not exist' do - get('/svenfuchs/does-not-exist.png').should deliver_result_image_for('unknown') - end - it '"unknown" when it only has one build that is not finished' do Build.delete_all Factory(:build, repository: repo, state: :created, result: nil) @@ -74,10 +68,6 @@ describe 'v1 repos' do let(:on_foo) { Factory(:commit, branch: 'foo') } let(:on_bar) { Factory(:commit, branch: 'bar') } - it '"unknown" when the repository does not exist' do - get('/svenfuchs/does-not-exist.png?branch=foo,bar').should deliver_result_image_for('unknown') - end - it '"unknown" when it only has unfinished builds on the relevant branches' do Build.delete_all Factory(:build, repository: repo, state: :started, commit: on_foo) diff --git a/spec/integration/v1/workers_spec.rb b/spec/integration/v1/workers_spec.rb index 11bdf4ec..1c897384 100644 --- a/spec/integration/v1/workers_spec.rb +++ b/spec/integration/v1/workers_spec.rb @@ -1,21 +1,12 @@ require 'spec_helper' describe 'Workers' do - before(:each) do - Time.stubs(:now).returns(Time.utc(2011, 11, 11, 11, 11, 11)) - @workers = [ - Factory(:worker, :name => 'worker-1', :state => :working), - Factory(:worker, :name => 'worker-2', :state => :errored) - ] - end - - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } - - attr_reader :workers + let!(:workers) { [Worker.create(full_name: 'one'), Worker.create(full_name: 'two')] } + let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.1+json' } } it 'GET /workers' do response = get '/workers', {}, headers - response.should deliver_json_for(Worker.order(:host, :name), version: 'v1') + response.should deliver_json_for(Worker.all, version: 'v1', type: 'workers') end end diff --git a/spec/integration/v2/branches_spec.rb b/spec/integration/v2/branches_spec.rb index 6f52bbf7..3e54ecc8 100644 --- a/spec/integration/v2/branches_spec.rb +++ b/spec/integration/v2/branches_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Branches' do - before { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } diff --git a/spec/integration/v2/builds_spec.rb b/spec/integration/v2/builds_spec.rb index 1f948fb8..51eb82fb 100644 --- a/spec/integration/v2/builds_spec.rb +++ b/spec/integration/v2/builds_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Builds' do - before { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:build) { repo.builds.first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } @@ -31,4 +29,9 @@ describe 'Builds' do response = get "/repos/svenfuchs/minimal/builds/#{build.id}", {}, headers response.should deliver_json_for(build, version: 'v2') end + + it 'GET /builds/1?repository_id=1&branches=true' do + response = get "/builds?repository_id=#{repo.id}&branches=true", {}, headers + response.should deliver_json_for(repo.last_finished_builds_by_branches, version: 'v2') + end end diff --git a/spec/integration/v2/hooks_spec.rb b/spec/integration/v2/hooks_spec.rb index 37f1a931..c0a685de 100644 --- a/spec/integration/v2/hooks_spec.rb +++ b/spec/integration/v2/hooks_spec.rb @@ -3,7 +3,6 @@ require 'travis/testing/payloads' describe 'Hooks' do before(:each) do - Scenario.default user.permissions.create repository: repo, admin: true end diff --git a/spec/integration/v2/jobs_spec.rb b/spec/integration/v2/jobs_spec.rb index 4da7acae..4c7d202e 100644 --- a/spec/integration/v2/jobs_spec.rb +++ b/spec/integration/v2/jobs_spec.rb @@ -13,8 +13,66 @@ describe 'Jobs' do response.should deliver_json_for(Job.queued('builds.common'), version: 'v2') end - it '/jobs/:job_id' do + it '/jobs/:id' do response = get "/jobs/#{job.id}", {}, headers response.should deliver_json_for(job, version: 'v2') end + + context 'GET /jobs/:job_id/log.txt' do + it 'returns log for a job' do + job.log.update_attributes!(content: 'the log') + response = get "/jobs/#{job.id}/log.txt", {}, headers + response.should deliver_as_txt('the log', version: 'v2') + end + + context 'when log is archived' do + it 'redirects to archive' do + job.log.update_attributes!(content: 'the log', archived_at: Time.now, archive_verified: true) + response = get "/jobs/#{job.id}/log.txt", {}, headers + response.should redirect_to("https://s3.amazonaws.com/archive.travis-ci.org/jobs/#{job.id}/log.txt") + end + end + + context 'when log is missing' do + it 'redirects to archive' do + job.log.destroy + response = get "/jobs/#{job.id}/log.txt", {}, headers + response.should redirect_to("https://s3.amazonaws.com/archive.travis-ci.org/jobs/#{job.id}/log.txt") + end + end + + context 'with cors_hax param' do + it 'renders No Content response with location of the archived log' do + job.log.destroy + response = get "/jobs/#{job.id}/log.txt?cors_hax=true", {}, headers + response.status.should == 204 + response.headers['Location'].should == "https://s3.amazonaws.com/archive.travis-ci.org/jobs/#{job.id}/log.txt" + end + end + + context 'with chunked log requested' do + it 'responds with 406 when log is already aggregated' do + job.log.update_attributes(aggregated_at: Time.now) + headers = { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json; chunked=true' } + response = get "/jobs/#{job.id}/log", {}, headers + response.status.should == 406 + end + + it 'responds with chunks instead of full log' do + job.log.parts << Log::Part.new(content: 'foo', number: 1, final: false) + job.log.parts << Log::Part.new(content: 'bar', number: 2, final: true) + + headers = { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json; chunked=true' } + response = get "/jobs/#{job.id}/log", {}, headers + response.should deliver_json_for(job.log, version: 'v2', params: { chunked: true}) + end + + it 'responds with full log if chunks are not available and full log is accepted' do + job.log.update_attributes(aggregated_at: Time.now) + headers = { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json; chunked=true, application/vnd.travis-ci.2+json' } + response = get "/jobs/#{job.id}/log", {}, headers + response.should deliver_json_for(job.log, version: 'v2') + end + end + end end diff --git a/spec/integration/v2/repositories_spec.rb b/spec/integration/v2/repositories_spec.rb index fba8035d..114bf474 100644 --- a/spec/integration/v2/repositories_spec.rb +++ b/spec/integration/v2/repositories_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Repos' do - before(:each) { Scenario.default } - let(:repo) { Repository.by_slug('svenfuchs/minimal').first } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } @@ -76,6 +74,7 @@ describe 'Repos' do it 'GET /repos/1/cc.xml' do response = get "repos/#{repo.id}/cc.xml" response.should deliver_cc_xml_for(Repository.by_slug('svenfuchs/minimal').first) + response.content_type.should eq('application/xml;charset=utf-8') end it 'GET /repos/svenfuchs/minimal' do @@ -88,15 +87,26 @@ describe 'Repos' do response.should deliver_cc_xml_for(Repository.by_slug('svenfuchs/minimal').first) end + it 'does not respond with cc.xml for /repos list' do + response = get '/repos', {}, 'HTTP_ACCEPT' => 'application/xml; version=2' + response.status.should == 406 + end + + it 'responds with 404 when repo can\'t be found and format is png' do + result = get('/repos/foo/bar.png', {}, 'HTTP_ACCEPT' => 'image/png; version=2') + result.status.should == 404 + end + + it 'responds with 404 when repo can\'t be found and format is other than png' do + result = get('/repos/foo/bar', {}, 'HTTP_ACCEPT' => 'application/json; version=2') + result.status.should == 404 + JSON.parse(result.body).should == { 'file' => 'not found' } + end + describe 'GET /repos/svenfuchs/minimal.png?branch=foo,bar' do let(:on_foo) { Factory(:commit, branch: 'foo') } let(:on_bar) { Factory(:commit, branch: 'bar') } - it '"unknown" when the repository does not exist' do - result = get('/repos/svenfuchs/does-not-exist.png?branch=foo,bar', {}, headers) - result.should deliver_result_image_for('unknown') - end - it '"unknown" when it only has unfinished builds on the relevant branches' do Build.delete_all Factory(:build, repository: repo, state: :started, commit: on_foo) @@ -117,6 +127,7 @@ describe 'Repos' do Factory(:build, repository: repo, state: :passed, commit: on_bar) result = get('/repos/svenfuchs/minimal.png?branch=foo,bar', {}, headers) result.should deliver_result_image_for('passing') + result.headers['Last-Modified'].should == repo.last_build_finished_at.httpdate end it '"passing" when there is a running build but the previous one has passed' do diff --git a/spec/integration/v2/users_spec.rb b/spec/integration/v2/users_spec.rb index 945afa57..1653227e 100644 --- a/spec/integration/v2/users_spec.rb +++ b/spec/integration/v2/users_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'Users' do - let(:user) { Factory(:user, locale: 'en') } + let(:user) { User.first } let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: -1) } let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json', 'HTTP_AUTHORIZATION' => "token #{token}" } } @@ -17,6 +17,7 @@ describe 'Users' do context 'POST /users/sync' do it 'syncs current_user repos' do + user.update_attribute :is_syncing, false response = post "/users/sync", {}, headers response.should be_successful end diff --git a/spec/integration/v2/workers_spec.rb b/spec/integration/v2/workers_spec.rb index 17231394..120934a4 100644 --- a/spec/integration/v2/workers_spec.rb +++ b/spec/integration/v2/workers_spec.rb @@ -1,21 +1,12 @@ require 'spec_helper' describe 'Workers' do - before(:each) do - Time.stubs(:now).returns(Time.utc(2011, 11, 11, 11, 11, 11)) - @workers = [ - Factory(:worker, :name => 'worker-1', :state => :working), - Factory(:worker, :name => 'worker-2', :state => :errored) - ] - end - - let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } - - attr_reader :workers + let!(:workers) { [Worker.create(full_name: 'one'), Worker.create(full_name: 'two')] } + let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } it 'GET /workers' do response = get '/workers', {}, headers - response.should deliver_json_for(Worker.order(:host, :name), version: 'v2') + response.should deliver_json_for(Worker.all, version: 'v2', type: 'workers') end end diff --git a/spec/integration/version_spec.rb b/spec/integration/version_spec.rb new file mode 100644 index 00000000..818a0dbd --- /dev/null +++ b/spec/integration/version_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'App' do + before do + add_endpoint '/foo' do + get '/' do + respond_with foo: 'bar' + end + end + end + + it 'uses version from current accept header' do + Travis::Api.expects(:builder).with { |r, options| options[:version] == 'v1' } + + Travis::Api::App::Responders::Json.any_instance.stubs(:apply?). + returns(false).then.returns(true) + + response = get '/foo', {}, 'HTTP_ACCEPT' => 'application/json; version=2, application/json; version=1' + response.content_type.should == 'application/json;charset=utf-8' + end + + it 'uses v1 by default' do + Travis::Api.expects(:builder).with { |r, options| options[:version] == 'v1' } + get '/foo', {}, 'HTTP_ACCEPT' => 'application/json' + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index af4563da..44cd1061 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require 'support/matchers' Travis.logger = Logger.new(StringIO.new) Travis::Api::App.setup +Travis.config.client_domain = "www.example.com" module TestHelpers include Sinatra::TestHelpers @@ -41,7 +42,8 @@ RSpec.configure do |c| c.before :suite do DatabaseCleaner.strategy = :transaction - DatabaseCleaner.clean_with :transaction + DatabaseCleaner.clean_with :truncation + Scenario.default end c.before :each do diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 53cc97aa..798c9709 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -24,6 +24,26 @@ RSpec::Matchers.define :deliver_json_for do |resource, options = {}| end end +RSpec::Matchers.define :deliver_as_txt do |expected, options = {}| + match do |response| + if response.status == 200 + failure_message_for_should do + "expected\n\n#{actual}\n\nto equal\n\n#{expected}" + end + response.body.to_s == expected + else + failure_message_for_should do + "expected the request to be successful (200) but was #{response.status}" + end + false + end + end + + def parse(body) + MultiJson.decode(body) + end +end + RSpec::Matchers.define :deliver_result_image_for do |name| match do |response| header = response.headers['content-disposition'] @@ -42,7 +62,7 @@ RSpec::Matchers.define :deliver_cc_xml_for do |repo| "expected #{body} to be a valid cc.xml" end - body.include?('') && body.include?(%(name="#{repo.slug}")) + body.include?('') && body.include?(%(name="#{repo.slug}")) && body.include?("https://www.example.com/#{repo.slug}") end end diff --git a/spec/unit/access_token_spec.rb b/spec/unit/access_token_spec.rb new file mode 100644 index 00000000..3cb8794b --- /dev/null +++ b/spec/unit/access_token_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Travis::Api::App::AccessToken do + it 'errors out on wrong type of :expires_in argument' do + expect { + described_class.new(app_id: 1, user_id: 2, expires_in: 'foo') + }.to raise_error(ArgumentError, 'expires_in must be of integer type') + end + + it 'allows to skip expires_in' do + expect { + described_class.new(app_id: 1, user_id: 2, expires_in: nil) + }.to_not raise_error(ArgumentError) + end + + it 'does not reuse token if expires_in is set' do + token = described_class.new(app_id: 1, user_id: 2).tap(&:save) + new_token = described_class.new(app_id: 1, user_id: 2, expires_in: 10) + + token.token.should_not == new_token.token + end + + it 'expires the token after given period of time' do + token = described_class.new(app_id: 1, user_id: 2, expires_in: 1).tap(&:save) + + described_class.find_by_token(token.token).should_not be_nil + + sleep 2 + + described_class.find_by_token(token.token).should be_nil + end + + it 'allows to save extra information' do + attrs = { + app_id: 1, + user_id: 3, + expires_in: 1, + extra: { + required_params: { job_id: '1' } + } + } + + token = described_class.new(attrs).tap(&:save) + token.extra.should == attrs[:extra] + + token = described_class.find_by_token(token.token) + token.extra.should == { 'required_params' => { 'job_id' => '1' } } + end +end diff --git a/spec/unit/endpoint/accounts_spec.rb b/spec/unit/endpoint/accounts_spec.rb index 2277ea8d..ecae3c03 100644 --- a/spec/unit/endpoint/accounts_spec.rb +++ b/spec/unit/endpoint/accounts_spec.rb @@ -18,7 +18,7 @@ describe Travis::Api::App::Endpoint::Accounts do 'login' => user.login, 'name' => user.name, 'type' => 'user', - 'repos_count' => nil + 'repos_count' => 1 }] end end diff --git a/spec/unit/endpoint/authorization_spec.rb b/spec/unit/endpoint/authorization_spec.rb index 2f84ad19..7f1727f9 100644 --- a/spec/unit/endpoint/authorization_spec.rb +++ b/spec/unit/endpoint/authorization_spec.rb @@ -26,10 +26,10 @@ describe Travis::Api::App::Endpoint::Authorization do describe 'POST /auth/github' do before do data = { 'id' => user.github_id, 'name' => user.name, 'login' => user.login, 'gravatar_id' => user.gravatar_id } - GH.stubs(:with).with(token: 'private repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'repo'}, :to_hash => data) - GH.stubs(:with).with(token: 'public repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'public_repo'}, :to_hash => data) - GH.stubs(:with).with(token: 'no repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'user'}, :to_hash => data) - GH.stubs(:with).with(token: 'invalid token').raises(Faraday::Error::ClientError, 'CLIENT ERROR!') + GH.stubs(:with).with(token: 'private repos', client_id: nil).returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'repo'}, :to_hash => data) + GH.stubs(:with).with(token: 'public repos', client_id: nil).returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'public_repo'}, :to_hash => data) + GH.stubs(:with).with(token: 'no repos', client_id: nil).returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'user'}, :to_hash => data) + GH.stubs(:with).with(token: 'invalid token', client_id: nil).raises(Faraday::Error::ClientError, 'CLIENT ERROR!') end def get_token(github_token) diff --git a/spec/unit/helpers/accept_spec.rb b/spec/unit/helpers/accept_spec.rb new file mode 100644 index 00000000..cd41d0d3 --- /dev/null +++ b/spec/unit/helpers/accept_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +module Travis::Api::App::Helpers + describe Accept do + class FakeApp < Struct.new(:env) + include Accept + end + + it 'returns accept entries sorted properly' do + accept = "text/html; q=0.2; level=1, application/vnd.travis-ci.2+json, text/*, text/html;level=2; q=0.5" + FakeApp.new('HTTP_ACCEPT' => accept).accept_entries.map(&:to_s).should == + ["application/json; q=1", "text/*; q=1", "text/html; q=0.5; level=2", "text/html; q=0.2; level=1"] + end + + it 'properly parses params, quality and version' do + accept = "application/vnd.travis-ci.2+json; q=0.2; level=1; foo=bar" + accept_entry = FakeApp.new('HTTP_ACCEPT' => accept).accept_entries.first + accept_entry.quality.should == 0.2 + accept_entry.params.should == { 'level' => '1', 'foo' => 'bar' } + accept_entry.mime_type.should == 'application/json' + accept_entry.version.should == 'v2' + end + + it 'returns */* for empty accept header' do + accept_entry = FakeApp.new({}).accept_entries.first + accept_entry.mime_type.should == '*/*' + end + + describe Accept::Entry do + describe 'version' do + it 'can be passed as a vendor extension' do + entry = Accept::Entry.new('application/vnd.travis-ci.2+json') + entry.version.should == 'v2' + end + + it 'can be passed as a param' do + entry = Accept::Entry.new('application/json; version=2') + entry.version.should == 'v2' + end + + it 'has a higher priority when in vendor extension' do + entry = Accept::Entry.new('application/vnd.travis-ci.1+json; version=2') + entry.version.should == 'v1' + end + end + + describe 'accepts?' do + it 'accepts everything with */* type' do + entry = Accept::Entry.new('*/*') + entry.accepts?('application/json').should be_true + entry.accepts?('foo/bar').should be_true + end + + it 'accepts every subtype with application/* type' do + entry = Accept::Entry.new('application/*') + + entry.accepts?('application/foo').should be_true + entry.accepts?('application/bar').should be_true + entry.accepts?('text/plain').should be_false + end + + it 'accepts when type and subtype match' do + entry = Accept::Entry.new('application/json') + + entry.accepts?('application/json').should be_true + entry.accepts?('application/xml').should be_false + end + end + end + end +end diff --git a/spec/unit/responders/json_spec.rb b/spec/unit/responders/json_spec.rb new file mode 100644 index 00000000..5b43bbc1 --- /dev/null +++ b/spec/unit/responders/json_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +module Travis::Api::App::Responders + describe Json do + class MyJson < Json + end + + let(:request) { stub 'request', params: {} } + let(:endpoint) { stub 'endpoint', request: request, content_type: nil } + let(:resource) { stub 'resource' } + let(:accept) { stub 'accept entry', version: '2', params: {} } + let(:options) { { :accept => accept} } + let(:json) { MyJson.new(endpoint, resource, options) } + + context 'with resource not associated with Api data class' do + it 'returns nil result' do + json.apply.should be_false + end + end + + context 'with resource being' do + context 'a Hash instance' do + let(:resource) { { foo: 'bar' } } + + it 'returns resource converted to_json' do + json.apply.should == { foo: 'bar' } + end + end + + context 'nil' do + let(:resource) { nil } + + it 'responds with 404' do + json.apply?.should be_false + json.apply.should be_false + end + end + end + + context 'with resource associated with Api data class' do + let(:builder) { stub 'builder', data: { foo: 'bar' } } + let(:builder_class) { stub 'builder class', new: builder } + before do + json.stubs :builder => builder_class + end + + it 'returns proper data converted to json' do + json.apply.should == { foo: 'bar' } + end + end + end +end diff --git a/travis-api.gemspec b/travis-api.gemspec index 38449832..4fdfb215 100644 --- a/travis-api.gemspec +++ b/travis-api.gemspec @@ -12,9 +12,14 @@ Gem::Specification.new do |s| "Sven Fuchs", "Konstantin Haase", "Piotr Sarnacki", + "Henrik Hodne", "Mathias Meyer", + "Josh Kalderimis", + "Andre Arko", + "Erik Michaels-Ober", + "Steve Richert", "Brian Ford", - "Henrik Hodne" + "Nick Schonning" ] s.email = [ @@ -22,12 +27,18 @@ Gem::Specification.new do |s| "konstantin.mailinglists@googlemail.com", "drogus@gmail.com", "meyer@paperplanes.de", + "me@henrikhodne.com", "svenfuchs@artweb-design.de", + "josh.kalderimis@gmail.com", + "andre@arko.net", + "sferik@gmail.com", + "steve.richert@gmail.com", "bford@engineyard.com", - "me@henrikhodne.com" + "nschonni@gmail.com" ] s.files = [ + "CONTRIBUTING.md", "Procfile", "README.md", "Rakefile", @@ -37,6 +48,7 @@ Gem::Specification.new do |s| "config/unicorn.rb", "docs/00_overview.md", "docs/01_cross_origin.md", + "lib/tasks/build_update_pull_request_data.rake", "lib/travis/api/app.rb", "lib/travis/api/app/access_token.rb", "lib/travis/api/app/base.rb", @@ -54,6 +66,7 @@ Gem::Specification.new do |s| "lib/travis/api/app/endpoint/documentation/css/bootstrap.css", "lib/travis/api/app/endpoint/documentation/css/bootstrap.min.css", "lib/travis/api/app/endpoint/documentation/css/prettify.css", + "lib/travis/api/app/endpoint/documentation/css/style.css", "lib/travis/api/app/endpoint/documentation/img/glyphicons-halflings-white.png", "lib/travis/api/app/endpoint/documentation/img/glyphicons-halflings.png", "lib/travis/api/app/endpoint/documentation/img/grid-18px-masked.png", @@ -79,11 +92,13 @@ Gem::Specification.new do |s| "lib/travis/api/app/endpoint/documentation/js/lang-xq.js", "lib/travis/api/app/endpoint/documentation/js/lang-yaml.js", "lib/travis/api/app/endpoint/documentation/js/prettify.js", + "lib/travis/api/app/endpoint/documentation/resources.rb", "lib/travis/api/app/endpoint/endpoints.rb", "lib/travis/api/app/endpoint/events.rb", "lib/travis/api/app/endpoint/home.rb", "lib/travis/api/app/endpoint/hooks.rb", "lib/travis/api/app/endpoint/jobs.rb", + "lib/travis/api/app/endpoint/logs.rb", "lib/travis/api/app/endpoint/repos.rb", "lib/travis/api/app/endpoint/requests.rb", "lib/travis/api/app/endpoint/stats.rb", @@ -107,13 +122,18 @@ Gem::Specification.new do |s| "lib/travis/api/app/responders/base.rb", "lib/travis/api/app/responders/image.rb", "lib/travis/api/app/responders/json.rb", + "lib/travis/api/app/responders/plain.rb", "lib/travis/api/app/responders/service.rb", "lib/travis/api/app/responders/xml.rb", + "public/favicon.ico", + "public/images/result/error.png", "public/images/result/failing.png", "public/images/result/passing.png", + "public/images/result/pending.png", "public/images/result/unknown.png", "script/console", "script/server", + "spec/integration/formats_handling_spec.rb", "spec/integration/routes.backup.rb", "spec/integration/v1/branches_spec.rb", "spec/integration/v1/builds_spec.rb", @@ -130,6 +150,7 @@ Gem::Specification.new do |s| "spec/integration/v2/users_spec.rb", "spec/integration/v2/workers_spec.rb", "spec/integration/v2_spec.backup.rb", + "spec/integration/version_spec.rb", "spec/spec_helper.rb", "spec/support/matchers.rb", "spec/unit/app_spec.rb", @@ -152,10 +173,12 @@ Gem::Specification.new do |s| "spec/unit/extensions/scoping_spec.rb", "spec/unit/extensions/smart_constants_spec.rb", "spec/unit/extensions/subclass_tracker_spec.rb", + "spec/unit/helpers/accept_spec.rb", "spec/unit/helpers/json_renderer_spec.rb", "spec/unit/middleware/logging_spec.rb", "spec/unit/middleware/scope_check_spec.rb", "spec/unit/middleware_spec.rb", + "spec/unit/responders/json_spec.rb", "spec/unit/responders/service_spec.rb", "travis-api.gemspec" ] @@ -166,12 +189,12 @@ Gem::Specification.new do |s| s.add_dependency 'hubble', '~> 0.1' s.add_dependency 'backports', '~> 2.5' s.add_dependency 'pg', '~> 0.13.2' - s.add_dependency 'newrelic_rpm', '~> 3.5.0' + s.add_dependency 'newrelic_rpm', '~> 3.6.1.88' s.add_dependency 'thin', '~> 1.4' s.add_dependency 'sinatra', '~> 1.3' s.add_dependency 'sinatra-contrib', '~> 1.3' s.add_dependency 'redcarpet', '~> 2.1' - s.add_dependency 'rack-ssl', '~> 1.3' + s.add_dependency 'rack-ssl', '~> 1.3', '>= 1.3.3' s.add_dependency 'rack-contrib', '~> 1.1' end