foca-integrity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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