bantic-integrity 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/integrity.gemspec ADDED
@@ -0,0 +1,72 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{integrity}
3
+ s.version = "0.1.4"
4
+
5
+
6
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
7
+ s.authors = ["Nicol\303\241s Sanguinetti", "Simon Rozet"]
8
+ s.date = %q{2008-12-08}
9
+ s.default_executable = %q{integrity}
10
+ s.description = %q{Your Friendly Continuous Integration server. Easy, fun and painless!}
11
+ s.email = %q{contacto@nicolassanguinetti.info}
12
+ s.executables = ["integrity"]
13
+ s.files = ["README.markdown", "Rakefile", "VERSION.yml", "app.rb", "bin/integrity", "config/config.sample.ru", "config/config.sample.yml", "config/thin.sample.yml", "integrity.gemspec", "lib/integrity.rb", "lib/integrity/build.rb", "lib/integrity/builder.rb", "lib/integrity/core_ext/object.rb", "lib/integrity/core_ext/string.rb", "lib/integrity/core_ext/time.rb", "lib/integrity/notifier.rb", "lib/integrity/notifier/base.rb", "lib/integrity/project.rb", "lib/integrity/scm.rb", "lib/integrity/scm/git.rb", "lib/integrity/scm/git/uri.rb", "public/buttons.css", "public/reset.css", "public/spinner.gif", "vendor/sinatra-hacks/lib/hacks.rb", "views/build.haml", "views/build_info.haml", "views/error.haml", "views/home.haml", "views/integrity.sass", "views/layout.haml", "views/new.haml", "views/not_found.haml", "views/notifier.haml", "views/project.haml", "views/unauthorized.haml", "spec/spec_helper.rb", "spec/form_field_matchers.rb"]
14
+ s.homepage = %q{http://integrityapp.com}
15
+ s.post_install_message = %q{Run `integrity help` for information on how to setup Integrity.}
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{integrity}
18
+ s.rubygems_version = %q{1.2.0}
19
+ s.summary = %q{The easy and fun Continuous Integration server}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 2
24
+
25
+ if current_version >= 3 then
26
+ s.add_runtime_dependency(%q<sinatra>, [">= 0.3.2"])
27
+ s.add_runtime_dependency(%q<haml>, [">= 0"])
28
+ s.add_runtime_dependency(%q<dm-core>, [">= 0.9.5"])
29
+ s.add_runtime_dependency(%q<dm-validations>, [">= 0.9.5"])
30
+ s.add_runtime_dependency(%q<dm-types>, [">= 0.9.5"])
31
+ s.add_runtime_dependency(%q<dm-timestamps>, [">= 0.9.5"])
32
+ s.add_runtime_dependency(%q<dm-aggregates>, [">= 0.9.5"])
33
+ s.add_runtime_dependency(%q<data_objects>, [">= 0.9.5"])
34
+ s.add_runtime_dependency(%q<do_sqlite3>, [">= 0.9.5"])
35
+ s.add_runtime_dependency(%q<json>, [">= 0"])
36
+ s.add_runtime_dependency(%q<foca-sinatra-diddies>, [">= 0.0.2"])
37
+ s.add_runtime_dependency(%q<rspec_hpricot_matchers>, [">= 0"])
38
+ s.add_runtime_dependency(%q<thor>, [">= 0"])
39
+ s.add_runtime_dependency(%q<bcrypt-ruby>, [">= 0"])
40
+ else
41
+ s.add_dependency(%q<sinatra>, [">= 0.3.2"])
42
+ s.add_dependency(%q<haml>, [">= 0"])
43
+ s.add_dependency(%q<dm-core>, [">= 0.9.5"])
44
+ s.add_dependency(%q<dm-validations>, [">= 0.9.5"])
45
+ s.add_dependency(%q<dm-types>, [">= 0.9.5"])
46
+ s.add_dependency(%q<dm-timestamps>, [">= 0.9.5"])
47
+ s.add_dependency(%q<dm-aggregates>, [">= 0.9.5"])
48
+ s.add_dependency(%q<data_objects>, [">= 0.9.5"])
49
+ s.add_dependency(%q<do_sqlite3>, [">= 0.9.5"])
50
+ s.add_dependency(%q<json>, [">= 0"])
51
+ s.add_dependency(%q<foca-sinatra-diddies>, [">= 0.0.2"])
52
+ s.add_dependency(%q<rspec_hpricot_matchers>, [">= 0"])
53
+ s.add_dependency(%q<thor>, [">= 0"])
54
+ s.add_dependency(%q<bcrypt-ruby>, [">= 0"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<sinatra>, [">= 0.3.2"])
58
+ s.add_dependency(%q<haml>, [">= 0"])
59
+ s.add_dependency(%q<dm-core>, [">= 0.9.5"])
60
+ s.add_dependency(%q<dm-validations>, [">= 0.9.5"])
61
+ s.add_dependency(%q<dm-types>, [">= 0.9.5"])
62
+ s.add_dependency(%q<dm-timestamps>, [">= 0.9.5"])
63
+ s.add_dependency(%q<dm-aggregates>, [">= 0.9.5"])
64
+ s.add_dependency(%q<data_objects>, [">= 0.9.5"])
65
+ s.add_dependency(%q<do_sqlite3>, [">= 0.9.5"])
66
+ s.add_dependency(%q<json>, [">= 0"])
67
+ s.add_dependency(%q<foca-sinatra-diddies>, [">= 0.0.2"])
68
+ s.add_dependency(%q<rspec_hpricot_matchers>, [">= 0"])
69
+ s.add_dependency(%q<thor>, [">= 0"])
70
+ s.add_dependency(%q<bcrypt-ruby>, [">= 0"])
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ require 'ostruct'
2
+
3
+ module Integrity
4
+ class Build
5
+ include DataMapper::Resource
6
+
7
+ property :id, Integer, :serial => true
8
+ property :output, Text, :nullable => false, :default => ''
9
+ property :successful, Boolean, :nullable => false, :default => false
10
+ property :commit_identifier, String, :nullable => false
11
+ property :commit_metadata, Yaml, :nullable => false, :lazy => false
12
+ property :created_at, DateTime
13
+ property :updated_at, DateTime
14
+
15
+ belongs_to :project, :class_name => "Integrity::Project"
16
+
17
+ def failed?
18
+ !successful?
19
+ end
20
+
21
+ def status
22
+ successful? ? :success : :failed
23
+ end
24
+
25
+ def human_readable_status
26
+ successful? ? 'Build Successful' : 'Build Failed'
27
+ end
28
+
29
+ def short_commit_identifier
30
+ sha1?(commit_identifier) ? commit_identifier[0..6] : commit_identifier
31
+ end
32
+
33
+ def commit_author
34
+ @author ||= begin
35
+ commit_metadata[:author] =~ /^(.*) <(.*)>$/
36
+ OpenStruct.new(:name => $1.strip, :email => $2.strip, :full => commit_metadata[:author])
37
+ end
38
+ end
39
+
40
+ def commit_message
41
+ commit_metadata[:message]
42
+ end
43
+
44
+ def commited_at
45
+ case commit_metadata[:date]
46
+ when String then Time.parse(commit_metadata[:date])
47
+ else commit_metadata[:date]
48
+ end
49
+ end
50
+
51
+ private
52
+ def sha1?(string)
53
+ string =~ /^[a-f0-9]{40}$/
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ require 'fileutils'
2
+
3
+ module Integrity
4
+ class Builder
5
+ attr_reader :build_script
6
+
7
+ def initialize(project)
8
+ @uri = project.uri
9
+ @build_script = project.command
10
+ @branch = project.branch
11
+ @scm = SCM.new(@uri, @branch, export_directory)
12
+ @build = Build.new(:project => project)
13
+ end
14
+
15
+ def build(commit)
16
+ Integrity.log "Building #{commit} (#{@branch}) of #{@build.project.name} in #{export_directory} using #{scm_name}"
17
+ @scm.with_revision(commit) { run_build_script }
18
+ @build
19
+ ensure
20
+ @build.commit_identifier = @scm.commit_identifier(commit)
21
+ @build.commit_metadata = @scm.commit_metadata(commit)
22
+ @build.save
23
+ end
24
+
25
+ def delete_code
26
+ FileUtils.rm_r export_directory
27
+ rescue Errno::ENOENT
28
+ nil
29
+ end
30
+
31
+ private
32
+ def export_directory
33
+ Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
34
+ end
35
+
36
+ def scm_name
37
+ @scm.name
38
+ end
39
+
40
+ def run_build_script
41
+ Integrity.log "Running `#{build_script}` in #{@scm.working_directory}"
42
+
43
+ IO.popen "(cd #{@scm.working_directory} && #{build_script}) 2>&1", "r" do |pipe|
44
+ @build.output = pipe.read
45
+ end
46
+ @build.successful = $?.success?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ class Object
2
+ def tap
3
+ yield self
4
+ self
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def /(other)
3
+ File.join(self, other)
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ class Time
2
+ alias :strftime_without_ordinals :strftime
3
+ def strftime(format_string)
4
+ format_string.gsub! "%o", case day
5
+ when 1, 21, 31 then "st"
6
+ when 2, 22 then "nd"
7
+ when 3, 23 then "rd"
8
+ else "th"
9
+ end
10
+
11
+ strftime_without_ordinals(format_string)
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ module Integrity
2
+ class Notifier
3
+ class Base
4
+ def self.notify_of_build(build, config)
5
+ Timeout.timeout(8) { new(build, config).deliver! }
6
+ end
7
+
8
+ def self.to_haml
9
+ raise NoMethodError, "you need to implement this method in your notifier"
10
+ end
11
+
12
+ attr_reader :build
13
+
14
+ def initialize(build, config)
15
+ @build = build
16
+ @config = config
17
+ end
18
+
19
+ def deliver!
20
+ raise NoMethodError, "you need to implement this method in your notifier"
21
+ end
22
+
23
+ def short_message
24
+ "Build #{build.short_commit_identifier} #{build.successful? ? "was successful" : "failed"}"
25
+ end
26
+
27
+ def full_message
28
+ <<-EOM
29
+ "Build #{build.commit_identifier} #{build.successful? ? "was successful" : "failed"}"
30
+
31
+ Commit Message: #{build.commit_message}
32
+ Commit Date: #{build.commited_at}
33
+ Commit Author: #{build.commit_author.name}
34
+
35
+ Link: #{build_url}
36
+
37
+ Build Output:
38
+
39
+ #{stripped_build_output}
40
+ EOM
41
+ end
42
+
43
+ def build_url
44
+ raise if Integrity.config[:base_uri].nil?
45
+ Integrity.config[:base_uri] / build.project.permalink / "builds" / build.commit_identifier
46
+ end
47
+
48
+ private
49
+
50
+ def stripped_build_output
51
+ build.output.gsub("\e[0m", '').gsub(/\e\[3[1-7]m/, '')
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ module Integrity
2
+ class Notifier
3
+ include DataMapper::Resource
4
+
5
+ property :id, Integer, :serial => true
6
+ property :name, String, :nullable => false
7
+ property :config, Yaml, :nullable => false, :lazy => false
8
+
9
+ belongs_to :project, :class_name => "Integrity::Project"
10
+
11
+ validates_is_unique :name, :scope => :project_id
12
+ validates_present :project_id
13
+
14
+ def self.available
15
+ @available ||= constants.map { |name| const_get(name) }.select { |notifier| valid_notifier?(notifier) }
16
+ end
17
+
18
+ def self.enable_notifiers(project, enabled, config={})
19
+ all(:project_id => project).destroy!
20
+ list_of_enabled_notifiers(enabled).each do |name|
21
+ create! :project_id => project, :name => name, :config => config[name]
22
+ end
23
+
24
+ end
25
+
26
+ def notify_of_build(build)
27
+ to_const.notify_of_build(build, config)
28
+ end
29
+
30
+ private
31
+
32
+ def to_const
33
+ self.class.module_eval(name)
34
+ end
35
+
36
+ def self.list_of_enabled_notifiers(names)
37
+ [*names].reject { |n| n.nil? }
38
+ end
39
+ private_class_method :list_of_enabled_notifiers
40
+
41
+ def self.valid_notifier?(notifier)
42
+ notifier.respond_to?(:to_haml) && notifier.respond_to?(:notify_of_build) && notifier != Notifier::Base
43
+ end
44
+ private_class_method :valid_notifier?
45
+ end
46
+ end
47
+
48
+ require File.dirname(__FILE__) / 'notifier' / 'base'
49
+
50
+ Dir["#{File.dirname(__FILE__)}/notifier/*.rb"].each &method(:require)
@@ -0,0 +1,94 @@
1
+ module Integrity
2
+ class Project
3
+ include DataMapper::Resource
4
+
5
+ property :id, Integer, :serial => true
6
+ property :name, String, :nullable => false
7
+ property :permalink, String
8
+ property :uri, URI, :nullable => false, :length => 255
9
+ property :branch, String, :nullable => false, :default => "master"
10
+ property :command, String, :nullable => false, :length => 255, :default => "rake"
11
+ property :public, Boolean, :default => true
12
+ property :started_build_at, DateTime
13
+ property :created_at, DateTime
14
+ property :updated_at, DateTime
15
+
16
+ has n, :builds, :class_name => "Integrity::Build"
17
+ has n, :notifiers, :class_name => "Integrity::Notifier"
18
+
19
+ before :save, :set_permalink
20
+ before :destroy, :delete_code
21
+
22
+ validates_is_unique :name
23
+
24
+ def build(commit_identifier="HEAD")
25
+ return if building?
26
+ update_attributes(:started_build_at => Time.now)
27
+ Builder.new(self).build(commit_identifier)
28
+ ensure
29
+ update_attributes(:started_build_at => nil)
30
+ send_notifications
31
+ end
32
+
33
+ def building?
34
+ !started_build_at.nil?
35
+ end
36
+
37
+ def last_build
38
+ builds.last
39
+ end
40
+
41
+ def previous_builds
42
+ builds.all(:order => [:created_at.desc]).tap do |builds|
43
+ builds.shift
44
+ end
45
+ end
46
+
47
+ def status
48
+ last_build && last_build.status
49
+ end
50
+
51
+ def public=(flag)
52
+ attribute_set(:public, !!flag)
53
+ end
54
+
55
+ def config_for(notifier)
56
+ notifier = notifiers.first(:name => notifier.to_s.split(/::/).last)
57
+ notifier.blank? ? {} : notifier.config
58
+ end
59
+
60
+ def notifies?(notifier)
61
+ !notifiers.first(:name => notifier.to_s.split(/::/).last).blank?
62
+ end
63
+
64
+ def enable_notifiers(*args)
65
+ Notifier.enable_notifiers(id, *args)
66
+ end
67
+
68
+ private
69
+ def set_permalink
70
+ self.permalink = (name || "").downcase.
71
+ gsub(/'s/, "s").
72
+ gsub(/&/, "and").
73
+ gsub(/[^a-z0-9]+/, "-").
74
+ gsub(/-*$/, "")
75
+ end
76
+
77
+ def delete_code
78
+ builds.destroy!
79
+ Builder.new(self).delete_code
80
+ end
81
+
82
+ def send_notifications
83
+ notifiers.each do |notifier|
84
+ begin
85
+ Integrity.log "Notifying of build #{last_build.short_commit_identifier} using the #{notifier.name} notifier"
86
+ notifier.notify_of_build last_build
87
+ rescue Timeout::Error
88
+ Integrity.log "#{notifier.name} notifier timed out"
89
+ next
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,57 @@
1
+ module Integrity
2
+ module SCM
3
+ class Git
4
+ # From the git-pull man page:
5
+ #
6
+ # GIT URLS
7
+ # One of the following notations can be used to name the remote repository:
8
+ #
9
+ # rsync://host.xz/path/to/repo.git/
10
+ # http://host.xz/path/to/repo.git/
11
+ # git://host.xz/~user/path/to/repo.git/
12
+ # ssh://[user@]host.xz[:port]/path/to/repo.git/
13
+ # ssh://[user@]host.xz/path/to/repo.git/
14
+ # ssh://[user@]host.xz/~user/path/to/repo.git/
15
+ # ssh://[user@]host.xz/~/path/to/repo.git
16
+ #
17
+ # SSH is the default transport protocol over the network. You can optionally
18
+ # specify which user to log-in as, and an alternate, scp-like syntax is also
19
+ # supported
20
+ #
21
+ # Both syntaxes support username expansion, as does the native git protocol,
22
+ # but only the former supports port specification. The following three are
23
+ # identical to the last three above, respectively:
24
+ #
25
+ # [user@]host.xz:/path/to/repo.git/
26
+ # [user@]host.xz:~user/path/to/repo.git/
27
+ # [user@]host.xz:path/to/repo.git
28
+ #
29
+ class URI
30
+ def initialize(uri_string)
31
+ @uri = Addressable::URI.parse(uri_string)
32
+ end
33
+
34
+ def working_tree_path
35
+ strip_extension(path).gsub("/", "-")
36
+ end
37
+
38
+ private
39
+
40
+ def strip_extension(string)
41
+ uri = Pathname.new(string)
42
+ if uri.extname.any?
43
+ uri = Pathname.new(string)
44
+ string.gsub(Regexp.new("#{uri.extname}\/?"), "")
45
+ else
46
+ string
47
+ end
48
+ end
49
+
50
+ def path
51
+ path = @uri.path
52
+ path.gsub(/\~[a-zA-Z0-9]*\//, "").gsub(/^\//, "")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,83 @@
1
+ module Integrity
2
+ module SCM
3
+ class Git
4
+ require File.dirname(__FILE__) / "git/uri"
5
+
6
+ attr_reader :uri, :branch, :working_directory
7
+
8
+ def self.working_tree_path(uri)
9
+ Git::URI.new(uri).working_tree_path
10
+ end
11
+
12
+ def initialize(uri, branch, working_directory)
13
+ @uri = uri.to_s
14
+ @branch = branch.to_s
15
+ @working_directory = working_directory
16
+ end
17
+
18
+ def with_revision(revision)
19
+ fetch_code
20
+ checkout(revision)
21
+ yield
22
+ end
23
+
24
+ def commit_identifier(sha1)
25
+ `cd #{working_directory} && git show -s --pretty=format:%H #{sha1}`.chomp
26
+ end
27
+
28
+ def commit_metadata(sha1)
29
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:date: %ci%n)
30
+ YAML.load(`cd #{working_directory} && git show -s --pretty=format:"#{format}" #{sha1}`)
31
+ end
32
+
33
+ def name
34
+ self.class.name.split("::").last
35
+ end
36
+
37
+ private
38
+
39
+ def fetch_code
40
+ clone unless cloned?
41
+ checkout unless on_branch?
42
+ pull
43
+ end
44
+
45
+ def clone
46
+ log "Cloning #{uri} to #{working_directory}"
47
+ `git clone #{uri} #{working_directory}`
48
+ end
49
+
50
+ def checkout(treeish=nil)
51
+ strategy = case
52
+ when treeish then treeish
53
+ when local_branches.include?(branch) then branch
54
+ else "-b #{branch} origin/#{branch}"
55
+ end
56
+
57
+ log "Checking-out #{strategy}"
58
+ `cd #{working_directory} && git checkout #{strategy}`
59
+ end
60
+
61
+ def pull
62
+ log "Pull-ing in #{working_directory}"
63
+ `cd #{working_directory} && git pull`
64
+ end
65
+
66
+ def local_branches
67
+ `cd #{working_directory} && git branch`.split("\n").map {|b| b.delete("*").strip }
68
+ end
69
+
70
+ def cloned?
71
+ File.directory?(working_directory / ".git")
72
+ end
73
+
74
+ def on_branch?
75
+ File.basename(`cd #{working_directory} && git symbolic-ref HEAD`).chomp == branch
76
+ end
77
+
78
+ def log(message)
79
+ Integrity.log("Git") { message }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,22 @@
1
+ module Integrity
2
+ module SCM
3
+ class SCMUnknownError < StandardError; end
4
+
5
+ def self.new(uri, *args)
6
+ scm_class_for(uri).new(uri, *args)
7
+ end
8
+
9
+ def self.working_tree_path(uri)
10
+ scm_class_for(uri).working_tree_path(uri)
11
+ end
12
+
13
+ private
14
+
15
+ def self.scm_class_for(string)
16
+ case string.to_s
17
+ when /\.git\/?/ then Git
18
+ else raise SCMUnknownError, "could not find any SCM based on string '#{string}'"
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/integrity.rb ADDED
@@ -0,0 +1,71 @@
1
+ __DIR__ = File.dirname(__FILE__)
2
+ $:.unshift "#{__DIR__}/integrity", *Dir["#{__DIR__}/../vendor/**/lib"].to_a
3
+
4
+ require 'rubygems'
5
+ require 'json'
6
+ require 'dm-core'
7
+ require 'dm-validations'
8
+ require 'dm-types'
9
+ require 'dm-timestamps'
10
+ require 'dm-aggregates'
11
+
12
+ require 'yaml'
13
+ require 'logger'
14
+ require 'digest/sha1'
15
+
16
+ require 'core_ext/object'
17
+ require 'core_ext/string'
18
+ require 'core_ext/time'
19
+
20
+ require 'project'
21
+ require 'build'
22
+ require 'builder'
23
+ require 'scm'
24
+ require 'scm/git'
25
+ require 'notifier'
26
+
27
+ module Integrity
28
+ def self.new(config_file = nil)
29
+ self.config = config_file unless config_file.nil?
30
+ DataMapper.setup(:default, config[:database_uri])
31
+ end
32
+
33
+ def self.root
34
+ File.expand_path(File.join(File.dirname(__FILE__), ".."))
35
+ end
36
+
37
+ def self.default_configuration
38
+ @defaults ||= { :database_uri => 'sqlite3::memory:',
39
+ :export_directory => root / 'exports',
40
+ :log => STDOUT,
41
+ :base_uri => 'http://localhost:8910',
42
+ :use_basic_auth => false,
43
+ :build_all_commits => true}
44
+ end
45
+
46
+ def self.config
47
+ @config ||= default_configuration
48
+ end
49
+
50
+ def self.config=(file)
51
+ @config = default_configuration.merge(YAML.load_file(file))
52
+ end
53
+
54
+ def self.log(message, &block)
55
+ logger.info(message, &block)
56
+ end
57
+
58
+ def self.logger
59
+ @logger ||= Logger.new(config[:log], "daily").tap do |logger|
60
+ logger.formatter = LogFormatter.new
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ class LogFormatter < Logger::Formatter
67
+ def call(severity, time, progname, msg)
68
+ time.strftime("[%H:%M:%S] ") + msg2str(msg) + "\n"
69
+ end
70
+ end
71
+ end