integrity 0.1.8

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.
Files changed (57) hide show
  1. data/README.markdown +66 -0
  2. data/Rakefile +165 -0
  3. data/VERSION.yml +4 -0
  4. data/app.rb +138 -0
  5. data/bin/integrity +4 -0
  6. data/config/config.sample.ru +31 -0
  7. data/config/config.sample.yml +38 -0
  8. data/config/thin.sample.yml +13 -0
  9. data/integrity.gemspec +76 -0
  10. data/lib/integrity.rb +80 -0
  11. data/lib/integrity/build.rb +61 -0
  12. data/lib/integrity/core_ext/object.rb +6 -0
  13. data/lib/integrity/core_ext/string.rb +5 -0
  14. data/lib/integrity/helpers.rb +16 -0
  15. data/lib/integrity/helpers/authorization.rb +33 -0
  16. data/lib/integrity/helpers/breadcrumbs.rb +20 -0
  17. data/lib/integrity/helpers/forms.rb +28 -0
  18. data/lib/integrity/helpers/pretty_output.rb +45 -0
  19. data/lib/integrity/helpers/rendering.rb +14 -0
  20. data/lib/integrity/helpers/resources.rb +13 -0
  21. data/lib/integrity/helpers/urls.rb +47 -0
  22. data/lib/integrity/installer.rb +132 -0
  23. data/lib/integrity/migrations.rb +157 -0
  24. data/lib/integrity/notifier.rb +50 -0
  25. data/lib/integrity/notifier/base.rb +55 -0
  26. data/lib/integrity/project.rb +117 -0
  27. data/lib/integrity/project_builder.rb +47 -0
  28. data/lib/integrity/scm.rb +19 -0
  29. data/lib/integrity/scm/git.rb +83 -0
  30. data/lib/integrity/scm/git/uri.rb +57 -0
  31. data/public/buttons.css +82 -0
  32. data/public/reset.css +7 -0
  33. data/public/spinner.gif +0 -0
  34. data/test/helpers.rb +47 -0
  35. data/test/helpers/acceptance.rb +127 -0
  36. data/test/helpers/acceptance/git_helper.rb +99 -0
  37. data/test/helpers/acceptance/textfile_notifier.rb +26 -0
  38. data/test/helpers/expectations.rb +5 -0
  39. data/test/helpers/expectations/be_a.rb +23 -0
  40. data/test/helpers/expectations/change.rb +90 -0
  41. data/test/helpers/expectations/have.rb +105 -0
  42. data/test/helpers/expectations/have_tag.rb +128 -0
  43. data/test/helpers/expectations/predicates.rb +37 -0
  44. data/test/helpers/fixtures.rb +83 -0
  45. data/views/_build_info.haml +18 -0
  46. data/views/build.haml +2 -0
  47. data/views/error.haml +36 -0
  48. data/views/home.haml +23 -0
  49. data/views/integrity.sass +387 -0
  50. data/views/layout.haml +28 -0
  51. data/views/new.haml +51 -0
  52. data/views/not_found.haml +31 -0
  53. data/views/notifier.haml +7 -0
  54. data/views/project.builder +21 -0
  55. data/views/project.haml +28 -0
  56. data/views/unauthorized.haml +38 -0
  57. metadata +258 -0
@@ -0,0 +1,132 @@
1
+ require File.dirname(__FILE__) + "/../integrity"
2
+ require "thor"
3
+
4
+ module Integrity
5
+ class Installer < Thor
6
+ include FileUtils
7
+
8
+ desc "install [PATH]",
9
+ "Copy template files to PATH. Next, go there and edit them."
10
+ def install(path)
11
+ @root = File.expand_path(path)
12
+
13
+ create_dir_structure
14
+ copy_template_files
15
+ edit_template_files
16
+ create_db(root / "config.yml")
17
+ after_setup_message
18
+ end
19
+
20
+ desc "create_db [CONFIG]",
21
+ "Checks the `database_uri` in CONFIG and creates and bootstraps a database for integrity"
22
+ def create_db(config, direction="up")
23
+ Integrity.new(config)
24
+ migrate_db(direction, 1)
25
+ end
26
+
27
+ desc "version",
28
+ "Print the current integrity version"
29
+ def version
30
+ puts Integrity.version
31
+ end
32
+
33
+ private
34
+ attr_reader :root
35
+
36
+ def migrate_db(direction, level=nil)
37
+ require "integrity/migrations"
38
+
39
+ set_up_migrations unless migrations_already_set_up?
40
+ add_initial_migration if tables_from_before_migrations_exist?
41
+
42
+ case direction
43
+ when "up" then Integrity::Migrations.migrate_up!(level)
44
+ when "down" then Integrity::Migrations.migrate_down!(level)
45
+ else raise ArgumentError, "DIRECTION must be either up or down"
46
+ end
47
+ end
48
+
49
+ def create_dir_structure
50
+ mkdir_p root
51
+ mkdir_p root / "builds"
52
+ mkdir_p root / "log"
53
+ mkdir_p root / "public" # this one is to play nice with Passenger
54
+ mkdir_p root / "tmp" # this one is to play nice with Passenger
55
+ end
56
+
57
+ def copy_template_files
58
+ cp Integrity.root / "config" / "config.sample.ru", root / "config.ru"
59
+ cp Integrity.root / "config" / "config.sample.yml", root / "config.yml"
60
+ cp Integrity.root / "config" / "thin.sample.yml", root / "thin.yml"
61
+ end
62
+
63
+ def edit_template_files
64
+ edit_integrity_configuration
65
+ edit_thin_configuration
66
+ end
67
+
68
+ def edit_integrity_configuration
69
+ config = File.read(root / "config.yml")
70
+ config.gsub! %r(sqlite3:///var/integrity.db), "sqlite3://#{root}/integrity.db"
71
+ config.gsub! %r(/path/to/scm/exports), "#{root}/builds"
72
+ config.gsub! %r(/var/log), "#{root}/log"
73
+ File.open(root / "config.yml", "w") { |f| f.puts config }
74
+ end
75
+
76
+ def edit_thin_configuration
77
+ config = File.read(root / "thin.yml")
78
+ config.gsub! %r(/apps/integrity), root
79
+ File.open(root / "thin.yml", 'w') { |f| f.puts config }
80
+ end
81
+
82
+ def after_setup_message
83
+ puts
84
+ puts %Q(Awesome! Integrity was installed successfully!)
85
+ puts
86
+ puts %Q(If you want to enable notifiers, install the gems and then require them)
87
+ puts %Q(in #{root}/config.ru)
88
+ puts
89
+ puts %Q(For example:)
90
+ puts
91
+ puts %Q( sudo gem install -s http://gems.github.com foca-integrity-email)
92
+ puts
93
+ puts %Q(And then in #{root}/config.ru add:)
94
+ puts
95
+ puts %Q( require "notifier/email")
96
+ puts
97
+ puts %Q(Don't forget to tweak #{root / "config.yml"} to your needs.)
98
+ end
99
+
100
+ def set_up_migrations
101
+ database_adapter.execute %q(CREATE TABLE "migration_info" ("migration_name" VARCHAR(255));)
102
+ end
103
+
104
+ def add_initial_migration
105
+ database_adapter.execute %q(INSERT INTO "migration_info" ("migration_name") VALUES ("initial"))
106
+ end
107
+
108
+ def tables_from_before_migrations_exist?
109
+ table_exists?("integrity_projects") &&
110
+ table_exists?("integrity_builds") &&
111
+ table_exists?("integrity_notifiers")
112
+ end
113
+
114
+ def migrations_already_set_up?
115
+ table_exists?("migration_info")
116
+ end
117
+
118
+ def without_pluralizing_table_names
119
+ database_adapter.resource_naming_convention = DataMapper::NamingConventions::Resource::Underscored
120
+ yield
121
+ database_adapter.resource_naming_convention = DataMapper::NamingConventions::Resource::UnderscoredAndPluralized
122
+ end
123
+
124
+ def table_exists?(table_name)
125
+ database_adapter.storage_exists?(table_name)
126
+ end
127
+
128
+ def database_adapter
129
+ DataMapper.repository(:default).adapter
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,157 @@
1
+ require "dm-migrations"
2
+ require "migration_runner"
3
+
4
+ module Integrity
5
+ class Migrations
6
+ include DataMapper::Types
7
+
8
+ # not strictly necessary, but it makes it clear what is going on.
9
+ include DataMapper::MigrationRunner
10
+
11
+ migration 1, :initial, :verbose => false do
12
+ up do
13
+ create_table :integrity_projects do
14
+ column :id, Integer, :serial => true
15
+ column :name, String, :nullable => false
16
+ column :permalink, String
17
+ column :uri, URI, :nullable => false
18
+ column :branch, String, :nullable => false, :default => "master"
19
+ column :command, String, :nullable => false, :default => "rake"
20
+ column :public, Boolean, :default => true
21
+ column :building, Boolean, :default => false
22
+ column :created_at, DateTime
23
+ column :updated_at, DateTime
24
+
25
+ column :build_id, Integer
26
+ column :notifier_id, Integer
27
+ end
28
+
29
+ create_table :integrity_builds do
30
+ column :id, Integer, :serial => true
31
+ column :output, Text, :nullable => false, :default => ""
32
+ column :successful, Boolean, :nullable => false, :default => false
33
+ column :commit_identifier, String, :nullable => false
34
+ column :commit_metadata, Yaml, :nullable => false
35
+ column :created_at, DateTime
36
+ column :updated_at, DateTime
37
+
38
+ column :project_id, Integer
39
+ end
40
+
41
+ create_table :integrity_notifiers do
42
+ column :id, Integer, :serial => true
43
+ column :name, String, :nullable => false
44
+ column :config, Yaml, :nullable => false
45
+
46
+ column :project_id, Integer
47
+ end
48
+ end
49
+
50
+ down do
51
+ drop_table :integrity_notifiers
52
+ drop_table :integrity_projects
53
+ drop_table :integrity_builds
54
+ end
55
+ end
56
+
57
+ migration 2, :add_commits, :verbose => false do
58
+ up do
59
+ class ::Integrity::Build
60
+ property :commit_identifier, String
61
+ property :commit_metadata, Yaml, :lazy => false
62
+ property :project_id, Integer
63
+ end
64
+
65
+ create_table :integrity_commits do
66
+ column :id, Integer, :serial => true
67
+ column :identifier, String, :nullable => false
68
+ column :message, String, :nullable => false, :length => 255
69
+ column :author, String, :nullable => false, :length => 255
70
+ column :committed_at, DateTime, :nullable => false
71
+ column :created_at, DateTime
72
+ column :updated_at, DateTime
73
+
74
+ column :project_id, Integer
75
+ end
76
+
77
+ modify_table :integrity_builds do
78
+ add_column :commit_id, Integer
79
+ add_column :started_at, DateTime
80
+ add_column :completed_at, DateTime
81
+ end
82
+
83
+ # Die, orphans, die
84
+ Build.all(:project_id => nil).destroy!
85
+
86
+ # sqlite hodgepockery
87
+ all_builds = Build.all.each {|b| b.freeze }
88
+ drop_table :integrity_builds
89
+ create_table :integrity_builds do
90
+ column :id, Integer, :serial => true
91
+ column :started_at, DateTime
92
+ column :completed_at, DateTime
93
+ column :successful, Boolean
94
+ column :output, Text, :nullable => false, :default => ""
95
+ column :created_at, DateTime
96
+ column :updated_at, DateTime
97
+
98
+ column :commit_id, Integer
99
+ end
100
+
101
+ all_builds.each do |build|
102
+ commit = Commit.first(:identifier => build.commit_identifier)
103
+
104
+ if commit.nil?
105
+ commit = Commit.create(:identifier => build.commit_identifier,
106
+ :message => build.commit_metadata[:message],
107
+ :author => build.commit_metadata[:author],
108
+ :committed_at => build.commit_metadata[:date],
109
+ :project_id => build.project_id)
110
+ end
111
+
112
+ Build.create(:commit_id => commit.id,
113
+ :started_at => build.created_at,
114
+ :completed_at => build.updated_at,
115
+ :successful => build.successful,
116
+ :output => build.output)
117
+ end
118
+ end
119
+
120
+ down do
121
+ modify_table :integrity_builds do
122
+ add_column :commit_identifier, String, :nullable => false
123
+ add_column :commit_metadata, Yaml, :nullable => false
124
+ add_column :project_id, Integer
125
+ end
126
+
127
+ # sqlite hodgepockery
128
+ all_builds = Build.all.map {|b| b.freeze }
129
+ drop_table :integrity_builds
130
+ create_table :integrity_builds do
131
+ column :id, Integer, :serial => true
132
+ column :output, Text, :nullable => false, :default => ""
133
+ column :successful, Boolean, :nullable => false, :default => false
134
+ column :commit_identifier, String, :nullable => false
135
+ column :commit_metadata, Yaml, :nullable => false
136
+ column :created_at, DateTime
137
+ column :updated_at, DateTime
138
+ column :project_id, Integer
139
+ end
140
+
141
+ all_builds.each do |build|
142
+ Build.create(:project_id => build.commit.project_id,
143
+ :output => build.output,
144
+ :successful => build.successful,
145
+ :commit_identifier => build.commit.identifier,
146
+ :commit_metadata => {
147
+ :message => build.commit.message,
148
+ :author => build.commit.author.full,
149
+ :date => commit.committed_at
150
+ }.to_yaml)
151
+ end
152
+
153
+ drop_table :commits
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,50 @@
1
+ module Integrity
2
+ class Notifier
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
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,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,117 @@
1
+ module Integrity
2
+ class Project
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
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 :building, Boolean, :default => false
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 self.only_public_unless(condition)
25
+ if condition
26
+ all
27
+ else
28
+ all(:public => true)
29
+ end
30
+ end
31
+
32
+ def build(commit_identifier="HEAD")
33
+ return if building?
34
+ update_attributes(:building => true)
35
+ ProjectBuilder.new(self).build(commit_identifier)
36
+ ensure
37
+ update_attributes(:building => false)
38
+ send_notifications
39
+ end
40
+
41
+ def push(payload)
42
+ payload = JSON.parse(payload || "")
43
+
44
+ if Integrity.config[:build_all_commits]
45
+ payload["commits"].sort_by { |commit| Time.parse(commit["timestamp"]) }.each do |commit|
46
+ build(commit["id"]) if payload["ref"] =~ /#{branch}/
47
+ end
48
+ else
49
+ build(payload["after"]) if payload["ref"] =~ /#{branch}/
50
+ end
51
+ end
52
+
53
+ def last_build
54
+ all_builds.first
55
+ end
56
+
57
+ def previous_builds
58
+ all_builds.tap {|builds| builds.shift }
59
+ end
60
+
61
+ def status
62
+ last_build && last_build.status
63
+ end
64
+
65
+ def public=(flag)
66
+ attribute_set(:public, case flag
67
+ when "1", "0" then flag == "1"
68
+ else !!flag
69
+ end)
70
+ end
71
+
72
+ def config_for(notifier)
73
+ notifier = notifiers.first(:name => notifier.to_s.split(/::/).last)
74
+ notifier.blank? ? {} : notifier.config
75
+ end
76
+
77
+ def notifies?(notifier)
78
+ !notifiers.first(:name => notifier.to_s.split(/::/).last).blank?
79
+ end
80
+
81
+ def enable_notifiers(*args)
82
+ Notifier.enable_notifiers(id, *args)
83
+ end
84
+
85
+ private
86
+ def set_permalink
87
+ self.permalink = (name || "").downcase.
88
+ gsub(/'s/, "s").
89
+ gsub(/&/, "and").
90
+ gsub(/[^a-z0-9]+/, "-").
91
+ gsub(/-*$/, "")
92
+ end
93
+
94
+ def delete_code
95
+ builds.destroy!
96
+ ProjectBuilder.new(self).delete_code
97
+ rescue SCM::SCMUnknownError => error
98
+ Integrity.log "Problem while trying to deleting code: #{error}"
99
+ end
100
+
101
+ def send_notifications
102
+ notifiers.each do |notifier|
103
+ begin
104
+ Integrity.log "Notifying of build #{last_build.short_commit_identifier} using the #{notifier.name} notifier"
105
+ notifier.notify_of_build last_build
106
+ rescue Timeout::Error
107
+ Integrity.log "#{notifier.name} notifier timed out"
108
+ next
109
+ end
110
+ end
111
+ end
112
+
113
+ def all_builds
114
+ builds.all.sort_by {|b| b.commited_at }.reverse
115
+ end
116
+ end
117
+ end