integrity 0.1.8 → 0.1.9.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.
Files changed (77) hide show
  1. data/README.markdown +7 -0
  2. data/Rakefile +77 -124
  3. data/config/config.ru +29 -0
  4. data/config/config.sample.ru +6 -16
  5. data/config/config.sample.yml +15 -12
  6. data/config/config.yml +34 -0
  7. data/lib/integrity.rb +13 -13
  8. data/lib/integrity/app.rb +138 -0
  9. data/lib/integrity/author.rb +39 -0
  10. data/lib/integrity/build.rb +54 -31
  11. data/lib/integrity/commit.rb +71 -0
  12. data/lib/integrity/helpers.rb +3 -3
  13. data/lib/integrity/helpers/authorization.rb +2 -2
  14. data/lib/integrity/helpers/forms.rb +3 -3
  15. data/lib/integrity/helpers/pretty_output.rb +1 -1
  16. data/lib/integrity/helpers/rendering.rb +6 -1
  17. data/lib/integrity/helpers/resources.rb +9 -3
  18. data/lib/integrity/helpers/urls.rb +15 -13
  19. data/lib/integrity/installer.rb +43 -60
  20. data/lib/integrity/migrations.rb +31 -48
  21. data/lib/integrity/notifier.rb +14 -16
  22. data/lib/integrity/notifier/base.rb +29 -19
  23. data/lib/integrity/notifier/test_helpers.rb +100 -0
  24. data/lib/integrity/project.rb +69 -33
  25. data/lib/integrity/project_builder.rb +23 -14
  26. data/lib/integrity/scm/git.rb +15 -14
  27. data/lib/integrity/scm/git/uri.rb +9 -9
  28. data/test/acceptance/api_test.rb +97 -0
  29. data/test/acceptance/browse_project_builds_test.rb +65 -0
  30. data/test/acceptance/browse_project_test.rb +95 -0
  31. data/test/acceptance/build_notifications_test.rb +42 -0
  32. data/test/acceptance/create_project_test.rb +97 -0
  33. data/test/acceptance/delete_project_test.rb +53 -0
  34. data/test/acceptance/edit_project_test.rb +117 -0
  35. data/test/acceptance/error_page_test.rb +18 -0
  36. data/test/acceptance/helpers.rb +2 -0
  37. data/test/acceptance/installer_test.rb +62 -0
  38. data/test/acceptance/manual_build_project_test.rb +82 -0
  39. data/test/acceptance/notifier_test.rb +109 -0
  40. data/test/acceptance/project_syndication_test.rb +30 -0
  41. data/test/acceptance/stylesheet_test.rb +18 -0
  42. data/test/helpers.rb +59 -26
  43. data/test/helpers/acceptance.rb +19 -65
  44. data/test/helpers/acceptance/email_notifier.rb +55 -0
  45. data/test/helpers/acceptance/git_helper.rb +15 -15
  46. data/test/helpers/acceptance/textfile_notifier.rb +3 -3
  47. data/test/helpers/expectations.rb +0 -1
  48. data/test/helpers/expectations/be_a.rb +4 -4
  49. data/test/helpers/expectations/change.rb +5 -5
  50. data/test/helpers/expectations/have.rb +4 -4
  51. data/test/helpers/expectations/predicates.rb +4 -4
  52. data/test/helpers/fixtures.rb +44 -18
  53. data/test/helpers/initial_migration_fixture.sql +44 -0
  54. data/test/unit/build_test.rb +51 -0
  55. data/test/unit/commit_test.rb +83 -0
  56. data/test/unit/helpers_test.rb +56 -0
  57. data/test/unit/integrity_test.rb +18 -0
  58. data/test/unit/migrations_test.rb +56 -0
  59. data/test/unit/notifier_test.rb +123 -0
  60. data/test/unit/project_builder_test.rb +108 -0
  61. data/test/unit/project_test.rb +282 -0
  62. data/test/unit/scm_test.rb +54 -0
  63. data/views/_commit_info.haml +24 -0
  64. data/views/build.haml +2 -2
  65. data/views/error.haml +4 -3
  66. data/views/home.haml +3 -5
  67. data/views/integrity.sass +19 -6
  68. data/views/new.haml +6 -6
  69. data/views/project.builder +9 -9
  70. data/views/project.haml +14 -12
  71. metadata +89 -122
  72. data/VERSION.yml +0 -4
  73. data/app.rb +0 -138
  74. data/integrity.gemspec +0 -76
  75. data/lib/integrity/core_ext/string.rb +0 -5
  76. data/test/helpers/expectations/have_tag.rb +0 -128
  77. data/views/_build_info.haml +0 -18
@@ -2,7 +2,7 @@ module Integrity
2
2
  class Project
3
3
  include DataMapper::Resource
4
4
 
5
- property :id, Serial
5
+ property :id, Integer, :serial => true
6
6
  property :name, String, :nullable => false
7
7
  property :permalink, String
8
8
  property :uri, URI, :nullable => false, :length => 255
@@ -13,7 +13,7 @@ module Integrity
13
13
  property :created_at, DateTime
14
14
  property :updated_at, DateTime
15
15
 
16
- has n, :builds, :class_name => "Integrity::Build"
16
+ has n, :commits, :class_name => "Integrity::Commit"
17
17
  has n, :notifiers, :class_name => "Integrity::Notifier"
18
18
 
19
19
  before :save, :set_permalink
@@ -30,36 +30,52 @@ module Integrity
30
30
  end
31
31
 
32
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
33
+ commit_identifier = head_of_remote_repo if commit_identifier == "HEAD"
34
+ commit = find_or_create_commit_with_identifier(commit_identifier)
35
+ commit.queue_build
39
36
  end
40
37
 
41
38
  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}/
39
+ payload = parse_payload(payload)
40
+ raise ArgumentError unless valid_payload?(payload)
41
+
42
+ commits =
43
+ if Integrity.config[:build_all_commits]
44
+ payload["commits"]
45
+ else
46
+ [ payload["commits"].first ]
47
47
  end
48
- else
49
- build(payload["after"]) if payload["ref"] =~ /#{branch}/
48
+
49
+ commits.each do |commit_data|
50
+ create_commit_from(commit_data)
51
+ build(commit_data["id"])
50
52
  end
51
53
  end
52
54
 
55
+ def last_commit
56
+ commits.first(:project_id => id, :order => [:committed_at.desc])
57
+ end
58
+
53
59
  def last_build
54
- all_builds.first
60
+ warn "Project#last_build is deprecated, use Project#last_commit"
61
+ last_commit
62
+ end
63
+
64
+ def previous_commits
65
+ commits.all(:project_id => id, :order => [:committed_at.desc]).tap {|commits| commits.shift }
55
66
  end
56
67
 
57
68
  def previous_builds
58
- all_builds.tap {|builds| builds.shift }
69
+ warn "Project#previous_builds is deprecated, use Project#previous_commits"
70
+ previous_commits
59
71
  end
60
72
 
61
73
  def status
62
- last_build && last_build.status
74
+ last_commit && last_commit.status
75
+ end
76
+
77
+ def human_readable_status
78
+ last_commit && last_commit.human_readable_status
63
79
  end
64
80
 
65
81
  def public=(flag)
@@ -70,12 +86,12 @@ module Integrity
70
86
  end
71
87
 
72
88
  def config_for(notifier)
73
- notifier = notifiers.first(:name => notifier.to_s.split(/::/).last)
89
+ notifier = notifiers.first(:name => notifier.to_s.split(/::/).last, :project_id => id)
74
90
  notifier.blank? ? {} : notifier.config
75
91
  end
76
92
 
77
93
  def notifies?(notifier)
78
- !notifiers.first(:name => notifier.to_s.split(/::/).last).blank?
94
+ !notifiers.first(:name => notifier.to_s.split(/::/).last, :project_id => id).blank?
79
95
  end
80
96
 
81
97
  def enable_notifiers(*args)
@@ -83,6 +99,30 @@ module Integrity
83
99
  end
84
100
 
85
101
  private
102
+ def find_or_create_commit_with_identifier(commit_identifier)
103
+ # We abuse +committed_at+ here setting it to Time.now because we use it
104
+ # to sort (for last_commit and previous_commits). I don't like this
105
+ # very much, but for now it's the only solution I can find.
106
+ #
107
+ # This also creates a dependency, as now we *always* have to update the
108
+ # +committed_at+ field after building to ensure the date is correct :(
109
+ #
110
+ # This might also make your commit listings a little jumpy, if some
111
+ # commits change place every time a build finishes =\
112
+ commits.first_or_create({ :identifier => commit_identifier, :project_id => id }, :committed_at => Time.now)
113
+ end
114
+
115
+ def head_of_remote_repo
116
+ SCM.new(uri, branch).head
117
+ end
118
+
119
+ def create_commit_from(data)
120
+ commits.create(:identifier => data["id"],
121
+ :author => "#{data["author"]["name"]} <#{data["author"]["email"]}>",
122
+ :message => data["message"],
123
+ :committed_at => data["timestamp"])
124
+ end
125
+
86
126
  def set_permalink
87
127
  self.permalink = (name || "").downcase.
88
128
  gsub(/'s/, "s").
@@ -92,26 +132,22 @@ module Integrity
92
132
  end
93
133
 
94
134
  def delete_code
95
- builds.destroy!
135
+ commits.all(:project_id => id).destroy!
96
136
  ProjectBuilder.new(self).delete_code
97
137
  rescue SCM::SCMUnknownError => error
98
138
  Integrity.log "Problem while trying to deleting code: #{error}"
99
139
  end
100
140
 
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
141
+ def valid_payload?(payload)
142
+ payload && payload["ref"].to_s.include?(branch) &&
143
+ !payload["commits"].nil? &&
144
+ !payload["commits"].to_a.empty?
111
145
  end
112
146
 
113
- def all_builds
114
- builds.all.sort_by {|b| b.commited_at }.reverse
147
+ def parse_payload(payload)
148
+ JSON.parse(payload.to_s)
149
+ rescue JSON::ParserError
150
+ false
115
151
  end
116
152
  end
117
153
  end
@@ -1,23 +1,24 @@
1
1
  module Integrity
2
2
  class ProjectBuilder
3
- attr_reader :build_script
4
-
5
3
  def initialize(project)
4
+ @project = project
6
5
  @uri = project.uri
7
6
  @build_script = project.command
8
7
  @branch = project.branch
9
8
  @scm = SCM.new(@uri, @branch, export_directory)
10
- @build = Build.new(:project => project)
11
9
  end
12
10
 
13
11
  def build(commit)
14
- Integrity.log "Building #{commit} (#{@branch}) of #{@build.project.name} in #{export_directory} using #{scm_name}"
15
- @scm.with_revision(commit) { run_build_script }
12
+ @commit = commit
13
+ @build = commit.build
14
+ @build.start!
15
+ Integrity.log "Building #{commit.identifier} (#{@branch}) of #{@project.name} in #{export_directory} using #{@scm.name}"
16
+ @scm.with_revision(commit.identifier) { run_build_script }
16
17
  @build
17
18
  ensure
18
- @build.commit_identifier = @scm.commit_identifier(commit)
19
- @build.commit_metadata = @scm.commit_metadata(commit)
20
- @build.save
19
+ @build.complete!
20
+ @commit.update_attributes(@scm.info(commit.identifier))
21
+ send_notifications
21
22
  end
22
23
 
23
24
  def delete_code
@@ -27,18 +28,26 @@ module Integrity
27
28
  end
28
29
 
29
30
  private
30
- def export_directory
31
- Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
31
+ def send_notifications
32
+ @project.notifiers.each do |notifier|
33
+ begin
34
+ Integrity.log "Notifying of build #{@commit.short_identifier} using the #{notifier.name} notifier"
35
+ notifier.notify_of_build @commit
36
+ rescue Timeout::Error
37
+ Integrity.log "#{notifier.name} notifier timed out"
38
+ next
39
+ end
40
+ end
32
41
  end
33
42
 
34
- def scm_name
35
- @scm.name
43
+ def export_directory
44
+ Integrity.config[:export_directory] / "#{SCM.working_tree_path(@uri)}-#{@branch}"
36
45
  end
37
46
 
38
47
  def run_build_script
39
- Integrity.log "Running `#{build_script}` in #{@scm.working_directory}"
48
+ Integrity.log "Running `#{@build_script}` in #{@scm.working_directory}"
40
49
 
41
- IO.popen "(cd #{@scm.working_directory} && #{build_script}) 2>&1", "r" do |pipe|
50
+ IO.popen "(cd #{@scm.working_directory} && #{@build_script}) 2>&1", "r" do |pipe|
42
51
  @build.output = pipe.read
43
52
  end
44
53
  @build.successful = $?.success?
@@ -9,8 +9,8 @@ module Integrity
9
9
  Git::URI.new(uri).working_tree_path
10
10
  end
11
11
 
12
- def initialize(uri, branch, working_directory)
13
- @uri = uri.to_s
12
+ def initialize(uri, branch, working_directory=nil)
13
+ @uri = uri.to_s
14
14
  @branch = branch.to_s
15
15
  @working_directory = working_directory
16
16
  end
@@ -21,27 +21,28 @@ module Integrity
21
21
  yield
22
22
  end
23
23
 
24
- def commit_identifier(sha1)
25
- `cd #{working_directory} && git show -s --pretty=format:%H #{sha1}`.chomp
24
+ def name
25
+ self.class.name.split("::").last
26
26
  end
27
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}`)
28
+ def head
29
+ log "Getting the HEAD of '#{uri}' at '#{branch}'"
30
+ `git ls-remote --heads #{uri} #{branch} | awk '{print $1}'`.chomp
31
31
  end
32
-
33
- def name
34
- self.class.name.split("::").last
32
+
33
+ def info(revision)
34
+ format = %Q(---%n:author: %an <%ae>%n:message: >-%n %s%n:committed_at: %ci%n)
35
+ YAML.load(`cd #{working_directory} && git show -s --pretty=format:"#{format}" #{revision}`)
35
36
  end
36
37
 
37
38
  private
38
39
 
39
40
  def fetch_code
40
- clone unless cloned?
41
+ clone unless cloned?
41
42
  checkout unless on_branch?
42
43
  pull
43
44
  end
44
-
45
+
45
46
  def clone
46
47
  log "Cloning #{uri} to #{working_directory}"
47
48
  `git clone #{uri} #{working_directory} &>/dev/null`
@@ -51,11 +52,11 @@ module Integrity
51
52
  strategy = case
52
53
  when treeish then treeish
53
54
  when local_branches.include?(branch) then branch
54
- else "-b #{branch} origin/#{branch}"
55
+ else "origin/#{branch}"
55
56
  end
56
57
 
57
58
  log "Checking-out #{strategy}"
58
- `cd #{working_directory} && git checkout #{strategy} &>/dev/null`
59
+ `cd #{working_directory} && git reset --hard #{strategy} &>/dev/null`
59
60
  end
60
61
 
61
62
  def pull
@@ -13,13 +13,13 @@ module Integrity
13
13
  # ssh://[user@]host.xz/path/to/repo.git/
14
14
  # ssh://[user@]host.xz/~user/path/to/repo.git/
15
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
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
19
  # supported
20
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
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
23
  # identical to the last three above, respectively:
24
24
  #
25
25
  # [user@]host.xz:/path/to/repo.git/
@@ -30,13 +30,13 @@ module Integrity
30
30
  def initialize(uri_string)
31
31
  @uri = Addressable::URI.parse(uri_string)
32
32
  end
33
-
33
+
34
34
  def working_tree_path
35
35
  strip_extension(path).gsub("/", "-")
36
36
  end
37
-
37
+
38
38
  private
39
-
39
+
40
40
  def strip_extension(string)
41
41
  uri = Pathname.new(string)
42
42
  if uri.extname.any?
@@ -46,7 +46,7 @@ module Integrity
46
46
  string
47
47
  end
48
48
  end
49
-
49
+
50
50
  def path
51
51
  path = @uri.path
52
52
  path.gsub(/\~[a-zA-Z0-9]*\//, "").gsub(/^\//, "")
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+
3
+ class ApiTest < Test::Unit::AcceptanceTestCase
4
+ story <<-EOF
5
+ As a project owner,
6
+ I want to be able to use GitHub as a build triggerer
7
+ So that my project is built everytime I push to the Holy Hub
8
+ EOF
9
+
10
+ def payload(after, branch="master", commits=[])
11
+ payload = { "after" => "#{after}", "ref" => "refs/heads/#{branch}" }
12
+ payload["commits"] = commits if commits.any?
13
+ payload.to_json
14
+ end
15
+
16
+ def post(path, data)
17
+ request_page(path, "post", data)
18
+ end
19
+
20
+ scenario "it only build commits for the branch being monitored" do
21
+ repo = git_repo(:my_test_project) # initial commit && successful commit
22
+ Project.gen(:my_test_project, :uri => repo.path, :branch => "my-branch")
23
+
24
+ basic_auth "admin", "test"
25
+
26
+ lambda do
27
+ post "/my-test-project/push", :payload => payload(repo.head, "master", repo.commits)
28
+ response_code.should == 422
29
+ end.should_not change(Build, :count)
30
+
31
+ visit "/my-test-project"
32
+
33
+ assert_contain("No builds for this project")
34
+ end
35
+
36
+ it "receiving a build request with build_all_commits *enabled* builds all commits, most recent first" do
37
+ Integrity.config[:build_all_commits] = true
38
+
39
+ repo = git_repo(:my_test_project) # initial commit && successful commit
40
+ 3.times do |i|
41
+ repo.add_commit("commit #{i}") do
42
+ system "echo commit_#{i} >> test-file"
43
+ system "git add test-file &>/dev/null"
44
+ end
45
+ end
46
+
47
+ Project.gen(:my_test_project, :uri => repo.path, :command => "echo successful", :branch => "master")
48
+
49
+ basic_auth "admin", "test"
50
+ post "/my-test-project/push", :payload => payload(repo.head, "master", repo.commits)
51
+
52
+ visit "/my-test-project"
53
+
54
+ assert_have_tag("h1", :content => "Built #{git_repo(:my_test_project).short_head} successfully")
55
+ assert_have_tag(".attribution", :content => "by John Doe")
56
+
57
+ assert_have_tag("#previous_builds li", :count => 4)
58
+ end
59
+
60
+ scenario "receiving a build request with build_all_commits *disabled* only builds the last commit passed" do
61
+ Integrity.config[:build_all_commits] = false
62
+
63
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
64
+
65
+ git_repo(:my_test_project).add_failing_commit
66
+ git_repo(:my_test_project).add_successful_commit
67
+ head = git_repo(:my_test_project).head
68
+
69
+ basic_auth "admin", "test"
70
+ post "/my-test-project/push", :payload => payload(head, "master", git_repo(:my_test_project).commits)
71
+
72
+ response_code.should == 201
73
+
74
+ visit "/my-test-project"
75
+
76
+ assert_have_tag("h1", :content => "Built #{git_repo(:my_test_project).short_head} successfully")
77
+
78
+ assert_have_no_tag("#previous_builds li")
79
+ end
80
+
81
+ scenario "an unauthenticated request returns a 401" do
82
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
83
+ head = git_repo(:my_test_project).head
84
+ post "/my-test-project/push", :payload => payload(head, "master")
85
+
86
+ response_code.should == 401
87
+ end
88
+
89
+ scenario "receiving a build request with an invalid payload returns an Invalid Request error" do
90
+ Project.gen(:my_test_project, :uri => git_repo(:my_test_project).path)
91
+
92
+ basic_auth "admin", "test"
93
+
94
+ post "/my-test-project/push", :payload => "foo"
95
+ response_code.should == 422
96
+ end
97
+ end
@@ -0,0 +1,65 @@
1
+ require File.dirname(__FILE__) + "/helpers"
2
+
3
+ class BrowseProjectBuildsTest < Test::Unit::AcceptanceTestCase
4
+ story <<-EOS
5
+ As a user,
6
+ I want to browse the builds of a project in Integrity
7
+ So I can see the history of a project
8
+ EOS
9
+
10
+ scenario "a project with no builds should say so in a friendly manner" do
11
+ Project.gen(:integrity, :public => true, :commits => [])
12
+
13
+ visit "/integrity"
14
+
15
+ assert_have_no_tag("#last_build")
16
+ assert_have_no_tag("#previous_builds")
17
+ assert_contain("No builds for this project, buddy")
18
+ end
19
+
20
+ scenario "a user can see the last build and the list of previous builds on a project page" do
21
+ Project.gen(:integrity, :public => true, :commits => \
22
+ 3.of { Commit.gen(:successful) } +
23
+ 2.of { Commit.gen(:failed) } +
24
+ 2.of { Commit.gen(:pending) })
25
+
26
+ visit "/integrity"
27
+
28
+ assert_have_tag("#last_build")
29
+
30
+ within("ul#previous_builds") do
31
+ assert_have_tag("li.pending", :count => 2)
32
+ assert_have_tag("li.failed", :count => 2)
33
+ assert_have_tag("li.success", :count => 2)
34
+ end
35
+ end
36
+
37
+ scenario "a user can see details about the last build on the project page" do
38
+ commit = Commit.gen(:successful, :identifier => "7fee3f0014b529e2b76d591a8085d76eab0ff923",
39
+ :author => "Nicolas Sanguinetti <contacto@nicolassanguinetti.info>",
40
+ :message => "No more pending tests :)",
41
+ :committed_at => Time.mktime(2008, 12, 15, 18))
42
+ commit.build.update_attributes(:output => "This is the build output")
43
+ Project.gen(:integrity, :public => true, :commits => [commit])
44
+
45
+ visit "/integrity"
46
+
47
+ assert_have_tag("h1", :content => "Built 7fee3f0 successfully")
48
+ assert_have_tag("blockquote p", :content => "No more pending tests")
49
+ assert_have_tag("span.who", :content => "by: Nicolas Sanguinetti")
50
+ assert_have_tag("span.when", :content => "Dec 15th")
51
+ assert_have_tag("pre.output", :content => "This is the build output")
52
+ end
53
+
54
+ scenario "a user can browse to individual build pages" do
55
+ Project.gen(:integrity, :public => true, :commits => [
56
+ Commit.gen(:successful, :identifier => "7fee3f0014b529e2b76d591a8085d76eab0ff923"),
57
+ Commit.gen(:successful, :identifier => "87e673a83d273ecde121624a3fcfae57a04f2b76")
58
+ ])
59
+
60
+ visit "/integrity"
61
+ click_link(/Build 87e673a/)
62
+
63
+ assert_have_tag("h1", :content => "Built 87e673a successfully")
64
+ end
65
+ end