diff --git a/Gemfile b/Gemfile index 69bb11d3..e8365a53 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ -ruby '1.9.3' rescue nil - source 'https://rubygems.org' +ruby '1.9.3' gem 'puma' gem 'rack-ssl', '~> 1.3' diff --git a/assets/scripts/app/templates/layouts/top.hbs b/assets/scripts/app/templates/layouts/top.hbs index 27c96af5..1e614260 100644 --- a/assets/scripts/app/templates/layouts/top.hbs +++ b/assets/scripts/app/templates/layouts/top.hbs @@ -15,6 +15,9 @@
  • {{t layouts.top.docs}}
  • +
  • + {{t layouts.top.status}} +
  • {{t layouts.top.github_login}} diff --git a/assets/scripts/travis.coffee b/assets/scripts/travis.coffee index d2b6ea6e..e30412a0 100644 --- a/assets/scripts/travis.coffee +++ b/assets/scripts/travis.coffee @@ -4,6 +4,18 @@ require 'ext/ember/namespace' window.ENV ||= {} window.ENV.RAISE_ON_DEPRECATION = true +if window.history.state == undefined + window.history.state = {} + oldPushState = window.history.pushState + window.history.pushState = (state, title, href) -> + window.history.state = state + oldPushState.apply this, arguments + + oldReplaceState = window.history.replaceState + window.history.replaceState = (state, title, href) -> + window.history.state = state + oldReplaceState.apply this, arguments + # TODO: how can I put it in Travis namespace and use immediately? Storage = Em.Object.extend init: -> @@ -99,7 +111,6 @@ $.extend Travis, CONFIG_KEYS: ['rvm', 'gemfile', 'env', 'jdk', 'otp_release', 'php', 'node_js', 'perl', 'python', 'scala', 'compiler'] QUEUES: [ - { name: 'common', display: 'Common' } { name: 'linux', display: 'Linux' } { name: 'mac_osx', display: 'Mac and OSX' } ] diff --git a/config.ru b/config.ru index 4e95b1d7..f3272ca3 100644 --- a/config.ru +++ b/config.ru @@ -25,7 +25,7 @@ use Travis::Web::ApiRedirect do |app| app.settings.api_endpoint = ENV['API_ENDPOINT'] if ENV['API_ENDPOINT'] end -run Travis::Web::App.new( +run Travis::Web::App.build( environment: ENV['RACK_ENV'] || 'development', api_endpoint: ENV['API_ENDPOINT'], pusher_key: ENV['PUSHER_KEY'], diff --git a/lib/travis/web/app.rb b/lib/travis/web/app.rb index da5f475b..d459eee3 100644 --- a/lib/travis/web/app.rb +++ b/lib/travis/web/app.rb @@ -6,8 +6,11 @@ require 'delegate' require 'time' class Travis::Web::App + autoload :AltVersions, 'travis/web/app/alt_versions' autoload :MobileRedirect, 'travis/web/app/mobile_redirect' + S3_URL = 'https://s3.amazonaws.com/travis-web-production/assets' + # Simple Rack router that behaves like a hash. # Key is the path, value the response. class Router < DelegateClass(Hash) @@ -18,103 +21,83 @@ class Travis::Web::App end def call(env) - if main_app.custom_branch?(env) - main_app.response_for_custom_branch(env) - else - self[env['PATH_INFO']] - end + self[env['PATH_INFO']] end end - def self.new(options = {}) - return super unless options[:environment] == 'development' - proc { |e| super.call(e) } # poor man's reloader + class << self + def new(options = {}) + return super unless options[:environment] == 'development' + proc { |e| super.call(e) } # poor man's reloader + end + + def build(options = {}) + builder = Rack::Builder.new + if options[:environment] == 'production' + builder.use Rack::SSL + # builder.use Rack::Cache + end + builder.use Rack::Deflater + builder.use Rack::Head + builder.use Rack::Protection::XSSHeader + builder.use Rack::Protection::FrameOptions + builder.use Rack::Protection::PathTraversal + builder.use Rack::ConditionalGet + builder.use Travis::Web::App::AltVersions + builder.run new(options) + builder.to_app + end end - attr_reader :app, :router, :environment, :version, :last_modified, :age, :options, :root + attr_reader :routers, :version, :last_modified, :age, :options, :root def initialize(options = {}) @options = options - @environment = options.fetch(:environment) @root = options.fetch(:root) - @router = Router.new(self) - @app = builder.to_app @version = File.read File.expand_path('version', root) @last_modified = Time.now @age = 60 * 60 * 24 * 365 - load_routes + @routers = { default: create_router } end def call(env) - app.call(env) - end - - def response_for_custom_branch(env) - status, headers, body = response_for File.join(root, 'index.html'), custom_branch: custom_branch(env) - response = Rack::Response.new body, status, headers - - if disable_custom_branch?(env) - response.delete_cookie 'custom_branch' - elsif custom_branch_from_params(env) - response.set_cookie 'custom_branch', value: custom_branch_from_params(env), expires: Time.now + 31536000 - end - - response.finish - end - - def custom_branch?(env) - custom_branch(env) || disable_custom_branch?(env) + name = env['travis.alt'] || :default + routers[name] ||= create_router(alt: name) + routers[name].call(env) end private - def disable_custom_branch?(env) - env['QUERY_STRING'] =~ /disable[_-]custom[_-]branch/ + def create_router(options = {}) + router = Router.new(self) + load_routes(router, options) + router end - def custom_branch_from_params(env) - branch = custom_branch_from_string env['QUERY_STRING'] - end - - def custom_branch_from_cookie(env) - custom_branch_from_string env['HTTP_COOKIE'] - end - - def custom_branch_from_string(string) - $1 if string =~ /(? content.bytesize.to_s, - 'Content-Location' => route_for(file), + 'Content-Location' => path_for(file), 'Cache-Control' => cache_control(file), - 'Content-Location' => route_for(file), + 'Content-Location' => path_for(file), 'Content-Type' => mime_type(file), 'ETag' => version, 'Last-Modified' => last_modified.httpdate, 'Expires' => (last_modified + age).httpdate, 'Vary' => vary_for(file) } - - [ 200, headers, [ content ] ] + [ 200, headers, [content] ] end def each_file - pattern = File.join(root, '**/*') - Dir.glob(pattern) { |f| yield f if File.file? f } + Dir.glob(File.join(root, '**/*')) { |file| yield file if File.file?(file) } end def prefix?(file) @@ -126,18 +109,11 @@ class Travis::Web::App end def index?(file) - file.end_with? 'index.html' - end - - def route_for(file) - file = file.sub("#{root}/", '') - file = File.join(version, file) if prefix? file - file = "" if index? file - "/#{file}" + file.end_with?('index.html') end def cache_control(file) - case route_for(file) + case path_for(file) when '/' then "public, must-revalidate" when '/version' then "no-cache" else "public, max-age=#{age}" @@ -145,13 +121,20 @@ class Travis::Web::App end def vary_for(file) - case route_for(file) + case path_for(file) when '/' then 'Accept' when '/version' then '*' else '' end end + def path_for(file) + file = file.sub("#{root}/", '') + file = File.join(version, file) if prefix?(file) + file = "" if index?(file) + "/#{file}" + end + def mime_type(file) Rack::Mime.mime_type File.extname(file) end @@ -162,28 +145,7 @@ class Travis::Web::App end string.gsub! %r{(src|href)="(?:\/?)((styles|scripts)\/[^"]*)"} do - if opts[:custom_branch] - url = "https://s3.amazonaws.com/travis-web-production/assets/#{opts[:custom_branch]}/#{$2}" - %(#$1="#{url}") - else - %(#$1="/#{version}/#$2") - end + %(#{$1}=#{opts[:alt] ? "#{S3_URL}/#{opts[:alt]}/#{$2}" : "/#{version}/#{$2}"}) end end - - def builder - builder = Rack::Builder.new - if environment == 'production' - builder.use Rack::SSL - builder.use Rack::Cache - end - builder.use Rack::Deflater - builder.use Rack::Head - builder.use Rack::Protection::XSSHeader - builder.use Rack::Protection::FrameOptions - builder.use Rack::Protection::PathTraversal - builder.use Rack::ConditionalGet - builder.run router - builder - end end diff --git a/lib/travis/web/app/alt_versions.rb b/lib/travis/web/app/alt_versions.rb new file mode 100644 index 00000000..7a3671c7 --- /dev/null +++ b/lib/travis/web/app/alt_versions.rb @@ -0,0 +1,29 @@ +class Travis::Web::App::AltVersions + attr_reader :app + + def initialize(app) + @app = app + end + + def call(env) + alt = alt_from_params(env) || alt_from_cookie(env) + env['travis.alt'] = alt if alt && alt != 'default' + status, headers, body = app.call(env) + headers['Set-Cookie'] = cookie(alt) if alt + [status, headers, body] + end + + private + + def cookie(alt) + "alt=#{alt == 'default' ? '' : alt}; path=/; max-age=#{alt == 'default' ? 0 : 86400}" + end + + def alt_from_params(env) + $1 if env['QUERY_STRING'] =~ /alt=([^&]+)/ + end + + def alt_from_cookie(env) + $1 if env['HTTP_COOKIE'] =~ /alt=([^;]+)/ + end +end diff --git a/locales/en.yml b/locales/en.yml index d5ef97fd..19b3e348 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -96,6 +96,7 @@ en: sign_out: Sign Out signing_in: Signing In stats: Stats + status: Status locales: ca: de: Deutsch diff --git a/spec/app_spec.rb b/spec/app_spec.rb index 584616ba..535367fb 100644 --- a/spec/app_spec.rb +++ b/spec/app_spec.rb @@ -6,7 +6,7 @@ describe Travis::Web::App do end describe 'catch all' do - before { get('/foo/bar') } + before { get('/foo/bar') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/' } example { headers['Cache-Control'].should include('must-revalidate') } @@ -15,7 +15,7 @@ describe Travis::Web::App do end describe 'assets' do - before { get('/favicon.ico') } + before { get('/favicon.ico') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/favicon.ico' } example { headers['Cache-Control'].should_not include('must-revalidate') } @@ -24,30 +24,31 @@ describe Travis::Web::App do end describe 'version' do - before { get('/version') } + before { get('/version') } example { last_response.should be_ok } example { headers['Content-Location'].should be == '/version' } example { headers['Cache-Control'].should be == 'no-cache' } example { headers['Vary'].split(',').should_not include('Accept') } end - describe 'custom branch' do - context 'when passing custom branch as a param' do - before { get('/?custom-branch=foo') } + describe 'alternate asset versions' do + context 'not passing an alt param' do + before { get('/') } + example { headers['Set-Cookie'].should be_nil } + end + + context 'passing an alt param' do + before { get('/?alt=foo') } example { last_response.should be_ok } example { last_response.body.should include('/assets/foo/styles/app.css') } example { last_response.body.should include('/assets/foo/scripts/app.js') } - example { headers['Set-Cookie'].should include('custom_branch=foo') } + example { headers['Set-Cookie'].should == 'alt=foo; path=/; max-age=86400' } end - context 'disabling custom branch' do - before { get('/?disable-custom-branch=true') } - example { last_response.should be_ok } - example { last_response.body.should =~ %r{src="/[^\/]+/scripts/app.js} } - example { last_response.body.should_not include('/assets/true/styles/app.css') } - example { last_response.body.should_not include('/assets/foo/styles/app.css') } - example { last_response.body.should_not include('/assets/foo/scripts/app.js') } - example { headers['Set-Cookie'].should include('custom_branch=;') } + context 'passing default as an alt param' do + before { get('/?alt=default') } + example { last_response.body.should_not =~ /\/assets\/[^\/]+\/scripts\/app.js/ } + example { headers['Set-Cookie'].should == 'alt=; path=/; max-age=0' } end end end