glitter 1.0.0 → 2.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/README.md +68 -52
- data/lib/glitter.rb +11 -204
- data/lib/glitter/channel.rb +19 -0
- data/lib/glitter/cli.rb +26 -0
- data/lib/glitter/configurable.rb +46 -0
- data/lib/glitter/release.rb +118 -0
- data/lib/glitter/server.rb +41 -0
- data/lib/glitter/templates/appcast.xml.erb +21 -0
- data/lib/glitter/templates/notes.html.erb +24 -0
- data/lib/glitter/version.rb +1 -1
- data/spec/lib/glitter_spec.rb +8 -105
- metadata +19 -14
- data/lib/glitter/templates/Glitterfile +0 -13
- data/lib/glitter/templates/rss.xml.erb +0 -22
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,72 +1,88 @@
|
|
1
1
|
# About Glitter
|
2
2
|
|
3
|
-
Glitter is an easy way to publish
|
3
|
+
Glitter is an easy way to publish native application software updates to an Amazon S3 bucket. It was created at Poll Everywhere to eliminate the need maintaining additional server infrastructure for rolling out native desktop software updates. Glitter works with various "Sparkle" frameworks including:
|
4
4
|
|
5
|
-
|
5
|
+
* Sparkle - http://sparkle.andymatuschak.org/
|
6
|
+
* NetSparkle - http://netsparkle.codeplex.com/
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
1. Install the gem
|
10
|
-
|
11
|
-
$ gem install glitter
|
12
|
-
|
13
|
-
2. Generate a Glitterfile in your project directory
|
8
|
+
Glitter also supports the concepts of release channels, which makes it possible to have development, nightly, staging, beta, or production releases all from the command line interface. Release channels make it easier for developers to collaborate with customers and product owners about product features.
|
14
9
|
|
15
|
-
|
10
|
+
# Getting started
|
16
11
|
|
17
|
-
|
12
|
+
1. Install the gem.
|
18
13
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
archive "my_app.zip"
|
23
|
-
|
24
|
-
s3 {
|
25
|
-
bucket_name "my_app"
|
26
|
-
access_key "access"
|
27
|
-
secret_access_key "sekret"
|
28
|
-
# Set this to true to use an EU style bucket which uses a subdomain
|
29
|
-
# subdomain_bucket true
|
30
|
-
}
|
14
|
+
```sh
|
15
|
+
$ gem install glitter
|
16
|
+
```
|
31
17
|
|
32
|
-
|
18
|
+
2. Publish your app to the web.
|
33
19
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
20
|
+
```sh
|
21
|
+
$ AWS_ACCESS_KEY_ID=secret_access_key \
|
22
|
+
AWS_SECRET_ACCESS_KEY=access_key_id \
|
23
|
+
AWS_BUCKET_NAME=my-app-bucket \
|
24
|
+
glitter push my-app.dmg -v 1.2.5 -c "mac-edge" \
|
25
|
+
-m 'Added some really cool stuff to the mix!'
|
26
|
+
|
27
|
+
Pushing app my-app.dmg to https://s3.amazonaws.com/mac-edge/1.2.5/my-app.dmg
|
28
|
+
Updated head https://s3.amazonaws.com/mac-edge/my-app.dmg to https://s3.amazonaws.com/mac-edge/1.2.5/my-app.dmg
|
29
|
+
```
|
39
30
|
|
40
|
-
|
31
|
+
3. Distribute the link to your app.
|
41
32
|
|
42
|
-
https://s3.amazonaws.com/
|
33
|
+
https://s3.amazonaws.com/mac-edge/my-app.dmg is the "current" version of your application and a history is maintained with https://s3.amazonaws.com/mac-edge/1.2.5/my-app.dmg assets. You'll probably want to link to the "head" asset of your app and keep the older builds around for troubleshooting purposes.
|
43
34
|
|
44
35
|
If you want a vanity URL to distribte your app, setup a redirect like this in nginx:
|
45
36
|
|
46
|
-
rewrite ^/my-app.zip$ https://s3.amazonaws.com/
|
37
|
+
rewrite ^/my-app.zip$ https://s3.amazonaws.com/mac-edge/my-app.dmg;
|
47
38
|
|
48
39
|
Now send your users to mydomain.com/my-app.zip and they'll get the latest version of your app. I don't recommend using a CNAME with your application because it won't work with Amazon's HTTPS servers and you'll have to jump through some hoops to sign your app distributions with a DSA signature. Not worth it in my opinion.
|
49
40
|
|
50
|
-
|
41
|
+
# Contribute
|
42
|
+
|
43
|
+
Want to hack on glitter? Awesome! You'll need to setup an S3 bucket and run specs with the `AWS_URL` env var specified:
|
44
|
+
|
45
|
+
```sh
|
46
|
+
$ AWS_ACCESS_KEY_ID=secret_access_key \
|
47
|
+
AWS_SECRET_ACCESS_KEY=access_key_id \
|
48
|
+
AWS_BUCKET_NAME=my-app-bucket \
|
49
|
+
bundle exec rspec
|
50
|
+
```
|
51
|
+
|
52
|
+
Prefer to use Foreman? Create a `.env` file in the root of the project, then paste:
|
53
|
+
|
54
|
+
```sh
|
55
|
+
AWS_ACCESS_KEY_ID=secret_access_key
|
56
|
+
AWS_SECRET_ACCESS_KEY=access_key_id
|
57
|
+
AWS_BUCKET_NAME=my-app-bucket
|
58
|
+
```
|
59
|
+
|
60
|
+
and run specs via:
|
61
|
+
|
62
|
+
```sh
|
63
|
+
foreman run bundle exec rspec
|
64
|
+
```
|
65
|
+
|
66
|
+
Pretty sweet eh? That's it!
|
51
67
|
|
52
68
|
# License
|
53
69
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
Copyright (C) 2011 by Brad Gessler
|
71
|
+
|
72
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
73
|
+
of this software and associated documentation files (the "Software"), to deal
|
74
|
+
in the Software without restriction, including without limitation the rights
|
75
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
76
|
+
copies of the Software, and to permit persons to whom the Software is
|
77
|
+
furnished to do so, subject to the following conditions:
|
78
|
+
|
79
|
+
The above copyright notice and this permission notice shall be included in
|
80
|
+
all copies or substantial portions of the Software.
|
81
|
+
|
82
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
83
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
84
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
85
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
86
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
87
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
88
|
+
THE SOFTWARE.
|
data/lib/glitter.rb
CHANGED
@@ -1,209 +1,16 @@
|
|
1
|
-
require '
|
2
|
-
require 'thor'
|
3
|
-
require 'erb'
|
1
|
+
require 'glitter/version'
|
4
2
|
|
5
3
|
module Glitter
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
module InstanceMethods
|
14
|
-
def configure(path=nil,&block)
|
15
|
-
path ? instance_eval(File.read(path), path) : instance_eval(&block)
|
16
|
-
self
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
module ClassMethods
|
21
|
-
def attr_configurable(*attrs)
|
22
|
-
attrs.each do |attr|
|
23
|
-
class_eval %(
|
24
|
-
attr_writer :#{attr}
|
25
|
-
attr_overloaded :#{attr})
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def attr_overloaded(*attrs)
|
30
|
-
attrs.each do |attr|
|
31
|
-
class_eval(%{
|
32
|
-
def #{attr}(val=nil)
|
33
|
-
val ? instance_variable_set('@#{attr}', val) : instance_variable_get('@#{attr}')
|
34
|
-
end})
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def configure(*args, &block)
|
39
|
-
new.configure(*args, &block)
|
40
|
-
end
|
41
|
-
end
|
4
|
+
ExistingReleaseError = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
# Relative paths for glitter. Mostly our templating engines use this.
|
7
|
+
def self.path(*segments)
|
8
|
+
File.join File.expand_path('../glitter', __FILE__), *segments
|
42
9
|
end
|
43
10
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
include Configurable
|
51
|
-
attr_configurable :name, :version, :short_version_string, :release_notes_link, :notes, :archive, :s3
|
52
|
-
|
53
|
-
class S3
|
54
|
-
include Configurable
|
55
|
-
attr_configurable :bucket_name, :access_key, :secret_access_key, :subdomain_bucket
|
56
|
-
|
57
|
-
def url_for(path)
|
58
|
-
if subdomain_bucket
|
59
|
-
"https://#{bucket_name}.s3.amazonaws.com/#{path}"
|
60
|
-
else
|
61
|
-
"https://s3.amazonaws.com/#{bucket_name}/#{path}"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def service
|
66
|
-
@service ||= ::S3::Service.new(:access_key_id => access_key, :secret_access_key => secret_access_key, :use_ssl => true)
|
67
|
-
end
|
68
|
-
|
69
|
-
def bucket
|
70
|
-
service.buckets.find(bucket_name)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def s3(&block)
|
75
|
-
@s3 ||= S3.configure(&block)
|
76
|
-
end
|
77
|
-
|
78
|
-
class Appcast
|
79
|
-
ObjectName = 'appcast.xml'
|
80
|
-
|
81
|
-
attr_reader :app
|
82
|
-
|
83
|
-
def initialize(app)
|
84
|
-
@app = app
|
85
|
-
end
|
86
|
-
|
87
|
-
def url
|
88
|
-
app.s3.url_for ObjectName
|
89
|
-
end
|
90
|
-
|
91
|
-
def push
|
92
|
-
object.content = rss
|
93
|
-
object.save
|
94
|
-
end
|
95
|
-
|
96
|
-
def rss
|
97
|
-
@rss ||= ERB.new(File.read(RssTemplate)).result(binding)
|
98
|
-
end
|
99
|
-
|
100
|
-
private
|
101
|
-
def object
|
102
|
-
@object ||= app.s3.bucket.objects.build(ObjectName)
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def appcast
|
107
|
-
@appcast ||= Appcast.new(self)
|
108
|
-
end
|
109
|
-
|
110
|
-
def latest
|
111
|
-
assets[version]
|
112
|
-
end
|
113
|
-
|
114
|
-
def assets
|
115
|
-
@assets ||= Hash.new do |hash,key|
|
116
|
-
hash[key] = Asset.new do |r|
|
117
|
-
r.version = key
|
118
|
-
r.short_version_string = short_version_string
|
119
|
-
r.release_notes_link = release_notes_link
|
120
|
-
r.notes = notes
|
121
|
-
r.app = self
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
class Asset
|
128
|
-
attr_accessor :app, :version, :short_version_string, :release_notes_link, :notes, :published_at, :object
|
129
|
-
|
130
|
-
def initialize
|
131
|
-
@published_at = Time.now
|
132
|
-
yield self if block_given?
|
133
|
-
end
|
134
|
-
|
135
|
-
def name
|
136
|
-
"#{app.name} #{version}"
|
137
|
-
end
|
138
|
-
|
139
|
-
def object_name
|
140
|
-
"#{name.gsub(/\s/,'-').downcase}#{File.extname(file.path)}"
|
141
|
-
end
|
142
|
-
|
143
|
-
def object
|
144
|
-
@object ||= app.s3.bucket.objects.build(object_name)
|
145
|
-
end
|
146
|
-
|
147
|
-
def url
|
148
|
-
app.s3.url_for object_name
|
149
|
-
end
|
150
|
-
|
151
|
-
def push
|
152
|
-
object.content = file
|
153
|
-
object.save
|
154
|
-
self
|
155
|
-
end
|
156
|
-
|
157
|
-
# Sets this asset as the head on the S3 bucket
|
158
|
-
def head
|
159
|
-
@head ||= self.class.new do |a|
|
160
|
-
a.app = app
|
161
|
-
a.version = "head"
|
162
|
-
a.short_version_string = short_version_string
|
163
|
-
a.release_notes_link = release_notes_link
|
164
|
-
a.notes = app.notes
|
165
|
-
a.published_at = published_at
|
166
|
-
a.object = object.copy(:key => a.object_name)
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def file
|
171
|
-
File.new(app.archive)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
# Command line interface for cutting glitter builds
|
176
|
-
class CLI < Thor
|
177
|
-
desc "init PATH", "Generate a Glitterfile for the path"
|
178
|
-
def init(path)
|
179
|
-
glitterfile_path = File.join(path, 'Glitterfile')
|
180
|
-
puts "Writing new Glitterfile to #{File.expand_path glitterfile_path}"
|
181
|
-
|
182
|
-
File.open glitterfile_path, 'w+' do |file|
|
183
|
-
file.write File.read(App::TemplatePath)
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
desc "push", "pushes a build to S3 with release notes."
|
188
|
-
method_option :release_notes, :type => :string, :aliases => "-m"
|
189
|
-
def push
|
190
|
-
puts "Pushing app #{app.latest.object_name}"
|
191
|
-
# Push the latest release with release notes
|
192
|
-
puts "Notes are the following"
|
193
|
-
puts app.notes
|
194
|
-
#app.latest.notes = options[:release_notes] if options[:release_notes]
|
195
|
-
app.latest.push
|
196
|
-
puts "Asset pushed to #{app.latest.url}"
|
197
|
-
app.latest.head # Sets this release as the head.
|
198
|
-
puts "Updated head #{app.latest.head.url} to #{app.latest.url}"
|
199
|
-
# Update the appcast file
|
200
|
-
app.appcast.push
|
201
|
-
puts "Updated #{app.appcast.url}"
|
202
|
-
end
|
203
|
-
|
204
|
-
private
|
205
|
-
def app
|
206
|
-
@config ||= App.configure('./Glitterfile')
|
207
|
-
end
|
208
|
-
end
|
11
|
+
autoload :Configurable, Glitter.path('configurable')
|
12
|
+
autoload :Channel, Glitter.path('channel')
|
13
|
+
autoload :Release, Glitter.path('release')
|
14
|
+
autoload :Server, Glitter.path('server')
|
15
|
+
autoload :CLI, Glitter.path('cli')
|
209
16
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Glitter
|
2
|
+
# A Channel is where multiple releases are pushed and incremented monotonically.
|
3
|
+
# Sparkle enabled native applications should be pointed at the head of a channel.
|
4
|
+
class Channel
|
5
|
+
attr_reader :name, :server
|
6
|
+
|
7
|
+
def initialize(name, server)
|
8
|
+
@name, @server = name, server
|
9
|
+
end
|
10
|
+
|
11
|
+
def release(&block)
|
12
|
+
Release.new(self, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def versions
|
16
|
+
server.channel_versions[name]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/glitter/cli.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Glitter
|
4
|
+
# Command line interface for cutting glitter builds
|
5
|
+
class CLI < Thor
|
6
|
+
desc "push", "pushes a build to a channel with release notes."
|
7
|
+
method_option :notes, :type => :string, :aliases => "-n"
|
8
|
+
method_option :version, :type => :string, :aliases => "-v"
|
9
|
+
method_option :channel, :type => :string, :aliases => "-c"
|
10
|
+
|
11
|
+
def push(executable_path)
|
12
|
+
server = Server.new.channel(options.channel)
|
13
|
+
release = Release::Sparkle.new(server, options.version)
|
14
|
+
release.notes = options.notes
|
15
|
+
release.executable = File.open executable_path
|
16
|
+
release.push.head
|
17
|
+
end
|
18
|
+
|
19
|
+
# desc "yank", "remove a build from a release channel"
|
20
|
+
# method_option :version, :type => :string, :aliases => "-v"
|
21
|
+
# method_option :channel, :type => :string, :aliases => "-c"
|
22
|
+
# def yank
|
23
|
+
# end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Glitter
|
2
|
+
# This mix-in Creates a DSL for configuring a class so that we don't have to litter
|
3
|
+
# our application with `self.attr = "val"` all over the place or from config files.
|
4
|
+
module Configurable
|
5
|
+
def self.included(base)
|
6
|
+
base.send :extend, ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
def configure(path=nil,&block)
|
12
|
+
if path
|
13
|
+
# Read the config file up in thar.
|
14
|
+
instance_eval(File.read(path), path)
|
15
|
+
else
|
16
|
+
# Figure out how we want to call this thing.
|
17
|
+
block.arity.zero? ? instance_eval(&block) : block.call(self)
|
18
|
+
end
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def attr_configurable(*attrs)
|
25
|
+
attrs.each do |attr|
|
26
|
+
class_eval %(
|
27
|
+
attr_writer :#{attr}
|
28
|
+
attr_overloaded :#{attr})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def attr_overloaded(*attrs)
|
33
|
+
attrs.each do |attr|
|
34
|
+
class_eval(%{
|
35
|
+
def #{attr}(val=nil)
|
36
|
+
val ? instance_variable_set('@#{attr}', val) : instance_variable_get('@#{attr}')
|
37
|
+
end})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def configure(*args, &block)
|
42
|
+
new.configure(*args, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Glitter
|
5
|
+
module Release
|
6
|
+
# Break apart an S3 bucket key by /:channel/:version/:object-key
|
7
|
+
def self.object_segments(key)
|
8
|
+
if match = key.match(/\/?(.+)\/(.+)\/(.+)/)
|
9
|
+
match.captures
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Base
|
14
|
+
attr_reader :channel, :version, :logger
|
15
|
+
|
16
|
+
# Initialize a release and yield the block for configuration.
|
17
|
+
def initialize(channel, version, logger = Logger.new($stdout), &block)
|
18
|
+
@channel, @version, @logger = channel, version, logger
|
19
|
+
block.call self if block_given?
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Push assets up to the S3 bucket path `/:channel/:version/*`.
|
24
|
+
def push
|
25
|
+
raise ExistingReleaseError.new("Existing build at version #{version}. Increment the version and push again.") if version_exists?
|
26
|
+
logger.info "Pushing version #{version} to #{channel.name}"
|
27
|
+
assets.each do |key, object|
|
28
|
+
logger.info " PUT #{key} to #{object.url}"
|
29
|
+
object.save
|
30
|
+
end
|
31
|
+
logger.info "Version #{version} to #{channel.name} pushed!"
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Promote the release as the head releae. This copies the contents of the release to `/:channel/*`.
|
36
|
+
def head
|
37
|
+
logger.info "Promoting version #{version} to HEAD"
|
38
|
+
assets.map do |_, object|
|
39
|
+
channel, _, key = Release.object_segments(object.key)
|
40
|
+
object = object.copy key: File.join(channel, key)
|
41
|
+
logger.info " Copying #{key} to #{object.url}"
|
42
|
+
object.save
|
43
|
+
end
|
44
|
+
logger.info "Version #{version} promoted to HEAD!"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Registry of assets that are S3 objects. The key is `/:channel/:version/:object-key`. The hash returns an S3 object.
|
48
|
+
def assets
|
49
|
+
@assets ||= Hash.new { |h,k| h[k] = channel.server.bucket.objects.build object_key(k) } # /win-dev/1.1/appcast.xml
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
# Figure out if the version already exists on the remote bucket.
|
54
|
+
def version_exists?
|
55
|
+
channel.versions.include? version
|
56
|
+
end
|
57
|
+
|
58
|
+
# Build a key that we'll use to store these objects into S3.
|
59
|
+
def object_key(*segments)
|
60
|
+
File.join(channel.name, version, segments)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# A release consists of a binary asset, notes, a monotonically increasing version number, and
|
65
|
+
# lives inside of a channel.
|
66
|
+
class Sparkle < Base
|
67
|
+
attr_accessor :notes, :executable, :filename
|
68
|
+
attr_writer :published_at
|
69
|
+
|
70
|
+
# Yeah, lets publish this shiz NOW.
|
71
|
+
def published_at
|
72
|
+
@published_at ||= Time.now
|
73
|
+
end
|
74
|
+
|
75
|
+
# Generate assets and push
|
76
|
+
def push
|
77
|
+
notes_asset
|
78
|
+
appcast_asset
|
79
|
+
executable_asset
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
# Generates an HTML file of the release notes.
|
85
|
+
def notes_asset
|
86
|
+
assets['notes.html'].tap do |a|
|
87
|
+
a.content = render_template 'notes.html.erb'
|
88
|
+
a.content_type = 'text/html'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Generates the XML appcast file needed to publish zie document
|
93
|
+
def appcast_asset
|
94
|
+
assets['appcast.xml'].tap do |a|
|
95
|
+
a.content = render_template 'appcast.xml.erb'
|
96
|
+
a.content_type = 'application/xml'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Package up the installer executable and add to the release.
|
101
|
+
def executable_asset
|
102
|
+
assets[executable_key].tap do |a|
|
103
|
+
a.content = executable
|
104
|
+
a.content_type = 'application/octet-stream'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def executable_key
|
109
|
+
File.basename(executable)
|
110
|
+
end
|
111
|
+
|
112
|
+
def render_template(path)
|
113
|
+
ERB.new(File.read(Glitter.path('templates', path))).result(self.send(:binding))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 's3'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
# Glitter servers currently work with Amazon S3 buckets. They contain multiple channels which
|
5
|
+
# contain multiple releases of software.
|
6
|
+
module Glitter
|
7
|
+
class Server
|
8
|
+
attr_reader :access_key_id, :secret_access_key, :bucket_name
|
9
|
+
|
10
|
+
def initialize(access_key_id = ENV['AWS_ACCESS_KEY_ID'], secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'], bucket_name = ENV['AWS_BUCKET_NAME'])
|
11
|
+
@access_key_id, @secret_access_key, @bucket_name = access_key_id, secret_access_key, bucket_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def channel(name)
|
15
|
+
channels[name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def bucket
|
19
|
+
@bucket ||= s3.buckets.find bucket_name
|
20
|
+
end
|
21
|
+
|
22
|
+
# Iterate through the objects in S3 and return a hash of channels containing their
|
23
|
+
# respective released versions.
|
24
|
+
def channel_versions
|
25
|
+
bucket.objects.inject Hash.new { |h,k| h[k] = Set.new } do |hash, object|
|
26
|
+
channel, version, _ = Release.object_segments(URI(object.url).path)
|
27
|
+
hash[channel].add(version) if channel and version
|
28
|
+
hash
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def channels
|
34
|
+
Hash.new { |h,k| h[k] = Channel.new(k, self) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def s3
|
38
|
+
@s3 ||= ::S3::Service.new(:access_key_id => access_key_id, :secret_access_key => secret_access_key, :use_ssl => true)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
3
|
+
<channel>
|
4
|
+
<title><%= channel.name %></title>
|
5
|
+
<link><%= executable_asset.url %></link>
|
6
|
+
<description>Software updates for <%= channel.name %></description>
|
7
|
+
<language>en</language>
|
8
|
+
<item>
|
9
|
+
<title>Version <%= version %></title>
|
10
|
+
<sparkle:releaseNotesLink><%= notes_asset.url %></sparkle:releaseNotesLink>
|
11
|
+
<description>
|
12
|
+
<![CDATA[
|
13
|
+
<%= notes %>
|
14
|
+
]]>
|
15
|
+
</description>
|
16
|
+
<pubDate><%= published_at.strftime("%a, %d %b %Y %H:%M:%S %z") %></pubDate>
|
17
|
+
<enclosure url="<%= executable_asset.url %>" sparkle:version="<%= version %>" sparkle:shortVersionString="<%= version %>" length="<%= executable.size %>" type="application/octet-stream" />
|
18
|
+
<sparkle:releaseNotesLink><%= notes %></sparkle:releaseNotesLink>
|
19
|
+
</item>
|
20
|
+
</channel>
|
21
|
+
</rss>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<html lang="en-us">
|
2
|
+
<head>
|
3
|
+
<meta charset="utf-8">
|
4
|
+
<title><%= version %></title>
|
5
|
+
<style>
|
6
|
+
body {
|
7
|
+
font-family: sans-serif;
|
8
|
+
font-size: 11.5pt;
|
9
|
+
padding: 0.5em;
|
10
|
+
}
|
11
|
+
body > * {
|
12
|
+
margin-bottom: 0.5em;
|
13
|
+
}
|
14
|
+
h1 {
|
15
|
+
font-size: 1em;
|
16
|
+
font-weight: bold;
|
17
|
+
}
|
18
|
+
</style>
|
19
|
+
</head>
|
20
|
+
<body class="release">
|
21
|
+
<h1 class="version"><%= version %></h1>
|
22
|
+
<p class="notes"><%= notes %></p>
|
23
|
+
</body>
|
24
|
+
</html>
|
data/lib/glitter/version.rb
CHANGED
data/spec/lib/glitter_spec.rb
CHANGED
@@ -1,112 +1,15 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'rexml/document'
|
3
3
|
|
4
|
-
describe Glitter
|
5
|
-
|
6
|
-
|
7
|
-
name "My App"
|
8
|
-
version "1.0.0"
|
9
|
-
short_version_string "1.0"
|
10
|
-
release_notes_link "http://www.google.com"
|
11
|
-
archive "lib/glitter.rb"
|
12
|
-
|
13
|
-
s3 {
|
14
|
-
bucket_name "my_app"
|
15
|
-
access_key "access"
|
16
|
-
secret_access_key "sekret"
|
17
|
-
}
|
18
|
-
end
|
19
|
-
|
20
|
-
# Leave this in this order to test sorting.
|
21
|
-
@app.assets["3.0"].notes = "I'm the newest and greatest of them all. 3.0 I am!"
|
22
|
-
@app.assets["1.0"].notes = "Hi dude, 1.0"
|
23
|
-
@app.assets["2.0"].notes = "I'm way better than 2.0"
|
24
|
-
end
|
25
|
-
|
26
|
-
it "should have latest" do
|
27
|
-
@app.latest.version.should eql("1.0.0")
|
28
|
-
end
|
29
|
-
|
30
|
-
# TODO this should be speced out better using moching.
|
31
|
-
it "should have head" do
|
32
|
-
@app.latest.should respond_to(:head)
|
33
|
-
end
|
34
|
-
|
35
|
-
it "should generate rss" do
|
36
|
-
REXML::Document.new(@app.appcast.rss).root.attributes["sparkle"].should eql('http://www.andymatuschak.org/xml-namespaces/sparkle')
|
37
|
-
end
|
38
|
-
|
39
|
-
shared_examples_for "configuration" do
|
40
|
-
it "should read name" do
|
41
|
-
@config.name.should eql("My App")
|
42
|
-
end
|
43
|
-
|
44
|
-
it "should read version" do
|
45
|
-
@config.version.should eql("1.0.0")
|
46
|
-
end
|
47
|
-
|
48
|
-
it "should read short_version_string" do
|
49
|
-
@config.short_version_string.should eql("1.0")
|
50
|
-
end
|
51
|
-
|
52
|
-
it "should read release_notes_link" do
|
53
|
-
@config.release_notes_link.should eql("http://www.google.com")
|
54
|
-
end
|
55
|
-
|
56
|
-
it "should read archive" do
|
57
|
-
@config.archive.should eql("my_app.zip")
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should read notes" do
|
61
|
-
@config.notes.should eql("notes go here")
|
62
|
-
end
|
63
|
-
|
64
|
-
context "s3" do
|
65
|
-
it "should read bucket_name" do
|
66
|
-
@config.s3.bucket_name.should eql("my_app")
|
67
|
-
end
|
68
|
-
|
69
|
-
it "should read access_key" do
|
70
|
-
@config.s3.access_key.should eql("access")
|
71
|
-
end
|
72
|
-
|
73
|
-
it "should read secret_access_key" do
|
74
|
-
@config.s3.secret_access_key.should eql("sekret")
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
it "should have a valid Glitterfile template path" do
|
79
|
-
File.exists?(Glitter::App::TemplatePath).should be_true
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
context "block configuration" do
|
84
|
-
before(:all) do
|
85
|
-
@config = Glitter::App.configure do
|
86
|
-
name "My App"
|
87
|
-
version "1.0.0"
|
88
|
-
short_version_string "1.0"
|
89
|
-
release_notes_link "http://www.google.com"
|
90
|
-
archive "my_app.zip"
|
91
|
-
notes "notes go here"
|
92
|
-
|
93
|
-
s3 {
|
94
|
-
bucket_name "my_app"
|
95
|
-
access_key "access"
|
96
|
-
secret_access_key "sekret"
|
97
|
-
}
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
it_should_behave_like "configuration"
|
4
|
+
describe Glitter do
|
5
|
+
let(:server) do
|
6
|
+
Glitter::Server.new
|
102
7
|
end
|
103
8
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
it_should_behave_like "configuration"
|
9
|
+
it "should release to channel" do
|
10
|
+
Glitter::Release::Sparkle.new server.channel('test-channel'), "1.1.2-#{rand}" do |r|
|
11
|
+
r.executable = File.open(__FILE__)
|
12
|
+
r.notes = %[Did you know that its #{Time.now}? Wait, you can only answer yes to that question.]
|
13
|
+
end.push.head
|
110
14
|
end
|
111
|
-
|
112
15
|
end
|
metadata
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: glitter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease:
|
4
|
+
version: 2.0.0.alpha1
|
5
|
+
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Brad Gessler, Thomas Hanley
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-04-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: s3
|
16
|
-
requirement: &
|
16
|
+
requirement: &70321671996660 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70321671996660
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: haml
|
27
|
-
requirement: &
|
27
|
+
requirement: &70321671996200 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70321671996200
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: thor
|
38
|
-
requirement: &
|
38
|
+
requirement: &70321671995640 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,7 +43,7 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70321671995640
|
47
47
|
description: Glitter makes it easy to publish software updates via the Sparkle framework
|
48
48
|
by using S3 buckets.
|
49
49
|
email:
|
@@ -61,8 +61,13 @@ files:
|
|
61
61
|
- bin/glitter
|
62
62
|
- glitter.gemspec
|
63
63
|
- lib/glitter.rb
|
64
|
-
- lib/glitter/
|
65
|
-
- lib/glitter/
|
64
|
+
- lib/glitter/channel.rb
|
65
|
+
- lib/glitter/cli.rb
|
66
|
+
- lib/glitter/configurable.rb
|
67
|
+
- lib/glitter/release.rb
|
68
|
+
- lib/glitter/server.rb
|
69
|
+
- lib/glitter/templates/appcast.xml.erb
|
70
|
+
- lib/glitter/templates/notes.html.erb
|
66
71
|
- lib/glitter/version.rb
|
67
72
|
- spec/lib/glitter_spec.rb
|
68
73
|
- spec/spec_helper.rb
|
@@ -81,12 +86,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
86
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
87
|
none: false
|
83
88
|
requirements:
|
84
|
-
- - ! '
|
89
|
+
- - ! '>'
|
85
90
|
- !ruby/object:Gem::Version
|
86
|
-
version:
|
91
|
+
version: 1.3.1
|
87
92
|
requirements: []
|
88
93
|
rubyforge_project: glitter
|
89
|
-
rubygems_version: 1.8.
|
94
|
+
rubygems_version: 1.8.3
|
90
95
|
signing_key:
|
91
96
|
specification_version: 3
|
92
97
|
summary: Publish Mac software updates with the Sparkle framework and Amazon S3.
|
@@ -1,13 +0,0 @@
|
|
1
|
-
# After you configure this file, deploy your app with `glitter push`
|
2
|
-
name "My App"
|
3
|
-
version "1.0.0"
|
4
|
-
short_version_string "1.0"
|
5
|
-
release_notes_link "http://www.google.com"
|
6
|
-
archive "my_app.zip"
|
7
|
-
notes "notes go here"
|
8
|
-
|
9
|
-
s3 {
|
10
|
-
bucket_name "my_app"
|
11
|
-
access_key "access"
|
12
|
-
secret_access_key "sekret"
|
13
|
-
}
|
@@ -1,22 +0,0 @@
|
|
1
|
-
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
2
|
-
<channel>
|
3
|
-
<title><%= app.name %></title>
|
4
|
-
<link><%= app.appcast.url %></link>
|
5
|
-
<description>Software updates for <%= app.name %></description>
|
6
|
-
<language>en</language>
|
7
|
-
<% app.assets.each do |version, asset| %>
|
8
|
-
<item>
|
9
|
-
<title>Version <%= asset.short_version_string %></title>
|
10
|
-
<description>
|
11
|
-
<![CDATA[
|
12
|
-
<h2>New Features</h2>
|
13
|
-
<p><%= app.notes %></p>
|
14
|
-
]]>
|
15
|
-
</description>
|
16
|
-
<pubDate><%= asset.published_at.strftime("%a, %d %b %Y %H:%M:%S %z") %></pubDate>
|
17
|
-
<enclosure url="<%= asset.url %>" sparkle:version="<%= version %>" sparkle:shortVersionString="<%= asset.short_version_string %>" length="<%= asset.file.stat.size %>" type="application/octet-stream" />
|
18
|
-
<sparkle:releaseNotesLink><%= asset.release_notes_link %></sparkle:releaseNotesLink>
|
19
|
-
</item>
|
20
|
-
<% end %>
|
21
|
-
</channel>
|
22
|
-
</rss>
|