bantic-integrity 0.1.4

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.
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