foca-integrity 0.1.0

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