strobe 0.2.0.beta.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,8 @@ require 'active_support/core_ext/hash/indifferent_access'
8
8
  require 'active_support/core_ext/string/inflections'
9
9
 
10
10
  module Strobe
11
+ require 'strobe/version'
12
+
11
13
  autoload :Association, 'strobe/association'
12
14
  autoload :CLI, 'strobe/cli'
13
15
  autoload :Collection, 'strobe/collection'
@@ -50,8 +52,8 @@ module Strobe
50
52
  end
51
53
  end
52
54
 
53
- class RequestError < NotifiableError ; end
54
- class ServerError < NotifiableError ; end
55
+ class RequestError < NotifiableError ; end
56
+ class ServerError < NotifiableError ; end
55
57
 
56
58
  def self.connection
57
59
  @connection
@@ -1,129 +1,40 @@
1
- require 'oauth'
2
-
3
1
  module Strobe
4
2
  module Addons
5
3
  class Social
6
- TWITTER_URL = "http://api.twitter.com"
7
- REQUEST_TOKEN_SECRETS = {}
8
- ACCESS_TOKEN_SECRETS = {}
4
+ ADDON_PATH = %r[/([^/]+)(/.*)?]i
9
5
 
10
6
  def initialize(settings)
11
- @settings = settings
12
- @cookie_key = "twitter_access_token"
13
- @consumer_key = settings['social.twitter.consumer_key']
14
- @consumer_secret = settings['social.twitter.consumer_secret']
15
-
16
- unless @consumer_key
17
- raise "Social addon requires `social.twitter.consumer_key` configuration"
18
- end
19
-
20
- unless @consumer_secret
21
- raise "Social addon requires `social.twitter.consumer_secret` configuration"
22
- end
23
-
24
- @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, :site => TWITTER_URL)
7
+ @settings = settings
25
8
  end
26
9
 
27
10
  def call(env)
28
- dup._call(env)
29
- end
30
-
31
- def _call(env)
32
- @request = Rack::Request.new(env)
33
-
34
- case env["PATH_INFO"]
35
- when "/twitter/authenticate"
36
- get_request_token
37
- when "/twitter/callback"
38
- get_access_token
39
- when %r[^/twitter(.*)$]
40
- proxy
41
- else
42
- msg = "Not found"
43
- [404, {"Content-Length" => msg.bytesize.to_s}, [msg]]
44
- end
45
- end
46
-
47
- private
48
-
49
- def get_request_token
50
- request_token = @consumer.get_request_token(:oauth_callback => callback_url)
51
- REQUEST_TOKEN_SECRETS[request_token.token] = request_token.secret
52
-
53
- response = Rack::Response.new
54
- response.redirect(request_token.authorize_url(:oauth_callback => callback_url))
55
- response.finish
56
- end
11
+ if env["PATH_INFO"] =~ ADDON_PATH
12
+ addon, path = $1, $2
57
13
 
58
- def get_access_token
59
- response = Rack::Response.new
14
+ env = env.merge("SCRIPT_NAME" => "/_strobe/social/#{addon}", "PATH_INFO" => path || "")
60
15
 
61
- oauth_token = @request.params["oauth_token"]
62
- oauth_verifier = @request.params["oauth_verifier"]
63
- request_token = REQUEST_TOKEN_SECRETS[oauth_token]
64
-
65
- if oauth_token && request_token
66
- request_token = OAuth::RequestToken.new(@consumer, oauth_token, request_token)
67
- access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
68
- ACCESS_TOKEN_SECRETS[access_token.token] = access_token.secret
69
-
70
- response.body = ['<html><head><script>window.opener?window.close():(window.location = "/")</script></head></html>']
71
- response.set_cookie(@cookie_key, :value => access_token.token, :path => "/")
72
- response.finish
16
+ if addon_class = find_addon(addon)
17
+ addon_class.new(@settings).call(env)
18
+ else
19
+ [404, {}, ["Not found"]]
20
+ end
73
21
  else
74
- msg = "Could not match oauth response with an oauth request. Was the server restarted?\n"
75
- [404, {"Content-Type" => "text/plain", "Content-Length" => msg.bytesize.to_s}, msg]
22
+ [404, {}, ["Not found"]]
76
23
  end
77
24
  end
78
25
 
79
- def proxy
80
- token = @request.cookies[@cookie_key]
81
- access_token = OAuth::AccessToken.new(@consumer, token, ACCESS_TOKEN_SECRETS[token])
82
-
83
- request_method = @request.request_method.to_s.upcase
84
- request_path = @request.fullpath.sub(%r<^/_strobe/social/twitter>, "")
85
- request_body = @request.body.read
86
- request_headers = {}
87
- request_headers["Content-Type"] = @request.content_type if @request.content_type.present?
88
- request_headers["Content-Length"] = @request.content_length if @request.content_length.present?
89
-
90
- http = Net::HTTP.new(URI.parse(TWITTER_URL).host, URI.parse(TWITTER_URL).port)
91
- oauth_request = Net::HTTPGenericRequest.new(request_method, request_body.present?, request_method != "HEAD", request_path, request_headers)
92
- oauth_request.body = request_body if request_body.present?
93
- oauth_request.oauth!(http, @consumer, access_token)
94
-
95
- response_headers = {}
96
-
97
- response_headers["Authorization"] = oauth_request["Authorization"]
98
- response_headers["X-Strobe-Forward-Headers"] = "Authorization"
99
-
100
- response_headers["X-Strobe-Location"] = URI.join(TWITTER_URL, request_path).to_s
101
- response_headers["X-Strobe-Redirect-Method"] = request_method
102
-
103
- if oauth_request["Content-Type"].present?
104
- response_headers["X-Strobe-Forward-Headers"] += ",Content-Type"
105
- response_headers["Content-Type"] = oauth_request["Content-Type"]
106
- end
26
+ private
107
27
 
108
- if oauth_request["Content-Length"].present?
109
- response_headers["X-Strobe-Forward-Headers"] += ",Content-Length"
110
- response_headers["Content-Length"] = oauth_request["Content-Length"]
28
+ def find_addon(addon)
29
+ class_name = addon.camelize
30
+ Strobe::Addons::Social.const_get(class_name)
31
+ rescue
32
+ begin
33
+ require "strobe/addons/social/#{addon}"
34
+ Strobe::Addons::Social.const_get(class_name)
35
+ rescue
36
+ nil # not found
111
37
  end
112
-
113
- [302, response_headers, [request_body]]
114
- end
115
-
116
- def root_url
117
- url_for("")
118
- end
119
-
120
- def callback_url
121
- url_for("/twitter/callback")
122
- end
123
-
124
- def url_for(path)
125
- # host_with_port returns http://localhost:9293 in tests
126
- "#{@request.scheme}://#{@request.host_with_port.sub(%r<^https?://>, "")}#{@request.script_name}#{path}"
127
38
  end
128
39
  end
129
40
  end
@@ -0,0 +1,83 @@
1
+ require "cgi"
2
+
3
+ module Strobe
4
+ module Addons
5
+ class Social
6
+ class Facebook
7
+ COOKIE_KEY = "facebook_access_token"
8
+
9
+ class NotFoundError < StandardError
10
+ def code; 404 end
11
+ end
12
+
13
+ def initialize(settings)
14
+ @settings = settings
15
+ end
16
+
17
+ def call(env)
18
+ dup._call(env)
19
+ rescue NotFoundError => error
20
+ Rack::Response.new("#{error.message}\n", error.code, "Content-Type" => "text/plain").finish
21
+ end
22
+
23
+ def _call(env)
24
+ @request = Rack::Request.new(env)
25
+ @response = Rack::Response.new
26
+
27
+ case @request.path_info
28
+ when "/callback"
29
+ callback
30
+ when "/status"
31
+ status
32
+ else
33
+ raise NotFoundError, "Not found"
34
+ end
35
+
36
+ @response.finish
37
+ end
38
+
39
+ private
40
+
41
+ def callback
42
+ body = <<-EOHTML
43
+ <html><head><script>
44
+ var i, l, a, e, kv, p = (window.location.toString().split("#", 2)[1] || "").split("&");
45
+ for (i = 0, l = p.length; i < l; i++) {
46
+ kv = p[i].split("=", 2);
47
+ switch (kv[0]) {
48
+ case "access_token":
49
+ a = kv[1];
50
+ break;
51
+ case "expires_in":
52
+ e = kv[1];
53
+ break;
54
+ }
55
+ }
56
+ if (a && e) {
57
+ document.cookie = "#{COOKIE_KEY}=" + decodeURIComponent(a) + "; path=/_strobe/social/facebook; expires=" + new Date(+new Date() + +e * 1000).toString();
58
+ }
59
+ window.opener ? window.close() : window.location = "/";
60
+ </script></head></html>
61
+ EOHTML
62
+ @response["Content-Type"] = "text/html"
63
+ @response.write(body)
64
+ end
65
+
66
+ def status
67
+ @response["Content-Type"] = "text/plain"
68
+ if token.present?
69
+ @response.write("OK")
70
+ else
71
+ @response.write("UNAUTHORIZED")
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def token
78
+ CGI.escape(@request.cookies[COOKIE_KEY].to_s)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,159 @@
1
+ require "oauth"
2
+
3
+ module Strobe
4
+ module Addons
5
+ class Social
6
+ class Twitter
7
+ COOKIE_KEY = "twitter_access_token"
8
+ REQUEST_TOKEN_SECRETS = {}
9
+ ACCESS_TOKEN_SECRETS = {}
10
+
11
+ class NotFoundError < StandardError
12
+ def code; 404 end
13
+ end
14
+
15
+ class ServerError < StandardError
16
+ def code; 500 end
17
+ end
18
+
19
+ def initialize(settings)
20
+ @settings = settings
21
+ @consumer_key = settings["social.twitter.consumer_key"]
22
+ @consumer_secret = settings["social.twitter.consumer_secret"]
23
+ @url = settings["social.twitter.url"] || "http://api.twitter.com"
24
+ @consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, :site => @url)
25
+ end
26
+
27
+ def call(env)
28
+ dup._call(env)
29
+ rescue ServerError, NotFoundError => error
30
+ Rack::Response.new("#{error.message}\n", error.code, "Content-Type" => "text/plain").finish
31
+ end
32
+
33
+ def _call(env)
34
+ @request = Rack::Request.new(env)
35
+ @response = Rack::Response.new
36
+
37
+ raise ServerError, "Social addon requires `social.twitter.consumer_key` configuration" unless @consumer_key
38
+ raise ServerError, "Social addon requires `social.twitter.consumer_secret` configuration"unless @consumer_secret
39
+
40
+ case @request.path_info
41
+ when "/authentication"
42
+ env['REQUEST_METHOD'] == 'DELETE' ?
43
+ unauthenticate : authenticate
44
+ when "/callback"
45
+ callback
46
+ else
47
+ proxy
48
+ end
49
+
50
+ @response.finish
51
+ rescue Exception => e
52
+ puts e.message
53
+ puts e.backtrace
54
+ end
55
+
56
+ private
57
+
58
+ def authenticate
59
+ @response['content-type'] = 'application/json'
60
+
61
+ if ACCESS_TOKEN_SECRETS[token]
62
+ @response.body << {
63
+ :authentication => {
64
+ :status => 'authenticated',
65
+ :authentication_uri => nil
66
+ }
67
+ }.to_json
68
+ else
69
+ if oauth_callback = @request.params["oauth_callback"]
70
+
71
+ request_token = @consumer.get_request_token(:oauth_callback => oauth_callback)
72
+ REQUEST_TOKEN_SECRETS[request_token.token] = request_token.secret
73
+
74
+ @response.body << {
75
+ :authentication => {
76
+ :status => 'unauthenticated',
77
+ :authentication_uri => request_token.authorize_url(:oauth_callback => oauth_callback)
78
+ }
79
+ }.to_json
80
+ else
81
+ @response.status = 422
82
+ @response.body = ['{"errors": {"request": "The OAuth callback must be specified."}}']
83
+ end
84
+ end
85
+ end
86
+
87
+ def unauthenticate
88
+ @response.status = 204
89
+ @response.delete_cookie(COOKIE_KEY)
90
+ end
91
+
92
+ def callback
93
+ @response.write('<html><head><script>window.opener?window.close():(window.location="/")</script></head></html>')
94
+ @response["Content-Type"] = "text/html"
95
+
96
+ unless @request.params["denied"]
97
+ oauth_token = @request.params["oauth_token"]
98
+ oauth_verifier = @request.params["oauth_verifier"]
99
+ request_token_secret = REQUEST_TOKEN_SECRETS[oauth_token]
100
+
101
+ raise NotFoundError, "Could not find a proper request token. Was the server restarted?" unless request_token_secret
102
+
103
+ if oauth_token && request_token_secret
104
+ request_token = OAuth::RequestToken.new(@consumer, oauth_token, request_token_secret)
105
+ access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
106
+ ACCESS_TOKEN_SECRETS[access_token.token] = access_token.secret
107
+ @response.set_cookie(COOKIE_KEY, :value => access_token.token, :path => "/_strobe/social/twitter")
108
+ end
109
+ end
110
+ end
111
+
112
+ def proxy
113
+ token_secret = ACCESS_TOKEN_SECRETS[token]
114
+
115
+ raise NotFoundError, "Could not find a proper access token. Was the server restarted?" unless token_secret
116
+
117
+ access_token = OAuth::AccessToken.new(@consumer, token, token_secret)
118
+
119
+ request_method = @request.request_method.to_s.upcase
120
+ request_path = @request.fullpath.sub(%r<^/_strobe/social/twitter>, "")
121
+ request_body = @request.body.read
122
+ request_headers = {}
123
+ request_headers["Content-Type"] = @request.content_type if @request.content_type.present?
124
+ request_headers["Content-Length"] = @request.content_length if @request.content_length.present?
125
+
126
+ http = Net::HTTP.new(URI.parse(@url).host, URI.parse(@url).port)
127
+ oauth_request = Net::HTTPGenericRequest.new(request_method, request_body.present?, request_method != "HEAD", request_path, request_headers)
128
+ oauth_request.body = request_body if request_body.present?
129
+ oauth_request.oauth!(http, @consumer, access_token)
130
+
131
+ @response["Authorization"] = oauth_request["Authorization"]
132
+ @response["X-Strobe-Forward-Headers"] = "Authorization"
133
+
134
+ @response["X-Strobe-Location"] = URI.join(@url, request_path).to_s
135
+ @response["X-Strobe-Redirect-Method"] = request_method
136
+
137
+ if oauth_request["Content-Type"].present?
138
+ @response["X-Strobe-Forward-Headers"] += ",Content-Type"
139
+ @response["Content-Type"] = oauth_request["Content-Type"]
140
+ end
141
+
142
+ if oauth_request["Content-Length"].present?
143
+ @response["X-Strobe-Forward-Headers"] += ",Content-Length"
144
+ @response["Content-Length"] = oauth_request["Content-Length"]
145
+ end
146
+
147
+ @response.body = [request_body]
148
+ @response.status = 302
149
+ end
150
+
151
+ private
152
+
153
+ def token
154
+ @request.cookies[COOKIE_KEY]
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -13,6 +13,7 @@ module Strobe
13
13
  autoload :Ticker, 'strobe/cli/ticker'
14
14
  autoload :Users, 'strobe/cli/users'
15
15
  autoload :Input, 'strobe/cli/input'
16
+ autoload :Deploys, 'strobe/cli/deploys'
16
17
 
17
18
  include Resources
18
19
 
@@ -20,17 +21,17 @@ module Strobe
20
21
  class_option "yes", :type => :boolean, :aliases => "-y", :banner => "answer yes to all confirmation questions"
21
22
 
22
23
  def self.start(args)
23
- $ARGV = args
24
+ $__ARGV = args
24
25
  IdentityMap.wrap do
25
26
  super
26
27
  end
27
28
  rescue NotifiableError => e
28
- raise if $ARGV.include?('--backtrace')
29
+ raise if $__ARGV.include?('--backtrace')
29
30
  STDERR.puts "[ERROR] #{e.message}"
30
31
  ExceptionNotifier.notify(e, :action => args.join(" "))
31
32
  abort
32
33
  rescue StrobeError => e
33
- raise if $ARGV.include?('--backtrace')
34
+ raise if $__ARGV.include?('--backtrace')
34
35
  STDERR.puts "[ERROR] #{e.message}"
35
36
  abort
36
37
  rescue Interrupt
@@ -91,6 +92,11 @@ module Strobe
91
92
  @config = Config.new_or_stub(path) || Config.stub!(path)
92
93
  end
93
94
 
95
+ def application_dir?
96
+ File.exist?("#{path}/strobe/config.json") ||
97
+ File.exist?("#{path}/.strobe") || nil # return true or nil
98
+ end
99
+
94
100
  def settings
95
101
  @settings ||= Settings.for(path)
96
102
  end
@@ -0,0 +1,56 @@
1
+ module Strobe
2
+ class CLI::Deploys < CLI
3
+
4
+ method_option "application-id", :type => :numeric, :banner => "Use application with given id"
5
+ action "list", "list deploys" do
6
+ id = options['application-id'] || config[:application_id]
7
+ app = Application.get!(id)
8
+ deploys = app.deploys.all
9
+
10
+ deploys_table deploys
11
+ end
12
+
13
+ method_option "application-id", :type => :numeric, :banner => "Use application with given id"
14
+ method_option "full", :type => :boolean, :banner => "Display full deploy info"
15
+ method_option "include-builds", :type => :boolean, :banner => "Include builds data"
16
+ action "show", "show the last deploy" do |*args|
17
+ sha = args.first
18
+
19
+ id = options['application-id'] || config[:application_id]
20
+ app = Application.get!(id)
21
+
22
+ deploy = sha ? app.deploys.where(:id => sha) : Deploy.where(:limit => 1, :application_id => id).first
23
+
24
+ puts "deploy #{deploy[:id]}"
25
+
26
+ if options['full'] || options['include-builds']
27
+ puts <<-DEPLOY
28
+ Author: #{deploy[:name]} <#{deploy[:email]}>
29
+ Date: #{deploy[:created_at]}
30
+
31
+ #{deploy[:message]}
32
+ DEPLOY
33
+ end
34
+
35
+ if options['include-builds']
36
+ deploy.builds.all.each do |build|
37
+ puts
38
+ puts "Build ##{build[:id]}"
39
+ puts "Status: #{build[:status]}"
40
+ puts "Download uri: #{build[:download_uri]}"
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def deploys_table(deploys)
48
+ table deploys do |t|
49
+ t.column :key do |d| (d[:sha1] || d[:id])[0..7] end
50
+ t.column :name
51
+ t.column :email
52
+ t.column :message
53
+ end
54
+ end
55
+ end
56
+ end
@@ -12,9 +12,64 @@ module Strobe
12
12
  end
13
13
  end
14
14
 
15
+ desc "version", "Prints the version information"
16
+ def version
17
+ puts "Strobe version #{VERSION}"
18
+ end
19
+ map %w(-v --version) => :version
20
+
21
+ # TODO: add ability to extend CLI with addons
22
+ # TODO: remove this after it works in Hellfire
23
+ method_option "provisioning-profile", :type => :string
24
+ method_option "certificate", :type => :string
25
+ method_option "application-id", :type => :numeric, :banner => "Use application with given id"
26
+ action "phonegap:configure:ios", "configure ios for phonegap" do
27
+ pswd = @input.ask_for_password "password: "
28
+ provisioning_path = options["provisioning-profile"]
29
+ certificate_path = options["certificate"]
30
+
31
+ id = options['application-id'] || config[:application_id]
32
+ response = Strobe.connection.get "/applications/#{id}?include=platform_installs"
33
+ if response.status == 200
34
+ ios = response.body["platform_installs"].detect { |p| p["platform_id"] == 1 }
35
+ if ios
36
+ boundary = "57f5d6d3e21c57f5d6d3e2157f5d6d3e21c57f5"
37
+ body = []
38
+ body << "--#{boundary}\r\n"
39
+ body << "Content-Disposition: form-data; name=\"application[ios_cert_file]\"; filename=\"cert.p12\"\r\n"
40
+ body << "Content-Type: application/octet-stream\r\n"
41
+ body << "\r\n"
42
+ body << File.read(certificate_path)
43
+ body << "\r\n--#{boundary}\r\n"
44
+ body << "Content-Disposition: form-data; name=\"application[ios_profile_file]\"; filename=\"provision.mobileprovision\"\r\n"
45
+ body << "Content-Type: application/octet-stream\r\n"
46
+ body << "\r\n"
47
+ body << File.read(provisioning_path)
48
+ body << "\r\n--#{boundary}\r\n"
49
+ body << "Content-Disposition: form-data; name=\"application[ios_password]\"\r\n"
50
+ body << "\r\n"
51
+ body << "#{pswd}\r\n"
52
+ body << "--#{boundary}--\r\n"
53
+
54
+ response = Strobe.connection.post "/platform_installs/#{ios["id"]}/pages/configuration", body.join, "Content-Type" => "multipart/form-data; boundary=#{boundary}", "Connection" => ""
55
+ if response.status == 201 || response.status == 204
56
+ say "Successfuly updated ios keys"
57
+ else
58
+ say "Something went wrong, server responded with #{response.status}:"
59
+ puts response.body
60
+ end
61
+ else
62
+ say "[ERROR] ios seems to be not installed in this application"
63
+ end
64
+ else
65
+ say "[ERROR] #{response.body}"
66
+ end
67
+ end
68
+
15
69
  method_option "global", :type => :boolean, :banner => "Set option globally (for all applications)"
16
70
  method_option "unset", :type => :boolean, :banner => "Unset a config option"
17
- action "config", "configure strobe", :usage => "config [KEY] [VALUE]" do |key, val = nil|
71
+ action "config", "configure strobe", :usage => "config [KEY] [VALUE]" do |key, *val|
72
+ val = val.first
18
73
  case
19
74
  when val
20
75
  if options[:global]
@@ -37,7 +92,7 @@ module Strobe
37
92
  end
38
93
 
39
94
  action "users", 'manage users' do |*args|
40
- argv = $ARGV[1..-1]
95
+ argv = $__ARGV[1..-1]
41
96
  argv = [ 'list' ] + argv if args.empty?
42
97
  CLI::Users.start(argv)
43
98
  end
@@ -49,18 +104,10 @@ module Strobe
49
104
  Launchy.open("http://#{url}")
50
105
  end
51
106
 
52
- action "deploys", "manage deploys" do
53
- id = options['application-id'] || config[:application_id]
54
- app = Application.new(:id => id)
55
- deploys = app.deploys.all
56
-
57
- table deploys do |t|
58
- t.column :key do |d| d[:id][0..7] end
59
- t.column :parent do |d| d[:parent] ? d[:parent][0..7] : "" end
60
- t.column :name
61
- t.column :email
62
- t.column :message
63
- end
107
+ action "deploys", "manage deploys" do |*args|
108
+ argv = $__ARGV[1..-1]
109
+ argv = [ 'list' ] + argv if args.empty?
110
+ CLI::Deploys.start(argv)
64
111
  end
65
112
 
66
113
  action "signup", "signup for a new Strobe account" do
@@ -115,6 +162,30 @@ module Strobe
115
162
  end
116
163
 
117
164
  application_path_option
165
+ method_option "message", :type => :string, :banner => "add rollback message", :aliases => "-m"
166
+ method_option "staging", :type => :boolean, :banner => "rollback a staging environment"
167
+ action "rollback", "rollback application to a state from given deploy" do |sha|
168
+ ensure_computer_is_registered
169
+
170
+ id = options['application-id'] || config[:application_id]
171
+ resource Application.get!(id)
172
+
173
+ host = resource[:url]
174
+ host = "staging.#{host}" if options[:staging]
175
+
176
+ unless agree "Rolling back '#{resource[:name]}' to #{sha} for http://#{host}, continue? [Yn] "
177
+ say "exiting..."
178
+ exit
179
+ end
180
+
181
+ host = resource.rollback! sha, :environment => options[:staging] && 'staging',
182
+ :message => options[:message]
183
+
184
+ say "The application has successfully been rolled back to #{sha} and is available at #{host}"
185
+ end
186
+
187
+ application_path_option
188
+ method_option "message", :type => :string, :banner => "add deploy message", :aliases => "-m"
118
189
  method_option "staging", :type => :boolean, :banner => "deploy to a staging environment"
119
190
  method_option "sc-build", :type => :boolean, :banner => "run `sc-build -c` before deploying"
120
191
  method_option "no-sc-build", :type => :boolean, :banner => "skip the `sc-build -c` step"
@@ -161,8 +232,11 @@ module Strobe
161
232
 
162
233
  host = resource.deploy! :environment => options[:staging] && 'staging',
163
234
  :callback => CLI::DeployProgress.new,
235
+ :message => options[:message] || git_message,
164
236
  :sproutcore => is_sproutcore?,
165
- :bpm_project => bpm_project
237
+ :bpm_project => bpm_project,
238
+ :scm_version => scm_version,
239
+ :github_url => github_url
166
240
 
167
241
  say "The application has successfully been deployed and is available at #{host}"
168
242
  end
@@ -192,7 +266,7 @@ module Strobe
192
266
  application_path_option
193
267
  method_option "application-id", :type => :numeric, :banner => "Delete application with given id"
194
268
  action "delete", "delete a specific application" do
195
- id = options['application-id'] || config[:application_id]
269
+ id = options['application-id'] || (application_dir? && config[:application_id])
196
270
  resource(id ? Application.get!(id) : pick_application)
197
271
 
198
272
  say "Deleting application '#{resource[:name]}'"
@@ -321,12 +395,53 @@ module Strobe
321
395
  end
322
396
 
323
397
  def get_application(opts = {})
324
- unless id = opts.delete(:application_id) || config[:application_id]
398
+ unless application_dir? && (id = opts.delete(:application_id) || config[:application_id])
325
399
  error "No application found. Are you currently in the correct directory?"
326
400
  exit 1
327
401
  end
328
402
 
329
403
  Application.get!(id, opts)
330
404
  end
405
+
406
+ def git_repo?
407
+ @is_git_repo ||= begin
408
+ git_config_path = "#{path}/.git/config"
409
+ File.exist?(git_config_path)
410
+ end
411
+ end
412
+
413
+ def github_url
414
+ @github_url ||= if git_repo?
415
+ case git "config remote.origin.url"
416
+ when %r[^git@github.com:(.*?)\.git$]i
417
+ "https://github.com/#{$1}"
418
+ when %r[^https?://[^@]+@github\.com/(.*?)\.git$]i
419
+ "https://github.com/#{$1}"
420
+ when %r[^git://github\.com/(.*?)\.git]i
421
+ "https://github.com/#{$1}"
422
+ end
423
+ end
424
+ end
425
+
426
+ def scm_version
427
+ @scm_version ||= git "rev-parse HEAD" if git_repo?
428
+ end
429
+
430
+ def git_message
431
+ return unless git_repo?
432
+
433
+ msg = git "cat-file commit #{scm_version}"
434
+
435
+ return unless msg
436
+
437
+ msg.split("\n\n", 2).last
438
+ end
439
+
440
+ def git(cmd)
441
+ Dir.chdir path do
442
+ retval = `git #{cmd}`
443
+ retval.chomp if $? == 0
444
+ end
445
+ end
331
446
  end
332
447
  end
@@ -11,22 +11,15 @@ module Strobe
11
11
  alias wrapped_app app
12
12
  end
13
13
 
14
- private
15
-
16
- def preview_strobe_application
17
- is_sproutcore?? preview_sproutcore_application : preview_html_application
18
- end
19
-
20
- def bpm_project
21
- @bpm_project ||= BPM::Project.nearest_project(Dir.pwd)
22
- end
23
-
24
- def preview_html_application
14
+ def wrap(endpoint)
25
15
  project = bpm_project
26
16
 
17
+ c = config
18
+ s = settings
19
+
27
20
  app = Rack::Builder.new do
28
21
  use Middleware::Proxy
29
- use Middleware::Addons
22
+ use Middleware::Addons, :config => c, :settings => s
30
23
  use Middleware::Rewrite
31
24
 
32
25
  if project
@@ -36,15 +29,29 @@ module Strobe
36
29
  end
37
30
 
38
31
  map "/" do
39
- run Rack::Static.new lambda { |e| [] }, :urls => [ '/' ], :root => '.'
32
+ run endpoint
40
33
  end
41
34
  end.to_app
35
+ end
36
+
37
+ private
38
+
39
+ def preview_strobe_application
40
+ is_sproutcore?? preview_sproutcore_application : preview_html_application
41
+ end
42
+
43
+ def bpm_project
44
+ @bpm_project ||= BPM::Project.nearest_project(Dir.pwd)
45
+ end
42
46
 
47
+ def preview_html_application
48
+ app = wrap(Rack::Static.new lambda { |e| [] }, :urls => [ '/' ], :root => '.')
43
49
  Server.start :app => app, :host => '0.0.0.0', :server => "thin", :Port => 9292
44
50
  end
45
51
 
46
52
  def preview_sproutcore_application
47
53
  require 'strobe/sproutcore'
54
+ SC::Rack::Service.strobe_stack(self)
48
55
  SC::Tools.start ['server'] + ARGV[1..-1] + [ '--port', '9292' ]
49
56
  end
50
57
  end
@@ -56,7 +56,7 @@ module Strobe
56
56
  def current_application
57
57
  return @current_application if @current_application
58
58
 
59
- unless id = config[:application_id]
59
+ unless application_dir? && id = config[:application_id]
60
60
  error "No application found. Are you currently in the correct directory?"
61
61
  exit 1
62
62
  end
@@ -49,6 +49,14 @@ module Strobe
49
49
  to_a.length
50
50
  end
51
51
 
52
+ def first
53
+ to_a.first
54
+ end
55
+
56
+ def last
57
+ to_a.last
58
+ end
59
+
52
60
  def new(params = {})
53
61
  klass.new(@params.merge(params))
54
62
  end
@@ -19,7 +19,8 @@ module Strobe
19
19
  c = YAML.load_file(old_config_path)
20
20
  if Hash === c && application_id = c['STROBE_APPLICATION_ID']
21
21
  c.delete('STROBE_APPLICATION_ID')
22
- stub!(application_root, application_id.to_i)
22
+ inst = stub!(application_root, application_id.to_i)
23
+ inst
23
24
  end
24
25
  end
25
26
  end
@@ -104,7 +105,7 @@ module Strobe
104
105
  # to inject the application_id key at the very beginning
105
106
  elsif @hash.keys.length > 1
106
107
  json = raw_json
107
- json.sub!(/^(\s*){\s*(})?/m) do |m|
108
+ json.sub!(/^(\s*)\{\s*(\})?/m) do |m|
108
109
  lines = "#{$1}{\n" \
109
110
  "#{$1} // The Strobe Platform ID for the application. This is how the local\n" \
110
111
  "#{$1} // application is mapped to one on the server.\n" \
@@ -149,7 +149,7 @@ module Strobe
149
149
 
150
150
  def build_http
151
151
  http = Net::HTTP.new host, port || 80
152
- http.read_timeout = 60
152
+ http.read_timeout = 900 # For deploys
153
153
  http.open_timeout = 10
154
154
  http
155
155
  end
@@ -189,7 +189,9 @@ module Strobe
189
189
  end
190
190
 
191
191
  if body
192
- headers['content-type'] ||= 'application/json'
192
+ if Hash === body
193
+ headers['content-type'] ||= 'application/json'
194
+ end
193
195
 
194
196
  if headers['content-type'] == 'application/json'
195
197
  body = ActiveSupport::JSON.encode(body)
@@ -32,7 +32,12 @@ module Strobe
32
32
  env['REQUEST_METHOD'] = hdrs['x-strobe-redirect-method']
33
33
  env['REQUEST_METHOD'] ||= is_head ? 'HEAD' : 'GET'
34
34
  env['async.callback'] = callback
35
- env['rack.input'] = StringIO.new(body.join)
35
+
36
+ # Rack::Request doesn't have join method
37
+ body_str = ""
38
+ body.each { |chunk| body_str << chunk }
39
+
40
+ env['rack.input'] = StringIO.new(body_str)
36
41
  env
37
42
  end
38
43
 
@@ -84,9 +89,10 @@ module Strobe
84
89
 
85
90
  def call(env)
86
91
  if env['PATH_INFO'] =~ PROXY_PATH
92
+ p1, p2 = $1, $2
87
93
  scheme = scheme_from_env(env)
88
- host, port = $1.split(':')
89
- path = $2 || '/'
94
+ host, port = p1.split(':')
95
+ path = p2 || '/'
90
96
  port = (port || 80).to_i
91
97
 
92
98
  url = "#{scheme}://#{host}"
@@ -213,8 +219,9 @@ module Strobe
213
219
  @redirect = headers['Location'] && @http.req.follow_redirect?
214
220
 
215
221
  unless @redirect
216
- headers = headers.select { |k,v| !block_headers.include?(k.downcase) }
217
- headers = headers.merge(extra_headers)
222
+ h = {}
223
+ headers.each { |k,v| h[k] = v if !block_headers.include?(k.downcase) }
224
+ headers = h.merge(extra_headers)
218
225
  @response_headers = ::Rack::Utils::HeaderHash.new(headers)
219
226
  @body = DeferrableBody.new(self) if has_response_body?
220
227
  respond(@http.response_header.status, @response_headers)
@@ -10,5 +10,6 @@ module Strobe
10
10
  require 'strobe/resources/team'
11
11
  require 'strobe/resources/user'
12
12
  require 'strobe/resources/deploy'
13
+ require 'strobe/resources/build'
13
14
  end
14
15
  end
@@ -1,4 +1,5 @@
1
1
  require 'zlib'
2
+ require 'uri'
2
3
  require 'digest/sha1'
3
4
 
4
5
  module Strobe
@@ -15,17 +16,51 @@ module Strobe
15
16
 
16
17
  validates "account", "name", :presence => true
17
18
 
19
+ def rollback!(sha, opts = {})
20
+ environment = opts[:environment]
21
+ message = opts[:message]
22
+
23
+ id = self[:id]
24
+
25
+ request do
26
+ qs = []
27
+ qs << "environment=#{environment}" if environment
28
+ qs << "message=#{message}" if message
29
+ qs << "deploy=#{sha}"
30
+ qs << "application_id=#{id}"
31
+
32
+ uri = URI.escape("/deploys?#{qs.join('&')}")
33
+
34
+ response = connection.post(uri)
35
+ response
36
+ end
37
+
38
+ [ environment, self['url'] ].compact.join('.')
39
+ end
40
+
18
41
  def deploy!(opts = {})
19
42
  self['path'] = opts[:path] if opts[:path]
20
43
  environment = opts[:environment]
21
44
  callback = opts[:callback]
45
+ message = opts[:message]
46
+ github_url = opts[:github_url]
47
+ scm_version = opts[:scm_version]
22
48
 
23
49
  validate_for_deploy or return
24
50
 
25
51
  request do
26
- qs = "?environment=#{environment}" if environment
52
+ qs = []
53
+ qs << "environment=#{environment}" if environment
54
+ qs << "message=#{message}" if message
55
+ qs << "github_url=#{URI.escape(github_url)}" if github_url
56
+ qs << "scm_version=#{scm_version}" if scm_version
57
+
58
+ uri = "#{http_uri}/deploy"
59
+ uri = "#{uri}?#{qs.join('&')}" unless qs.blank?
60
+ uri = URI.escape(uri)
61
+
27
62
  packfile = build_packfile(opts)
28
- response = connection.put("#{http_uri}/deploy#{qs}", packfile, packfile.headers)
63
+ response = connection.put(uri, packfile, packfile.headers)
29
64
  callback.deploy_complete if callback
30
65
  response
31
66
  end
@@ -3,8 +3,8 @@ module Strobe
3
3
  class Assignment
4
4
  include Resource::Collection
5
5
 
6
- has 1, :team
7
- has 1, :application
6
+ has 1, :team, :include => true
7
+ has 1, :application, :include => true
8
8
 
9
9
  validates :team, :application, :presence => true
10
10
  end
@@ -0,0 +1,13 @@
1
+ module Strobe
2
+ module Resources
3
+ class Build
4
+ include Resource::Collection
5
+
6
+ key :id, String
7
+ key :download_uri, String
8
+ key :status, String
9
+
10
+ has 1, :deploy
11
+ end
12
+ end
13
+ end
@@ -1,14 +1,12 @@
1
- require 'zlib'
2
- require 'digest/sha1'
3
-
4
1
  module Strobe
5
2
  module Resources
6
3
  class Deploy
7
4
  include Resource::Collection
8
5
 
9
- key :id, String
6
+ key :id, String
10
7
 
11
8
  has 1, :application
9
+ has n, :builds
12
10
  end
13
11
  end
14
12
  end
@@ -1,12 +1,16 @@
1
1
  require 'sproutcore'
2
2
 
3
3
  class SC::Rack::Service
4
+ def self.strobe_stack(obj = nil)
5
+ @strobe_stack = obj if obj
6
+ @strobe_stack
7
+ end
8
+
4
9
  def call(env)
5
10
  wrapped_app.call(env)
6
11
  end
7
12
 
8
13
  def wrapped_app
9
- # @wrapped_app ||= Strobe::Middleware::Proxy.new(@app)
10
- @app
14
+ @wrapped_app ||= self.class.strobe_stack.wrap(@app)
11
15
  end
12
16
  end
@@ -0,0 +1,3 @@
1
+ module Strobe
2
+ VERSION = "0.2.0"
3
+ end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strobe
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: 6
5
- version: 0.2.0.beta.2
4
+ prerelease:
5
+ version: 0.2.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Carl Lerche
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-07-18 00:00:00 +09:00
13
+ date: 2011-08-16 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -54,7 +54,7 @@ dependencies:
54
54
  requirements:
55
55
  - - ~>
56
56
  - !ruby/object:Gem::Version
57
- version: 1.2.0
57
+ version: 1.3.0
58
58
  type: :runtime
59
59
  version_requirements: *id004
60
60
  - !ruby/object:Gem::Dependency
@@ -98,7 +98,7 @@ dependencies:
98
98
  requirements:
99
99
  - - ~>
100
100
  - !ruby/object:Gem::Version
101
- version: 0.4.4
101
+ version: 0.4.5
102
102
  type: :runtime
103
103
  version_requirements: *id008
104
104
  - !ruby/object:Gem::Dependency
@@ -122,9 +122,12 @@ extensions: []
122
122
  extra_rdoc_files: []
123
123
 
124
124
  files:
125
+ - lib/strobe/addons/social/facebook.rb
126
+ - lib/strobe/addons/social/twitter.rb
125
127
  - lib/strobe/addons/social.rb
126
128
  - lib/strobe/association.rb
127
129
  - lib/strobe/cli/deploy_progress.rb
130
+ - lib/strobe/cli/deploys.rb
128
131
  - lib/strobe/cli/input.rb
129
132
  - lib/strobe/cli/main.rb
130
133
  - lib/strobe/cli/preview.rb
@@ -148,6 +151,7 @@ files:
148
151
  - lib/strobe/resources/account.rb
149
152
  - lib/strobe/resources/application.rb
150
153
  - lib/strobe/resources/assignment.rb
154
+ - lib/strobe/resources/build.rb
151
155
  - lib/strobe/resources/deploy.rb
152
156
  - lib/strobe/resources/me.rb
153
157
  - lib/strobe/resources/membership.rb
@@ -157,6 +161,7 @@ files:
157
161
  - lib/strobe/resources.rb
158
162
  - lib/strobe/sproutcore.rb
159
163
  - lib/strobe/validations.rb
164
+ - lib/strobe/version.rb
160
165
  - lib/strobe.rb
161
166
  - CHANGELOG.md
162
167
  - README.md