danger 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -3
- data/lib/danger/ci_source/buildkite.rb +13 -7
- data/lib/danger/ci_source/ci_source.rb +18 -1
- data/lib/danger/ci_source/circle.rb +11 -3
- data/lib/danger/ci_source/drone.rb +11 -4
- data/lib/danger/ci_source/jenkins.rb +12 -10
- data/lib/danger/ci_source/local_git_repo.rb +4 -0
- data/lib/danger/ci_source/semaphore.rb +11 -4
- data/lib/danger/ci_source/teamcity.rb +22 -0
- data/lib/danger/ci_source/travis.rb +10 -2
- data/lib/danger/ci_source/xcode_server.rb +8 -1
- data/lib/danger/commands/runner.rb +18 -4
- data/lib/danger/comment_generators/github.md.erb +16 -17
- data/lib/danger/danger_core/dangerfile.rb +1 -1
- data/lib/danger/danger_core/environment_manager.rb +22 -29
- data/lib/danger/danger_core/plugins/dangerfile_github_plugin.rb +1 -1
- data/lib/danger/plugin_support/plugin.rb +10 -9
- data/lib/danger/request_source/github.rb +180 -156
- data/lib/danger/request_source/request_source.rb +48 -0
- data/lib/danger/scm_source/git_repo.rb +4 -0
- data/lib/danger/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 961961730193f38ada39854b1e3b14a665534539
|
4
|
+
data.tar.gz: 161dfd213ef838bf04dc47c95cd3a59d5e745cc7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c298a712a18d76022aba76603f6946f03ef5999a2203c06c4cc34013e2131d9657fa3ad2972b0bbf9c406f9d6922f5d2677160068482aaf0fbd3572444a31df
|
7
|
+
data.tar.gz: b657e664d71d270a6118bfe68033f039ac2604f969d9494b2e16631e4a9b65448b7e8de1842909e628da7a97aae65ba72939b7b9dc3e2783d8bd33ec62946056
|
data/README.md
CHANGED
@@ -43,7 +43,7 @@ This will look at your `Dangerfile` and update the pull request accordingly. Whi
|
|
43
43
|
|
44
44
|
## CI Support
|
45
45
|
|
46
|
-
Danger currently is supported on Travis CI, Circle CI, Xcode Bots via Buildasaur, BuildKite, Jenkins, Semaphore,
|
46
|
+
Danger currently is supported on Travis CI, Circle CI, Xcode Bots via Buildasaur, BuildKite, Jenkins, Semaphore, Drone CI and TeamCity. These work via environment variables, so it's easy to extend to include your own.
|
47
47
|
|
48
48
|
## What happens?
|
49
49
|
|
@@ -62,7 +62,7 @@ Danger runs at the end of a CI build, she will execute a `Dangerfile`. This file
|
|
62
62
|
:busts_in_silhouette: | `pr_author` | The author who submitted the PR
|
63
63
|
:bookmark: | `pr_labels` | The labels added to the PR
|
64
64
|
|
65
|
-
The `Dangerfile` is a
|
65
|
+
The `Dangerfile` is a Ruby file, so really, you can do anything. However, at this stage you might need selling on the idea a bit more, so lets take some real examples:
|
66
66
|
|
67
67
|
#### Dealing with WIP pull requests
|
68
68
|
|
@@ -192,6 +192,20 @@ Danger will update the comment to cross it out. If you don't want this behavior,
|
|
192
192
|
fail("PR needs labels", sticky: false) if pr_labels.empty?
|
193
193
|
```
|
194
194
|
|
195
|
+
## Multiple Dangers
|
196
|
+
|
197
|
+
If one Danger is not enough for you, you can run several ones on the same PR. Just use the `danger_id` param. For example:
|
198
|
+
|
199
|
+
```
|
200
|
+
bundle exec danger --danger_id=unit_tests
|
201
|
+
```
|
202
|
+
|
203
|
+
You can have each instance of Danger running on a different CI provider and even doing different validations. An use case would be:
|
204
|
+
|
205
|
+
* `basic` runs on a Linux environment (such as Circle CI) and validates the PR itself (title, etc)
|
206
|
+
* `compilation` runs on a Mac after running unit tests for your iOS app and comments about warnings, test failures, etc
|
207
|
+
* `uitests` runs on a Mac after running UI Unit tests and comments about test failures
|
208
|
+
|
195
209
|
## Useful bits of knowledge
|
196
210
|
|
197
211
|
* You can set the base branch in the command line arguments see: `bundle exec danger --help`, if you commonly merge into non-master branches.
|
@@ -206,7 +220,7 @@ Danger allows usage with GitHub Enterprise by setting 2 environment variables:
|
|
206
220
|
|
207
221
|
## License, Contributor's Guidelines and Code of Conduct
|
208
222
|
|
209
|
-
|
223
|
+
We try to keep as much discussion as possible in GitHub issues, but also have a slack, if you'd like an invite ping [@Orta](https://twitter.com/orta/) a DM on twitter with your email.
|
210
224
|
|
211
225
|
> This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs.
|
212
226
|
|
@@ -5,17 +5,23 @@ module Danger
|
|
5
5
|
module CISource
|
6
6
|
class Buildkite < CI
|
7
7
|
def self.validates?(env)
|
8
|
-
return
|
8
|
+
return false unless env["BUILDKITE"]
|
9
|
+
return false unless env["BUILDKITE_REPO"]
|
10
|
+
return false unless env["BUILDKITE_PULL_REQUEST"]
|
11
|
+
|
12
|
+
return true
|
9
13
|
end
|
10
14
|
|
11
15
|
def initialize(env)
|
12
|
-
|
13
|
-
unless repo.nil?
|
14
|
-
repo_matches = repo.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
|
15
|
-
self.repo_slug = repo_matches[2] unless repo_matches.nil?
|
16
|
-
end
|
17
|
-
|
16
|
+
self.repo_url = env["BUILDKITE_REPO"]
|
18
17
|
self.pull_request_id = env["BUILDKITE_PULL_REQUEST"]
|
18
|
+
|
19
|
+
repo_matches = self.repo_url.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
|
20
|
+
self.repo_slug = repo_matches[2] unless repo_matches.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def supported_request_sources
|
24
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
19
25
|
end
|
20
26
|
end
|
21
27
|
end
|
@@ -2,7 +2,24 @@ module Danger
|
|
2
2
|
module CISource
|
3
3
|
# "abstract" CI class
|
4
4
|
class CI
|
5
|
-
attr_accessor :repo_slug, :pull_request_id
|
5
|
+
attr_accessor :repo_slug, :pull_request_id, :repo_url, :supported_request_sources
|
6
|
+
|
7
|
+
def self.inherited(child_class)
|
8
|
+
available_ci_sources.add child_class
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.available_ci_sources
|
13
|
+
@available_ci_sources ||= Set.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def supported_request_sources
|
17
|
+
raise "CISource subclass must specify the supported request sources"
|
18
|
+
end
|
19
|
+
|
20
|
+
def supports?(request_source)
|
21
|
+
supported_request_sources.include? request_source
|
22
|
+
end
|
6
23
|
|
7
24
|
def self.validates?(_env)
|
8
25
|
false
|
@@ -6,10 +6,16 @@ module Danger
|
|
6
6
|
module CISource
|
7
7
|
class CircleCI < CI
|
8
8
|
def self.validates?(env)
|
9
|
-
return false
|
10
|
-
return
|
9
|
+
return false unless env["CIRCLE_BUILD_NUM"]
|
10
|
+
return false unless env["CI_PULL_REQUEST"]
|
11
|
+
return false unless env["CIRCLE_PROJECT_USERNAME"]
|
12
|
+
return false unless env["CIRCLE_PROJECT_REPONAME"]
|
11
13
|
|
12
|
-
return
|
14
|
+
return true
|
15
|
+
end
|
16
|
+
|
17
|
+
def supported_request_sources
|
18
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
13
19
|
end
|
14
20
|
|
15
21
|
def client
|
@@ -33,6 +39,8 @@ module Danger
|
|
33
39
|
end
|
34
40
|
|
35
41
|
def initialize(env)
|
42
|
+
self.repo_url = GitRepo.new.origins # CircleCI doesn't provide a repo url env variable :/
|
43
|
+
|
36
44
|
@circle_token = env["CIRCLE_CI_API_TOKEN"]
|
37
45
|
url = pull_request_url(env)
|
38
46
|
|
@@ -4,14 +4,21 @@ module Danger
|
|
4
4
|
module CISource
|
5
5
|
class Drone < CI
|
6
6
|
def self.validates?(env)
|
7
|
-
return
|
7
|
+
return false unless env["DRONE"]
|
8
|
+
return false unless env["DRONE_REPO"]
|
9
|
+
return false unless env["DRONE_PULL_REQUEST"].to_i > 0
|
10
|
+
|
11
|
+
return true
|
12
|
+
end
|
13
|
+
|
14
|
+
def supported_request_sources
|
15
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
8
16
|
end
|
9
17
|
|
10
18
|
def initialize(env)
|
11
19
|
self.repo_slug = env["DRONE_REPO"]
|
12
|
-
|
13
|
-
|
14
|
-
end
|
20
|
+
self.pull_request_id = env["DRONE_PULL_REQUEST"]
|
21
|
+
self.repo_url = GitRepo.new.origins # Drone doesn't provide a repo url env variable :/
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -5,20 +5,22 @@ module Danger
|
|
5
5
|
module CISource
|
6
6
|
class Jenkins < CI
|
7
7
|
def self.validates?(env)
|
8
|
-
return
|
8
|
+
return false unless env["ghprbPullId"].to_i > 0
|
9
|
+
return false unless env["GIT_URL"]
|
10
|
+
|
11
|
+
return true
|
12
|
+
end
|
13
|
+
|
14
|
+
def supported_request_sources
|
15
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
9
16
|
end
|
10
17
|
|
11
18
|
def initialize(env)
|
12
|
-
|
13
|
-
|
14
|
-
repo_matches = repo.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
|
15
|
-
self.repo_slug = repo_matches[2] unless repo_matches.nil?
|
16
|
-
end
|
19
|
+
self.repo_url = env["GIT_URL"]
|
20
|
+
self.pull_request_id = env["ghprbPullId"]
|
17
21
|
|
18
|
-
|
19
|
-
|
20
|
-
self.pull_request_id = env["ghprbPullId"]
|
21
|
-
end
|
22
|
+
repo_matches = self.repo_url.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
|
23
|
+
self.repo_slug = repo_matches[2] unless repo_matches.nil?
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -4,14 +4,21 @@ module Danger
|
|
4
4
|
module CISource
|
5
5
|
class Semaphore < CI
|
6
6
|
def self.validates?(env)
|
7
|
-
return
|
7
|
+
return false unless env["SEMAPHORE"]
|
8
|
+
return false unless env["SEMAPHORE_REPO_SLUG"]
|
9
|
+
return false unless env["PULL_REQUEST_NUMBER"].to_i > 0
|
10
|
+
|
11
|
+
return true
|
12
|
+
end
|
13
|
+
|
14
|
+
def supported_request_sources
|
15
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
8
16
|
end
|
9
17
|
|
10
18
|
def initialize(env)
|
11
19
|
self.repo_slug = env["SEMAPHORE_REPO_SLUG"]
|
12
|
-
|
13
|
-
|
14
|
-
end
|
20
|
+
self.pull_request_id = env["PULL_REQUEST_NUMBER"]
|
21
|
+
self.repo_url = GitRepo.new.origins # Semaphore doesn't provide a repo url env variable :/
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Danger
|
2
|
+
module CISource
|
3
|
+
class TeamCity < CI
|
4
|
+
def self.validates?(env)
|
5
|
+
env.key? 'TEAMCITY_VERSION'
|
6
|
+
end
|
7
|
+
|
8
|
+
def supported_request_sources
|
9
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(env)
|
13
|
+
# NB: Unfortunately TeamCity doesn't provide these variables
|
14
|
+
# automatically so you have to add these variables manually to your
|
15
|
+
# project or build configuration
|
16
|
+
self.repo_slug = env['GITHUB_REPO_SLUG']
|
17
|
+
self.pull_request_id = env['GITHUB_PULL_REQUEST_ID'].to_i
|
18
|
+
self.repo_url = env['GITHUB_REPO_URL']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -5,15 +5,23 @@ module Danger
|
|
5
5
|
module CISource
|
6
6
|
class Travis < CI
|
7
7
|
def self.validates?(env)
|
8
|
-
return
|
8
|
+
return false unless env["HAS_JOSH_K_SEAL_OF_APPROVAL"]
|
9
|
+
return false unless env["TRAVIS_REPO_SLUG"]
|
10
|
+
return false unless env["TRAVIS_PULL_REQUEST"]
|
11
|
+
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
def supported_request_sources
|
16
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
9
17
|
end
|
10
18
|
|
11
19
|
def initialize(env)
|
12
20
|
self.repo_slug = env["TRAVIS_REPO_SLUG"]
|
13
|
-
# from https://docs.travis-ci.com/user/pull-requests, as otherwise it's "false"
|
14
21
|
if env["TRAVIS_PULL_REQUEST"].to_i > 0
|
15
22
|
self.pull_request_id = env["TRAVIS_PULL_REQUEST"]
|
16
23
|
end
|
24
|
+
self.repo_url = GitRepo.new.origins # Travis doesn't provide a repo url env variable :/
|
17
25
|
end
|
18
26
|
end
|
19
27
|
end
|
@@ -4,7 +4,13 @@ module Danger
|
|
4
4
|
module CISource
|
5
5
|
class XcodeServer < CI
|
6
6
|
def self.validates?(env)
|
7
|
-
return
|
7
|
+
return false unless env["XCS_BOT_NAME"]
|
8
|
+
|
9
|
+
return true
|
10
|
+
end
|
11
|
+
|
12
|
+
def supported_request_sources
|
13
|
+
@supported_request_sources ||= [Danger::RequestSources::GitHub]
|
8
14
|
end
|
9
15
|
|
10
16
|
def initialize(env)
|
@@ -15,6 +21,7 @@ module Danger
|
|
15
21
|
self.repo_slug = repo_matches[1] unless repo_matches.nil?
|
16
22
|
pull_request_id_matches = bot_name.match(/#(\d+)/)
|
17
23
|
self.pull_request_id = pull_request_id_matches[1] unless pull_request_id_matches.nil?
|
24
|
+
self.repo_url = GitRepo.new.origins # Xcode Server doesn't provide a repo url env variable :/
|
18
25
|
end
|
19
26
|
end
|
20
27
|
end
|
@@ -15,6 +15,7 @@ module Danger
|
|
15
15
|
@dangerfile_path = dangerfile if File.exist? dangerfile
|
16
16
|
@base = argv.option('base')
|
17
17
|
@head = argv.option('head')
|
18
|
+
@danger_id = argv.option('danger_id', 'danger')
|
18
19
|
super
|
19
20
|
end
|
20
21
|
|
@@ -29,7 +30,8 @@ module Danger
|
|
29
30
|
[
|
30
31
|
['--base=[master|dev|stable]', 'A branch/tag/commit to use as the base of the diff'],
|
31
32
|
['--head=[master|dev|stable]', 'A branch/tag/commit to use as the head'],
|
32
|
-
['--dangerfile=<path/to/dangerfile>', 'The location of your Dangerfile']
|
33
|
+
['--dangerfile=<path/to/dangerfile>', 'The location of your Dangerfile'],
|
34
|
+
['--danger_id=<id>', 'The identifier of this Danger instance']
|
33
35
|
].concat(super)
|
34
36
|
end
|
35
37
|
|
@@ -47,8 +49,8 @@ module Danger
|
|
47
49
|
dm.env.ensure_danger_branches_are_setup
|
48
50
|
|
49
51
|
# Offer the chance for a user to specify a branch through the command line
|
50
|
-
ci_base = @base ||
|
51
|
-
ci_head = @head ||
|
52
|
+
ci_base = @base || EnvironmentManager.danger_base_branch
|
53
|
+
ci_head = @head || EnvironmentManager.danger_head_branch
|
52
54
|
dm.env.scm.diff_for_folder(".", from: ci_base, to: ci_head)
|
53
55
|
|
54
56
|
dm.parse Pathname.new(@dangerfile_path)
|
@@ -68,7 +70,19 @@ module Danger
|
|
68
70
|
violations = dm.violation_report
|
69
71
|
status = dm.status_report
|
70
72
|
|
71
|
-
gh.update_pull_request!(warnings: violations[:warnings], errors: violations[:errors], messages: violations[:messages], markdowns: status[:markdowns])
|
73
|
+
gh.update_pull_request!(warnings: violations[:warnings], errors: violations[:errors], messages: violations[:messages], markdowns: status[:markdowns], danger_id: @danger_id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.report_error(exception)
|
77
|
+
raise exception if exception.kind_of?(SystemExit)
|
78
|
+
message = "#{exception.message.red} (#{exception.class.to_s.yellow})"
|
79
|
+
if exception.backtrace
|
80
|
+
danger_lib = File.expand_path('../../..', __FILE__)
|
81
|
+
message << "\n\t" << exception.backtrace.reverse_each.
|
82
|
+
drop_while { |bt| !bt.start_with?(danger_lib) }.reverse.
|
83
|
+
join("\n\t")
|
84
|
+
end
|
85
|
+
abort(message)
|
72
86
|
end
|
73
87
|
end
|
74
88
|
end
|
@@ -1,41 +1,40 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
<%- @tables.each do |table| -%>
|
2
|
+
<%- if table[:content].any? || table[:resolved].any? -%>
|
3
3
|
<table>
|
4
4
|
<thead>
|
5
5
|
<tr>
|
6
6
|
<th width="50"></th>
|
7
7
|
<th width="100%" data-kind="<%= table[:name] %>">
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
<%- if table[:count] > 0 -%>
|
9
|
+
<%= table[:count] %> <%= table[:name] %><%= "s" unless table[:count] == 1 %>
|
10
|
+
<%- else -%>
|
11
|
+
:white_check_mark: <%= random_compliment %>
|
12
|
+
<%- end -%>
|
13
13
|
</th>
|
14
14
|
</tr>
|
15
15
|
</thead>
|
16
16
|
<tbody>
|
17
|
-
|
17
|
+
<%- table[:content].each do |violation| -%>
|
18
18
|
<tr>
|
19
19
|
<td>:<%= table[:emoji] %>:</td>
|
20
20
|
<td data-sticky="<%= violation.sticky %>"><%= violation.message %></td>
|
21
21
|
</tr>
|
22
|
-
|
23
|
-
|
22
|
+
<%- end -%>
|
23
|
+
<%- table[:resolved].each do |message| -%>
|
24
24
|
<tr>
|
25
25
|
<td>:white_check_mark:</td>
|
26
26
|
<td data-sticky="true"><del><%= message %></del></td>
|
27
27
|
</tr>
|
28
|
-
|
28
|
+
<%- end -%>
|
29
29
|
</tbody>
|
30
30
|
</table>
|
31
|
-
|
32
|
-
|
31
|
+
<%- end -%>
|
32
|
+
<%- end -%>
|
33
33
|
|
34
|
-
|
34
|
+
<%- @markdowns.each do |current| -%>
|
35
35
|
<%= current %>
|
36
36
|
<%# the previous line has to be aligned far to the left, otherwise markdown can break easily %>
|
37
|
-
|
38
|
-
|
39
|
-
<p align="right" data-meta="generated_by_danger">
|
37
|
+
<%- end -%>
|
38
|
+
<p align="right" data-meta="generated_by_<%= @danger_id %>">
|
40
39
|
Generated by :no_entry_sign: <a href="https://github.com/danger/danger/">danger</a>
|
41
40
|
</p>
|
@@ -82,7 +82,7 @@ module Danger
|
|
82
82
|
# Iterate through available plugin classes and initialize them with
|
83
83
|
# a reference to this Dangerfile
|
84
84
|
def refresh_plugins
|
85
|
-
plugins =
|
85
|
+
plugins = Plugin.all_plugins
|
86
86
|
plugins.each do |klass|
|
87
87
|
next if klass.respond_to?(:singleton_class?) && klass.singleton_class?
|
88
88
|
plugin = klass.new(self)
|
@@ -1,17 +1,15 @@
|
|
1
1
|
require "danger/ci_source/ci_source"
|
2
|
-
require "danger/request_source/
|
2
|
+
require "danger/request_source/request_source"
|
3
3
|
|
4
4
|
module Danger
|
5
5
|
class EnvironmentManager
|
6
6
|
attr_accessor :ci_source, :request_source, :scm
|
7
7
|
|
8
8
|
def initialize(env)
|
9
|
-
CISource.
|
10
|
-
|
11
|
-
next unless c.kind_of?(Class)
|
12
|
-
next unless c.validates?(env)
|
9
|
+
CISource::CI.available_ci_sources.each do |klass|
|
10
|
+
next unless klass.validates?(env)
|
13
11
|
|
14
|
-
self.ci_source =
|
12
|
+
self.ci_source = klass.new(env)
|
15
13
|
if self.ci_source.repo_slug and self.ci_source.pull_request_id
|
16
14
|
break
|
17
15
|
else
|
@@ -20,12 +18,19 @@ module Danger
|
|
20
18
|
end
|
21
19
|
end
|
22
20
|
|
23
|
-
raise "Could not find a CI
|
21
|
+
raise "Could not find a valid pull request within the known CI sources".red unless self.ci_source
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
23
|
+
RequestSources::RequestSource.available_request_sources.each do |klass|
|
24
|
+
next unless self.ci_source.supports?(klass)
|
25
|
+
|
26
|
+
request_source = klass.new(self.ci_source, ENV)
|
27
|
+
next unless request_source.validates?
|
28
|
+
self.request_source = request_source
|
29
|
+
end
|
30
|
+
|
31
|
+
raise "Could not find a Request Source".red unless self.request_source
|
32
|
+
|
33
|
+
self.scm = self.request_source.scm
|
29
34
|
end
|
30
35
|
|
31
36
|
def pr?
|
@@ -39,40 +44,28 @@ module Danger
|
|
39
44
|
def ensure_danger_branches_are_setup
|
40
45
|
clean_up
|
41
46
|
|
42
|
-
|
43
|
-
pull_id = ci_source.pull_request_id
|
44
|
-
|
45
|
-
# TODO: This isn't optimal, should be hidden behind some kind of facade, but the plugin makes that
|
46
|
-
# difficult to do without accessing the dangerfile
|
47
|
-
test_branch = request_source.pr_json[:base][:sha]
|
48
|
-
|
49
|
-
# Next, we want to ensure that we have a version of the current branch at a known location
|
50
|
-
scm.exec "branch #{danger_base_branch} #{test_branch}"
|
51
|
-
|
52
|
-
# OK, so we want to ensure that we have a known head branch, this will always represent
|
53
|
-
# the head of the PR ( e.g. the most recent commit that will be merged. )
|
54
|
-
scm.exec "fetch origin +refs/pull/#{pull_id}/merge:#{danger_head_branch}"
|
47
|
+
self.request_source.setup_danger_branches
|
55
48
|
end
|
56
49
|
|
57
50
|
def clean_up
|
58
|
-
[danger_base_branch,
|
51
|
+
[EnvironmentManager.danger_base_branch, EnvironmentManager.danger_head_branch].each do |branch|
|
59
52
|
scm.exec("branch -D #{branch}") unless scm.exec("rev-parse --quiet --verify #{branch}").empty?
|
60
53
|
end
|
61
54
|
end
|
62
55
|
|
63
56
|
def meta_info_for_base
|
64
|
-
scm.exec("--no-pager log #{danger_base_branch} -n1")
|
57
|
+
scm.exec("--no-pager log #{EnvironmentManager.danger_base_branch} -n1")
|
65
58
|
end
|
66
59
|
|
67
60
|
def meta_info_for_head
|
68
|
-
scm.exec("--no-pager log #{danger_head_branch} -n1")
|
61
|
+
scm.exec("--no-pager log #{EnvironmentManager.danger_head_branch} -n1")
|
69
62
|
end
|
70
63
|
|
71
|
-
def danger_head_branch
|
64
|
+
def self.danger_head_branch
|
72
65
|
"danger_head"
|
73
66
|
end
|
74
67
|
|
75
|
-
def danger_base_branch
|
68
|
+
def self.danger_base_branch
|
76
69
|
"danger_base"
|
77
70
|
end
|
78
71
|
end
|
@@ -4,7 +4,7 @@ module Danger
|
|
4
4
|
class DangerfileGitHubPlugin < Plugin
|
5
5
|
def initialize(dangerfile)
|
6
6
|
super(dangerfile)
|
7
|
-
return nil unless dangerfile.env.request_source.class == Danger::GitHub
|
7
|
+
return nil unless dangerfile.env.request_source.class == Danger::RequestSources::GitHub
|
8
8
|
|
9
9
|
@github = dangerfile.env.request_source
|
10
10
|
end
|
@@ -5,7 +5,7 @@ module Danger
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def self.instance_name
|
8
|
-
|
8
|
+
to_s.gsub("Danger", "").danger_underscore.split('/').last
|
9
9
|
end
|
10
10
|
|
11
11
|
# Both of these methods exist on all objects
|
@@ -13,19 +13,20 @@ module Danger
|
|
13
13
|
# http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-fail
|
14
14
|
# However, as we're using using them in the DSL, they won't
|
15
15
|
# get method_missing called correctly.
|
16
|
+
undef :warn, :fail
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
# Since we have a reference to the Dangerfile containing all the information
|
19
|
+
# We need to redirect the self calls to the Dangerfile
|
20
|
+
def method_missing(method_sym, *arguments, &block)
|
21
|
+
@dangerfile.send(method_sym, *arguments, &block)
|
19
22
|
end
|
20
23
|
|
21
|
-
def
|
22
|
-
|
24
|
+
def self.all_plugins
|
25
|
+
@all_plugins ||= []
|
23
26
|
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
def method_missing(method_sym, *arguments, &_block)
|
28
|
-
@dangerfile.send(method_sym, *arguments)
|
28
|
+
def self.inherited(plugin)
|
29
|
+
Plugin.all_plugins.push(plugin)
|
29
30
|
end
|
30
31
|
end
|
31
32
|
end
|
@@ -3,201 +3,225 @@ require 'octokit'
|
|
3
3
|
require 'redcarpet'
|
4
4
|
|
5
5
|
module Danger
|
6
|
-
|
7
|
-
|
6
|
+
module RequestSources
|
7
|
+
class GitHub < RequestSource
|
8
|
+
attr_accessor :pr_json, :issue_json, :support_tokenless_auth
|
9
|
+
|
10
|
+
def initialize(ci_source, environment)
|
11
|
+
self.ci_source = ci_source
|
12
|
+
self.environment = environment
|
13
|
+
self.support_tokenless_auth = false
|
14
|
+
|
15
|
+
Octokit.auto_paginate = true
|
16
|
+
@token = @environment["DANGER_GITHUB_API_TOKEN"]
|
17
|
+
if @environment["DANGER_GITHUB_API_HOST"]
|
18
|
+
Octokit.api_endpoint = @environment["DANGER_GITHUB_API_HOST"]
|
19
|
+
end
|
20
|
+
end
|
8
21
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
self.support_tokenless_auth = false
|
22
|
+
def scm
|
23
|
+
@scm ||= GitRepo.new
|
24
|
+
end
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
self.github_host = @environment["DANGER_GITHUB_HOST"] || "github.com"
|
17
|
-
if @environment["DANGER_GITHUB_API_HOST"]
|
18
|
-
Octokit.api_endpoint = @environment["DANGER_GITHUB_API_HOST"]
|
26
|
+
def host
|
27
|
+
@host = @environment["DANGER_GITHUB_HOST"] || "github.com"
|
19
28
|
end
|
20
|
-
end
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
def client
|
31
|
+
raise "No API token given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
|
32
|
+
@client ||= Octokit::Client.new(access_token: @token)
|
33
|
+
end
|
26
34
|
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
def markdown_parser
|
36
|
+
@markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true)
|
37
|
+
end
|
30
38
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
39
|
+
def setup_danger_branches
|
40
|
+
# we can use a github specific feature here:
|
41
|
+
pull_id = self.ci_source.pull_request_id
|
42
|
+
test_branch = self.pr_json[:base][:sha]
|
36
43
|
|
37
|
-
|
38
|
-
|
39
|
-
return [] if pr_body.nil?
|
40
|
-
pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
|
41
|
-
end
|
44
|
+
# Next, we want to ensure that we have a version of the current branch at a known location
|
45
|
+
self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{test_branch}"
|
42
46
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
+
# OK, so we want to ensure that we have a known head branch, this will always represent
|
48
|
+
# the head of the PR ( e.g. the most recent commit that will be merged. )
|
49
|
+
self.scm.exec "fetch origin +refs/pull/#{pull_id}/merge:#{EnvironmentManager.danger_head_branch}"
|
50
|
+
end
|
47
51
|
|
48
|
-
|
49
|
-
|
50
|
-
|
52
|
+
def fetch_details
|
53
|
+
self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
|
54
|
+
fetch_issue_details(self.pr_json)
|
55
|
+
self.ignored_violations = ignored_violations_from_pr(self.pr_json)
|
56
|
+
end
|
51
57
|
|
52
|
-
|
53
|
-
|
58
|
+
def ignored_violations_from_pr(pr_json)
|
59
|
+
pr_body = pr_json[:body]
|
60
|
+
return [] if pr_body.nil?
|
61
|
+
pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
|
62
|
+
end
|
54
63
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
comment = editable_issues.first[:body]
|
59
|
-
previous_violations = parse_comment(comment)
|
64
|
+
def fetch_issue_details(pr_json)
|
65
|
+
href = pr_json[:_links][:issue][:href]
|
66
|
+
self.issue_json = client.get(href)
|
60
67
|
end
|
61
68
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
messages: messages,
|
69
|
-
markdowns: markdowns,
|
70
|
-
previous_violations: previous_violations)
|
69
|
+
# Sending data to GitHub
|
70
|
+
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: 'danger')
|
71
|
+
comment_result = {}
|
72
|
+
|
73
|
+
issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
|
74
|
+
editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_#{danger_id}") == false }
|
71
75
|
|
72
76
|
if editable_issues.empty?
|
73
|
-
|
77
|
+
previous_violations = {}
|
74
78
|
else
|
75
|
-
|
76
|
-
|
79
|
+
comment = editable_issues.first[:body]
|
80
|
+
previous_violations = parse_comment(comment)
|
77
81
|
end
|
78
|
-
end
|
79
82
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
+
if previous_violations.empty? && (warnings + errors + messages + markdowns).empty?
|
84
|
+
# Just remove the comment, if there's nothing to say.
|
85
|
+
delete_old_comments!(danger_id: danger_id)
|
86
|
+
else
|
87
|
+
body = generate_comment(warnings: warnings,
|
83
88
|
errors: errors,
|
84
|
-
|
85
|
-
|
89
|
+
messages: messages,
|
90
|
+
markdowns: markdowns,
|
91
|
+
previous_violations: previous_violations,
|
92
|
+
danger_id: danger_id)
|
93
|
+
|
94
|
+
if editable_issues.empty?
|
95
|
+
comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
|
96
|
+
else
|
97
|
+
original_id = editable_issues.first[:id]
|
98
|
+
comment_result = client.update_comment(ci_source.repo_slug, original_id, body)
|
99
|
+
end
|
100
|
+
end
|
86
101
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
context: "danger/danger",
|
93
|
-
target_url: details_url
|
94
|
-
})
|
95
|
-
rescue
|
96
|
-
# This usually means the user has no commit access to this repo
|
97
|
-
# That's always the case for open source projects where you can only
|
98
|
-
# use a read-only GitHub account
|
99
|
-
if errors.count > 0
|
100
|
-
# We need to fail the actual build here
|
101
|
-
abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR set a PR status.")
|
102
|
-
else
|
103
|
-
puts message
|
102
|
+
# Now, set the pull request status.
|
103
|
+
# Note: this can terminate the entire process.
|
104
|
+
submit_pull_request_status!(warnings: warnings,
|
105
|
+
errors: errors,
|
106
|
+
details_url: comment_result['html_url'])
|
104
107
|
end
|
105
|
-
end
|
106
108
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
109
|
+
def submit_pull_request_status!(warnings: nil, errors: nil, details_url: nil)
|
110
|
+
status = (errors.count == 0 ? 'success' : 'failure')
|
111
|
+
message = generate_github_description(warnings: warnings, errors: errors)
|
112
|
+
client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
|
113
|
+
description: message,
|
114
|
+
context: "danger/danger",
|
115
|
+
target_url: details_url
|
116
|
+
})
|
117
|
+
rescue
|
118
|
+
# This usually means the user has no commit access to this repo
|
119
|
+
# That's always the case for open source projects where you can only
|
120
|
+
# use a read-only GitHub account
|
121
|
+
if errors.count > 0
|
122
|
+
# We need to fail the actual build here
|
123
|
+
abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR set a PR status.")
|
124
|
+
else
|
125
|
+
puts message
|
126
|
+
end
|
114
127
|
end
|
115
|
-
end
|
116
128
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
return "All green. #{random_compliment}"
|
126
|
-
else
|
127
|
-
message = "⚠ "
|
128
|
-
message += "#{'Error'.danger_pluralize(errors.count)}. " unless errors.empty?
|
129
|
-
message += "#{'Warning'.danger_pluralize(warnings.count)}. " unless warnings.empty?
|
130
|
-
message += "Don't worry, everything is fixable."
|
131
|
-
return message
|
129
|
+
# Get rid of the previously posted comment, to only have the latest one
|
130
|
+
def delete_old_comments!(except: nil, danger_id: 'danger')
|
131
|
+
issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
|
132
|
+
issues.each do |issue|
|
133
|
+
next unless issue[:body].include?("generated_by_#{danger_id}")
|
134
|
+
next if issue[:id] == except
|
135
|
+
client.delete_comment(ci_source.repo_slug, issue[:id])
|
136
|
+
end
|
132
137
|
end
|
133
|
-
end
|
134
138
|
|
135
|
-
|
136
|
-
|
139
|
+
def random_compliment
|
140
|
+
compliment = ["Well done.", "Congrats.", "Woo!",
|
141
|
+
"Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
|
142
|
+
compliment.sample
|
143
|
+
end
|
137
144
|
|
138
|
-
|
145
|
+
def generate_github_description(warnings: nil, errors: nil)
|
146
|
+
if errors.empty? && warnings.empty?
|
147
|
+
return "All green. #{random_compliment}"
|
148
|
+
else
|
149
|
+
message = "⚠ "
|
150
|
+
message += "#{'Error'.danger_pluralize(errors.count)}. " unless errors.empty?
|
151
|
+
message += "#{'Warning'.danger_pluralize(warnings.count)}. " unless warnings.empty?
|
152
|
+
message += "Don't worry, everything is fixable."
|
153
|
+
return message
|
154
|
+
end
|
155
|
+
end
|
139
156
|
|
140
|
-
|
141
|
-
|
142
|
-
@tables = [
|
143
|
-
table("Error", "no_entry_sign", errors, previous_violations),
|
144
|
-
table("Warning", "warning", warnings, previous_violations),
|
145
|
-
table("Message", "book", messages, previous_violations)
|
146
|
-
]
|
147
|
-
@markdowns = markdowns
|
157
|
+
def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: 'danger')
|
158
|
+
require 'erb'
|
148
159
|
|
149
|
-
|
150
|
-
end
|
160
|
+
md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")
|
151
161
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
162
|
+
# erb: http://www.rrn.dk/rubys-erb-templating-system
|
163
|
+
# for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
|
164
|
+
@tables = [
|
165
|
+
table("Error", "no_entry_sign", errors, previous_violations),
|
166
|
+
table("Warning", "warning", warnings, previous_violations),
|
167
|
+
table("Message", "book", messages, previous_violations)
|
168
|
+
]
|
169
|
+
@markdowns = markdowns
|
170
|
+
@danger_id = danger_id
|
161
171
|
|
162
|
-
|
163
|
-
|
164
|
-
violations = {}
|
165
|
-
tables.each do |table|
|
166
|
-
next unless table =~ %r{<th width="100%"(.*?)</th>}im
|
167
|
-
title = Regexp.last_match(1)
|
168
|
-
kind = table_kind_from_title(title)
|
169
|
-
next unless kind
|
172
|
+
return ERB.new(File.read(md_template), 0, "-").result(binding)
|
173
|
+
end
|
170
174
|
|
171
|
-
|
175
|
+
def table(name, emoji, violations, all_previous_violations)
|
176
|
+
content = violations.map { |v| process_markdown(v) }.uniq
|
177
|
+
kind = table_kind_from_title(name)
|
178
|
+
previous_violations = all_previous_violations[kind] || []
|
179
|
+
messages = content.map(&:message)
|
180
|
+
resolved_violations = previous_violations.uniq - messages
|
181
|
+
count = content.count
|
182
|
+
{ name: name, emoji: emoji, content: content, resolved: resolved_violations, count: count }
|
172
183
|
end
|
173
184
|
|
174
|
-
|
175
|
-
|
185
|
+
def parse_comment(comment)
|
186
|
+
tables = parse_tables_from_comment(comment)
|
187
|
+
violations = {}
|
188
|
+
tables.each do |table|
|
189
|
+
next unless table =~ %r{<th width="100%"(.*?)</th>}im
|
190
|
+
title = Regexp.last_match(1)
|
191
|
+
kind = table_kind_from_title(title)
|
192
|
+
next unless kind
|
176
193
|
|
177
|
-
|
178
|
-
|
179
|
-
table.scan(regex).flatten.map(&:strip)
|
180
|
-
end
|
194
|
+
violations[kind] = violations_from_table(table)
|
195
|
+
end
|
181
196
|
|
182
|
-
|
183
|
-
if title =~ /error/i
|
184
|
-
:error
|
185
|
-
elsif title =~ /warning/i
|
186
|
-
:warning
|
187
|
-
elsif title =~ /message/i
|
188
|
-
:message
|
197
|
+
violations.reject { |_, v| v.empty? }
|
189
198
|
end
|
190
|
-
end
|
191
199
|
|
192
|
-
|
193
|
-
|
194
|
-
|
200
|
+
def violations_from_table(table)
|
201
|
+
regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
|
202
|
+
table.scan(regex).flatten.map(&:strip)
|
203
|
+
end
|
195
204
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
205
|
+
def table_kind_from_title(title)
|
206
|
+
if title =~ /error/i
|
207
|
+
:error
|
208
|
+
elsif title =~ /warning/i
|
209
|
+
:warning
|
210
|
+
elsif title =~ /message/i
|
211
|
+
:message
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def parse_tables_from_comment(comment)
|
216
|
+
comment.split('</table>')
|
217
|
+
end
|
218
|
+
|
219
|
+
def process_markdown(violation)
|
220
|
+
html = markdown_parser.render(violation.message)
|
221
|
+
match = html.match(%r{^<p>(.*)</p>$})
|
222
|
+
message = match.nil? ? html : match.captures.first
|
223
|
+
Violation.new(message, violation.sticky)
|
224
|
+
end
|
201
225
|
end
|
202
226
|
end
|
203
227
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Danger
|
2
|
+
module RequestSources
|
3
|
+
class RequestSource
|
4
|
+
attr_accessor :ci_source, :environment, :scm, :host, :ignored_violations
|
5
|
+
|
6
|
+
def self.inherited(child_class)
|
7
|
+
available_request_sources.add child_class
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.available_request_sources
|
12
|
+
@available_request_sources ||= Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(_ci_source, _environment)
|
16
|
+
raise "Subclass and overwrite initialize"
|
17
|
+
end
|
18
|
+
|
19
|
+
def validates?
|
20
|
+
!!self.scm.origins.match(%r{#{Regexp.escape self.host}(:|/)(?<repo_slug>.+/.+?)(?:\.git)?$})
|
21
|
+
end
|
22
|
+
|
23
|
+
def scm
|
24
|
+
@scm ||= nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def host
|
28
|
+
@host ||= nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def ignored_violations
|
32
|
+
@ignored_violations ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_pull_request!(_warnings: [], _errors: [], _messages: [], _markdowns: [])
|
36
|
+
raise "Subclass and overwrite update_pull_request!"
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_danger_branches
|
40
|
+
raise "Subclass and overwrite setup_danger_branches"
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_details
|
44
|
+
raise "Subclass and overwrite initialize"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/danger/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: danger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Orta Therox
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-06-
|
12
|
+
date: 2016-06-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: claide
|
@@ -273,6 +273,7 @@ files:
|
|
273
273
|
- lib/danger/ci_source/jenkins.rb
|
274
274
|
- lib/danger/ci_source/local_git_repo.rb
|
275
275
|
- lib/danger/ci_source/semaphore.rb
|
276
|
+
- lib/danger/ci_source/teamcity.rb
|
276
277
|
- lib/danger/ci_source/travis.rb
|
277
278
|
- lib/danger/ci_source/xcode_server.rb
|
278
279
|
- lib/danger/commands/init.rb
|
@@ -295,6 +296,7 @@ files:
|
|
295
296
|
- lib/danger/plugin_support/plugin.rb
|
296
297
|
- lib/danger/plugin_support/plugin_parser.rb
|
297
298
|
- lib/danger/request_source/github.rb
|
299
|
+
- lib/danger/request_source/request_source.rb
|
298
300
|
- lib/danger/scm_source/git_repo.rb
|
299
301
|
- lib/danger/version.rb
|
300
302
|
homepage: http://github.com/danger/danger
|