defunkt-integrity 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ Integrity
2
+ =========
3
+
4
+ Integrity is your friendly automated Continuous Integration server.
5
+
6
+ It's fully usable from within its web interface (backed by [Sinatra][]),
7
+ allowing you to add a project, set preferences for it (where's the code
8
+ repository, is it completely private or public, etc), and run the build command
9
+ from there.
10
+
11
+ It has been designed with ruby projects in mind, but any project that can be
12
+ tested in an unix-y fashion (with a command line tool that returns 0 on success
13
+ and non-zero on failure) works with it.
14
+
15
+ Getting Started
16
+ ===============
17
+
18
+ Install the `integrity` gem from GitHub:
19
+
20
+ gem sources add http://gems.github.com
21
+ sudo gem install foca-integrity
22
+
23
+ In order to setup Integrity, run the following command:
24
+
25
+ integrity install /path/to/my/app
26
+
27
+ Then browse to /path/to/my/app and edit the config files at your convenience.
28
+ The default configuration should be "good enough" in most cases, so you should
29
+ be pretty much ready to rock.
30
+
31
+ For deployment, we recommend [Thin][]. Provided with Integrity comes a thin.yml
32
+ file, so all you need to do after running `integrity install` should be
33
+
34
+ thin -C /path/to/my/app/thin.yml -R /path/to/my/app/config.ru start
35
+
36
+ And you should be up and running.
37
+
38
+ If you want automatic commit processing, you currently need to be using
39
+ [GitHub][]. Click the edit link on your GitHub project, and add an integrity
40
+ link that looks like the following to the `Post-Receive URL` field:
41
+
42
+ http://integrity.domain.tld/projectname/push
43
+
44
+ Receiving Notifications
45
+ =======================
46
+
47
+ If you want to be notified after each build, you need to install our notifiers.
48
+ For example, in order to receive an email after each build, install:
49
+
50
+ sudo gem install foca-integrity-email
51
+
52
+ And then edit `/path/to/my/app/config.ru` and add:
53
+
54
+ require "notifier/email"
55
+
56
+ After all the `require` lines.
57
+
58
+ Resources
59
+ ========
60
+
61
+ We have a [Lighthouse account][lighthouse] where you can submit patches or
62
+ feature requests. Also, someone is usually around [#integrity][irc-channel] on
63
+ Freenode, so don't hesitate to stop by for ideas, help, patches or something.
64
+
65
+ Future plans
66
+ ============
67
+
68
+ * [Twitter][]/[IRC][]/etc bots
69
+ * A sample generic post-receive-hook so you can run this from any git repo
70
+ * Better integration with GitHub
71
+
72
+ Development
73
+ ===========
74
+
75
+ The code is stored in [GitHub][repo]. Feel free to fork, play with it, and send
76
+ a pull request afterwards.
77
+
78
+ In order to run the test suite you'll need a few more gems: [rspec][], [rcov][]
79
+ and [hpricot][]. With that installed running `rake` will run the specs and
80
+ ensure the code coverage stays high.
81
+
82
+ Thanks
83
+ ======
84
+
85
+ Thanks to the fellowing people for their feedbacks, ideas and patches :
86
+
87
+ * [James Adam][james]
88
+ * [Elliott Cable][ec]
89
+ * [Corey Donohoe][atmos]
90
+ * [Kyle Hargraves][kyle]
91
+ * [Pier-Hugues Pellerin][ph]
92
+ * [Simon Rozet][sr]
93
+ * [Scott Taylor][scott]
94
+
95
+ [james]: http://github.com/lazyatom
96
+ [ec]: http://github.com/elliotcabble
97
+ [atmos]: http://github.com/atmos
98
+ [kyle]: http://github.com/pd
99
+ [ph]: http://github.com/ph
100
+ [sr]: http://purl.org/net/sr/
101
+ [scott]: http://github.com/smtlaissezfaire
102
+
103
+ License
104
+ =======
105
+
106
+ (The MIT License)
107
+
108
+ Copyright (c) 2008 [Nicolás Sanguinetti][foca], [entp][]
109
+
110
+ Permission is hereby granted, free of charge, to any person obtaining
111
+ a copy of this software and associated documentation files (the
112
+ 'Software'), to deal in the Software without restriction, including
113
+ without limitation the rights to use, copy, modify, merge, publish,
114
+ distribute, sublicense, and/or sell copies of the Software, and to
115
+ permit persons to whom the Software is furnished to do so, subject to
116
+ the following conditions:
117
+
118
+ The above copyright notice and this permission notice shall be
119
+ included in all copies or substantial portions of the Software.
120
+
121
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
122
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
123
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
124
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
125
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
126
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
127
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
128
+
129
+ [Sinatra]: http://sinatrarb.com
130
+ [git]: http://git.or.cz
131
+ [svn]: http://subversion.tigris.org
132
+ [Twitter]: http://twitter.com
133
+ [IRC]: http://wikipedia.org/wiki/IRC
134
+ [entp]: http://entp.com
135
+ [GitHub]: http://github.com
136
+ [Thin]: http://code.macournoyer.com/thin/
137
+
138
+ [rspec]: http://rspec.info
139
+ [rcov]: http://eigenclass.org/hiki.rb?rcov
140
+ [hpricot]: http://code.whytheluckystiff.net/hpricot
141
+
142
+ [repo]: http://github.com/foca/integrity
143
+ [lighthouse]: http://integrity.lighthouseapp.com/projects/14308-integrity
144
+ [irc-channel]: irc://irc.freenode.net/integrity
145
+
146
+ [foca]: http://nicolassanguinetti.info/
@@ -0,0 +1,89 @@
1
+ require File.dirname(__FILE__) + "/lib/integrity"
2
+ require 'spec/rake/spectask'
3
+ require 'spec/rake/verify_rcov'
4
+
5
+ task :default => ["spec:coverage", "spec:coverage:verify"]
6
+
7
+ Spec::Rake::SpecTask.new(:spec) do |t|
8
+ t.spec_opts = ["--color", "--format", "progress"]
9
+ t.spec_files = Dir['spec/**/*_spec.rb'].sort
10
+ t.libs = ['lib']
11
+ t.rcov = false
12
+ end
13
+
14
+ namespace :spec do
15
+ Spec::Rake::SpecTask.new(:coverage) do |t|
16
+ t.spec_opts = ["--color", "--format", "progress"]
17
+ t.spec_files = Dir['spec/**/*_spec.rb'].sort
18
+ t.libs = ['lib']
19
+ t.rcov = true
20
+ t.rcov_opts = ['--exclude-only', '".*"', '--include-file', '^lib']
21
+ end
22
+
23
+ namespace :coverage do
24
+ RCov::VerifyTask.new(:verify) do |t|
25
+ t.threshold = 100
26
+ t.index_html = "coverage" / 'index.html'
27
+ end
28
+ end
29
+ end
30
+
31
+ namespace :db do
32
+ desc "Setup connection."
33
+ task :connect do
34
+ config = File.expand_path(ENV['CONFIG']) if ENV['CONFIG']
35
+ config = Integrity.root / 'config.yml' if File.exists?(Integrity.root / 'config.yml')
36
+ Integrity.config = config if config
37
+ Integrity.new
38
+ end
39
+
40
+ desc "Automigrate the database"
41
+ task :migrate => :connect do
42
+ require "project"
43
+ require "build"
44
+ require "notifier"
45
+ DataMapper.auto_migrate!
46
+ end
47
+ end
48
+
49
+ namespace :gem do
50
+ desc "Generate the gemspec at the root dir"
51
+ task :gemspec do
52
+ files = `git ls-files`.split("\n").reject {|f| f =~ %r(^spec) || f =~ %r(^vendor/rspec) || f =~ /^\.git/ }
53
+ files += %w(spec/spec_helper.rb spec/form_field_matchers.rb)
54
+
55
+ gemspec = <<-GEM
56
+ Gem::Specification.new do |s|
57
+ s.name = 'integrity'
58
+ s.version = '#{Integrity::VERSION}'
59
+ s.date = '#{Date.today.to_s}'
60
+ s.summary = 'The easy and fun Continuous Integration server'
61
+ s.description = 'Your Friendly Continuous Integration server. Easy, fun and painless!'
62
+ s.homepage = 'http://integrityapp.com'
63
+ s.rubyforge_project = 'integrity'
64
+ s.email = 'contacto@nicolassanguinetti.info'
65
+ s.authors = ['Nicolás Sanguinetti', 'Simon Rozet']
66
+ s.has_rdoc = false
67
+ s.executables = ['integrity']
68
+ s.files = %w(
69
+ #{files.join("\n" + " " * 26)}
70
+ )
71
+
72
+ s.add_dependency 'sinatra', ['>= 0.3.2']
73
+ s.add_dependency 'dm-core', ['>= 0.9.5']
74
+ s.add_dependency 'dm-validations', ['>= 0.9.5']
75
+ s.add_dependency 'dm-types', ['>= 0.9.5']
76
+ s.add_dependency 'dm-timestamps', ['>= 0.9.5']
77
+ s.add_dependency 'dm-aggregates', ['>= 0.9.5']
78
+ s.add_dependency 'data_objects', ['>= 0.9.5']
79
+ s.add_dependency 'do_sqlite3', ['>= 0.9.5']
80
+ s.add_dependency 'json'
81
+ s.add_dependency 'foca-sinatra-diddies', ['>= 0.0.2']
82
+ s.add_dependency 'rspec_hpricot_matchers'
83
+ s.add_dependency 'thor'
84
+ end
85
+ GEM
86
+
87
+ File.open(Integrity.root / "integrity.gemspec", "w") {|f| f.puts gemspec }
88
+ end
89
+ end
data/app.rb ADDED
@@ -0,0 +1,249 @@
1
+ require File.dirname(__FILE__) + '/lib/integrity'
2
+ require 'sinatra'
3
+ require 'diddies'
4
+ require 'hacks'
5
+
6
+ set :root, Integrity.root
7
+ set :public, Integrity.root / "public"
8
+ set :views, Integrity.root / "views"
9
+
10
+ enable :sessions
11
+
12
+ include Integrity
13
+
14
+ configure do
15
+ config = Integrity.root / "config.yml"
16
+ Integrity.config = config if File.exists? config
17
+ Integrity.new
18
+ end
19
+
20
+ not_found do
21
+ status 404
22
+ show :not_found, :title => "lost, are we?"
23
+ end
24
+
25
+ before do
26
+ # The browser only sends http auth data for requests that are explicitly
27
+ # required to do so. This way we get the real values of +#logged_in?+ and
28
+ # +#current_user+
29
+ login_required if session[:user]
30
+ end
31
+
32
+ get "/" do
33
+ @projects = Project.all(authorized? ? {} : { :public => true })
34
+ show :home, :title => "projects"
35
+ end
36
+
37
+ get "/login" do
38
+ login_required
39
+ session[:user] = current_user
40
+ redirect "/"
41
+ end
42
+
43
+ get "/new" do
44
+ login_required
45
+
46
+ @project = Project.new
47
+ show :new, :title => ["projects", "new project"]
48
+ end
49
+
50
+ post "/" do
51
+ login_required
52
+
53
+ @project = Project.new(params[:project_data])
54
+ if @project.save
55
+ @project.enable_notifiers(params["enabled_notifiers[]"], params["notifiers"])
56
+ redirect project_url(@project)
57
+ else
58
+ show :new, :title => ["projects", "new project"]
59
+ end
60
+ end
61
+
62
+ get "/:project" do
63
+ login_required unless current_project.public?
64
+ show :project, :title => ["projects", current_project.permalink]
65
+ end
66
+
67
+ put "/:project" do
68
+ login_required
69
+
70
+ if current_project.update_attributes(params[:project_data])
71
+ current_project.enable_notifiers(params["enabled_notifiers[]"], params["notifiers"])
72
+ redirect project_url(current_project)
73
+ else
74
+ show :new, :title => ["projects", current_project.permalink, "edit"]
75
+ end
76
+ end
77
+
78
+ delete "/:project" do
79
+ login_required
80
+
81
+ current_project.destroy
82
+ redirect "/"
83
+ end
84
+
85
+ get "/:project/edit" do
86
+ login_required
87
+
88
+ show :new, :title => ["projects", current_project.permalink, "edit"]
89
+ end
90
+
91
+ post "/:project/push" do
92
+ content_type 'text/plain'
93
+
94
+ begin
95
+ payload = JSON.parse(params[:payload] || "")
96
+ payload['commits'].reverse.each do |commit|
97
+ current_project.build(commit['id']) if payload['ref'] =~ /#{current_project.branch}/
98
+ end
99
+ 'Thanks, build started.'
100
+ rescue JSON::ParserError => exception
101
+ invalid_payload!(exception.to_s)
102
+ end
103
+ end
104
+
105
+ post "/:project/builds" do
106
+ login_required
107
+
108
+ current_project.build
109
+ redirect project_url(@project)
110
+ end
111
+
112
+ get '/:project/builds/:build' do
113
+ login_required unless current_project.public?
114
+ show :build, :title => ["projects", current_project.permalink, current_build.short_commit_identifier]
115
+ end
116
+
117
+ get "/integrity.css" do
118
+ header "Content-Type" => "text/css; charset=utf-8"
119
+ sass :integrity
120
+ end
121
+
122
+ helpers do
123
+ include Rack::Utils
124
+ include Sinatra::Authorization
125
+ alias_method :h, :escape_html
126
+
127
+ def authorization_realm
128
+ "Integrity"
129
+ end
130
+
131
+ def authorized?
132
+ return true unless Integrity.config[:use_basic_auth]
133
+ !!request.env['REMOTE_USER']
134
+ end
135
+
136
+ def authorize(user, password)
137
+ if Integrity.config[:hash_admin_password]
138
+ password = Digest::SHA1.hexdigest(password)
139
+ end
140
+
141
+ !Integrity.config[:use_basic_auth] ||
142
+ (Integrity.config[:admin_username] == user &&
143
+ Integrity.config[:admin_password] == password)
144
+ end
145
+
146
+ def unauthorized!(realm=authorization_realm)
147
+ header 'WWW-Authenticate' => %(Basic realm="#{realm}")
148
+ throw :halt, [401, show(:unauthorized, :title => "incorrect credentials")]
149
+ end
150
+
151
+ def invalid_payload!(msg=nil)
152
+ throw :halt, [422, msg || 'No payload given']
153
+ end
154
+
155
+ def current_project
156
+ @project ||= Project.first(:permalink => params[:project]) or raise Sinatra::NotFound
157
+ end
158
+
159
+ def current_build
160
+ @build ||= current_project.builds.first(:commit_identifier => params[:build]) or raise Sinatra::NotFound
161
+ end
162
+
163
+ def show(view, options={})
164
+ @title = breadcrumbs(*options[:title])
165
+ haml view
166
+ end
167
+
168
+ def pages
169
+ @pages ||= [["projects", "/"], ["new project", "/new"]]
170
+ end
171
+
172
+ def breadcrumbs(*crumbs)
173
+ crumbs[0..-2].map do |crumb|
174
+ if page_data = pages.detect {|c| c.first == crumb }
175
+ %Q(<a href="#{page_data.last}">#{page_data.first}</a>)
176
+ elsif @project && @project.permalink == crumb
177
+ %Q(<a href="#{project_url(@project)}">#{@project.permalink}</a>)
178
+ end
179
+ end + [crumbs.last]
180
+ end
181
+
182
+ def cycle(*values)
183
+ @cycles ||= {}
184
+ @cycles[values] ||= -1 # first value returned is 0
185
+ next_value = @cycles[values] = (@cycles[values] + 1) % values.size
186
+ values[next_value]
187
+ end
188
+
189
+ def project_url(project, *path)
190
+ "/" << [project.permalink, *path].join("/")
191
+ end
192
+
193
+ def push_url_for(project)
194
+ Addressable::URI.parse(Integrity.config[:base_uri]).join("#{project_url(project)}/push").to_s
195
+ end
196
+
197
+ def build_url(build)
198
+ "/#{build.project.permalink}/builds/#{build.commit_identifier}"
199
+ end
200
+
201
+ def filter_attributes_of(model)
202
+ valid = model.properties.collect {|p| p.name.to_s }
203
+ Hash[*params.dup.select {|k,_| valid.include?(k) }.flatten]
204
+ end
205
+
206
+ def errors_on(object, field)
207
+ return "" unless errors = object.errors.on(field)
208
+ errors.map {|e| e.gsub(/#{field} /i, "") }.join(", ")
209
+ end
210
+
211
+ def error_class(object, field)
212
+ object.errors.on(field).nil? ? "" : "with_errors"
213
+ end
214
+
215
+ def checkbox(name, condition, extras={})
216
+ attrs = { :name => name, :type => "checkbox" }.merge(condition ? { :checked => "checked" } : {})
217
+ attrs.merge(extras)
218
+ end
219
+
220
+ def bash_color_codes(string)
221
+ string.gsub("\e[0m", '</span>').
222
+ gsub("\e[31m", '<span class="color31">').
223
+ gsub("\e[32m", '<span class="color32">').
224
+ gsub("\e[33m", '<span class="color33">').
225
+ gsub("\e[34m", '<span class="color34">').
226
+ gsub("\e[35m", '<span class="color35">').
227
+ gsub("\e[36m", '<span class="color36">').
228
+ gsub("\e[37m", '<span class="color37">')
229
+ end
230
+
231
+ def pretty_date(date_time)
232
+ today = Date.today
233
+ if date_time.day == today.day && date_time.month == today.month && date_time.year == today.year
234
+ "today"
235
+ elsif date_time.day == today.day - 1 && date_time.month == today.month && date_time.year == today.year
236
+ "yesterday"
237
+ else
238
+ date_time.strftime("on %b %d%o")
239
+ end
240
+ end
241
+
242
+ def notifier_form(notifier)
243
+ haml(notifier.to_haml, :layout => :notifier, :locals => {
244
+ :config => current_project.config_for(notifier),
245
+ :notifier => "#{notifier.to_s.split(/::/).last}",
246
+ :enabled => current_project.notifies?(notifier)
247
+ })
248
+ end
249
+ end