danger 9.4.3 → 9.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235adb876d4329ef24664c6139ed7266298db3f672415e83cf78d1b9dfb87af9
4
- data.tar.gz: 359771a09da8778f29d3d6d0fa26d803474813ae8d4a154fdef66489f7b3919b
3
+ metadata.gz: 67fd79bdc597c9e9f26f590ad3ca50c045cbd9a543225ac53d66ba174ba20ff5
4
+ data.tar.gz: 1ba194d96a1eb7a0b430270e2d129df3de3989e904833bba703e672f27973c91
5
5
  SHA512:
6
- metadata.gz: ee8c64ffa617f2dc8e4c9dd00861dfac8243c6898cd4b10f2185358b95bbe3278ab6d3265db375cd6cb7fc33d1b5e6c7499db879a735f8e22e46737e5fc8a2da
7
- data.tar.gz: 82bc4ac05e20fda0f0fc0936ed14eeadb8446effd43f65c673edbdcb6af364c3c0c22106bf8e3d667e1efba1e11ee9e9d9d83b7f13818b85779159f9bcbed9ad
6
+ metadata.gz: 4cd4fdc164a74ffd74b20cf9cbe1b0472d515f848ea6e0da9cfad60587d1aef1c09fe0bda83bbfa6d666fba51213f1c6613774597b9c1624b43facae2ff57200
7
+ data.tar.gz: f35871a565daf2296a1493ced5b9536d2b0aa385ac8acdfeff0301f247628424ba645c2c51b7ac2d2026307d07c7ae4da11ef6239a889175a85be21c0807ec6b
data/README.md CHANGED
@@ -19,11 +19,11 @@ Formalize your Pull Request etiquette.
19
19
 
20
20
  ## What is Danger?
21
21
 
22
- Danger runs after your CI, automating your team's conventions surrounding code review.
22
+ Danger runs during your CI process, and gives teams the chance to automate common code review chores.
23
23
 
24
24
  This provides another logical step in your process, through this Danger can help lint your rote tasks in daily code review.
25
25
 
26
- You can use Danger to codify your team's norms, leaving humans to think about harder problems.
26
+ You can use Danger to codify your teams norms. Leaving humans to think about harder problems.
27
27
 
28
28
  ## For example?
29
29
 
@@ -19,7 +19,7 @@ module Danger
19
19
  # #### GitHub
20
20
  #
21
21
  # You need to add the `DANGER_GITHUB_API_TOKEN` environment variable, to do this, go to your build definition's variables tab.
22
- # #
22
+ #
23
23
  # #### Azure Git
24
24
  #
25
25
  # You need to add the `DANGER_VSTS_API_TOKEN` and `DANGER_VSTS_HOST` environment variable, to do this,
@@ -10,10 +10,31 @@ module Danger
10
10
  #
11
11
  # ### Token Setup
12
12
  #
13
- # Add `DANGER_BITBUCKETCLOUD_USERNAME` and `DANGER_BITBUCKETCLOUD_PASSWORD` to your pipeline repository variable
14
- # or instead using `DANGER_BITBUCKETCLOUD_OAUTH_KEY` and `DANGER_BITBUCKETCLOUD_OAUTH_SECRET`.
13
+ # For username and password, you need to set.
15
14
  #
16
- # You can find them in Settings > Pipelines > Repository Variables
15
+ # - `DANGER_BITBUCKETCLOUD_USERNAME` = The username for the account used to comment, as shown on
16
+ # https://bitbucket.org/account/
17
+ # - `DANGER_BITBUCKETCLOUD_PASSWORD` = The password for the account used to comment, you could use
18
+ # [App passwords](https://confluence.atlassian.com/bitbucket/app-passwords-828781300.html#Apppasswords-Aboutapppasswords)
19
+ # with Read Pull Requests and Read Account Permissions.
20
+ #
21
+ # For OAuth key and OAuth secret, you can get them from.
22
+ #
23
+ # - Open [BitBucket Cloud Website](https://bitbucket.org)
24
+ # - Navigate to Settings > OAuth > Add consumer
25
+ # - Put `https://bitbucket.org/site/oauth2/authorize` for `Callback URL`, and enable Read Pull requests, and Read Account
26
+ # Permission.
27
+ #
28
+ # - `DANGER_BITBUCKETCLOUD_OAUTH_KEY` = The consumer key for the account used to comment, as show as `Key` on the website.
29
+ # - `DANGER_BITBUCKETCLOUD_OAUTH_SECRET` = The consumer secret for the account used to comment, as show as `Secret` on the
30
+ # website.
31
+ #
32
+ # For [repository access token](https://support.atlassian.com/bitbucket-cloud/docs/repository-access-tokens/), what you
33
+ # need to create one is:
34
+ #
35
+ # - Open your repository URL
36
+ # - Navigate to Settings > Security > Access Tokens > Create Repository Access Token
37
+ # - Give it a name and set Pull requests write scope
17
38
 
18
39
  class BitbucketPipelines < CI
19
40
  def self.validates_as_ci?(env)
@@ -19,13 +19,13 @@ module Danger
19
19
  #
20
20
  # Add the `DANGER_GITHUB_API_TOKEN` to your workflow's [Secret App Env Vars](https://blog.bitrise.io/anyone-even-prs-can-have-secrets).
21
21
  #
22
- # ### bitbucket server and bitrise
22
+ # ### Bitbucket Server and Bitrise
23
23
  #
24
- # Danger will read the environment variable GIT_REPOSITORY_URL to construct the Bitbucket Server API URL
25
- # finding the project and repo slug in the GIT_REPOSITORY_URL variable. This GIT_REPOSITORY_URL variable
24
+ # Danger will read the environment variable `GIT_REPOSITORY_URL` to construct the Bitbucket Server API URL
25
+ # finding the project and repo slug in the `GIT_REPOSITORY_URL` variable. This `GIT_REPOSITORY_URL` variable
26
26
  # comes from the App Settings tab for your Bitrise App. If you are manually setting a repo URL in the
27
27
  # Git Clone Repo step, you may need to set adjust this property in the settings tab, maybe even fake it.
28
- # The patterns used are `(%r{\.com/(.*)})` and `(%r{\.com:(.*)})` and .split(/\.git$|$/) to remove ".git" if the URL contains it.
28
+ # The patterns used are `(%r{\.com/(.*)})` and `(%r{\.com:(.*)})` and `.split(/\.git$|$/)` to remove ".git" if the URL contains it.
29
29
  #
30
30
  class Bitrise < CI
31
31
  def self.validates_as_ci?(env)
@@ -7,7 +7,7 @@ module Danger
7
7
  # ### CI Setup
8
8
  #
9
9
  # With BuildKite you run the server yourself, so you will want to run it as a part of your build process.
10
- # It is common to have build steps, so we would recommend adding this to your scrip:
10
+ # It is common to have build steps, so we would recommend adding this to your script:
11
11
  #
12
12
  # ```shell
13
13
  # echo "--- Running Danger"
@@ -4,8 +4,8 @@ require "danger/request_sources/github/github"
4
4
  module Danger
5
5
  # ### CI Setup
6
6
  #
7
- # In CodeBuild, make sure to correctly forward CODEBUILD_BUILD_ID, CODEBUILD_SOURCE_VERSION, CODEBUILD_SOURCE_REPO_URL and DANGER_GITHUB_API_TOKEN.
8
- # In CodeBuild with batch builds, make sure to correctly forward CODEBUILD_BUILD_ID, CODEBUILD_WEBHOOK_TRIGGER, CODEBUILD_SOURCE_REPO_URL, CODEBUILD_BATCH_BUILD_IDENTIFIER and DANGER_GITHUB_API_TOKEN.
7
+ # In CodeBuild, make sure to correctly forward `CODEBUILD_BUILD_ID`, `CODEBUILD_SOURCE_VERSION`, `CODEBUILD_SOURCE_REPO_URL` and `DANGER_GITHUB_API_TOKEN`.
8
+ # In CodeBuild with batch builds, make sure to correctly forward `CODEBUILD_BUILD_ID`, `CODEBUILD_WEBHOOK_TRIGGER`, `CODEBUILD_SOURCE_REPO_URL`, `CODEBUILD_BATCH_BUILD_IDENTIFIER` and `DANGER_GITHUB_API_TOKEN`.
9
9
  #
10
10
  # ### Token Setup
11
11
  #
@@ -14,7 +14,6 @@ require "danger/ci_source/support/pull_request_finder"
14
14
  require "danger/ci_source/support/commits"
15
15
 
16
16
  module Danger
17
- # ignore
18
17
  class LocalGitRepo < CI
19
18
  attr_accessor :base_commit, :head_commit
20
19
 
@@ -4,7 +4,6 @@ require "faraday/http_cache"
4
4
  require "fileutils"
5
5
  require "octokit"
6
6
  require "tmpdir"
7
- require "no_proxy_fix"
8
7
 
9
8
  module Danger
10
9
  class PR < Runner
@@ -7,6 +7,8 @@ module Danger
7
7
 
8
8
  # Finds a Danger::CI class based on the ENV
9
9
  def self.local_ci_source(env)
10
+ return Danger::LocalOnlyGitRepo if LocalOnlyGitRepo.validates_as_ci? env
11
+
10
12
  CI.available_ci_sources.find { |ci| ci.validates_as_ci? env }
11
13
  end
12
14
 
@@ -106,7 +108,7 @@ module Danger
106
108
  title = "For Danger to run on this project, you need to expose a set of following the ENV vars:\n#{RequestSources::RequestSource.available_source_names_and_envs.join("\n")}"
107
109
  end
108
110
 
109
- [title, (subtitle || "")]
111
+ [title, subtitle || ""]
110
112
  end
111
113
 
112
114
  def ui_display_no_request_source_error_message(ui, env, title, subtitle)
@@ -237,7 +237,7 @@ module Danger
237
237
  # @!group GitHub Misc
238
238
  # Use to ignore inline messages which lay outside a diff's range, thereby not posting them in the main comment.
239
239
  # You can set hash to change behavior per each kinds. (ex. `{warning: true, error: false}`)
240
- # @param [Bool] or [Hash<Symbol, Bool>] dismiss
240
+ # @param [Bool or Hash<Symbol, Bool>] dismiss
241
241
  # Ignore out of range inline messages, defaults to `true`
242
242
  #
243
243
  # @return [void]
@@ -248,7 +248,7 @@ module Danger
248
248
  # @!group Gitlab Misc
249
249
  # Use to ignore inline messages which lay outside a diff's range, thereby not posting the comment.
250
250
  # You can set hash to change behavior per each kinds. (ex. `{warning: true, error: false}`)
251
- # @param [Bool] or [Hash<Symbol, Bool>] dismiss
251
+ # @param [Bool or Hash<Symbol, Bool>] dismiss
252
252
  # Ignore out of range inline messages, defaults to `true`
253
253
  #
254
254
  # @return [void]
@@ -13,12 +13,13 @@ module Danger
13
13
  # If it's not called again on subsequent runs.
14
14
  #
15
15
  # Each of `message`, `warn`, `fail` and `markdown` support multiple passed arguments
16
- # @example
17
16
  #
18
- # message 'Hello', 'World', file: "Dangerfile", line: 1
19
- # warn ['This', 'is', 'warning'], file: "Dangerfile", line: 1
20
- # failure 'Ooops', 'bad bad error', sticky: false
21
- # markdown '# And', '# Even', '# Markdown', file: "Dangerfile", line: 1
17
+ # @example Multiple passed arguments
18
+ #
19
+ # message 'Hello', 'World', file: "Dangerfile", line: 1
20
+ # warn ['This', 'is', 'warning'], file: "Dangerfile", line: 1
21
+ # failure 'Ooops', 'bad bad error', sticky: false
22
+ # markdown '# And', '# Even', '# Markdown', file: "Dangerfile", line: 1
22
23
  #
23
24
  # By default, using `failure` would fail the corresponding build. Either via an API call, or
24
25
  # via the return value for the danger command. Older code examples use `fail` which is an alias
@@ -87,11 +88,12 @@ module Danger
87
88
  # @!group Core
88
89
  # Print markdown to below the table
89
90
  #
90
- # @param [String, Array<String>] message
91
+ # @param [Hash] options
92
+ # @option [String, Array<String>] markdowns
91
93
  # The markdown based message to be printed below the table
92
- # @param [String] file
94
+ # @option [String] file
93
95
  # Optional. Path to the file that the message is for.
94
- # @param [String] line
96
+ # @option [String] line
95
97
  # Optional. The line in the file to present the message in.
96
98
  # @return [void]
97
99
  #
@@ -107,14 +109,15 @@ module Danger
107
109
  # @!group Core
108
110
  # Print out a generate message on the PR
109
111
  #
110
- # @param [String, Array<String>] message
112
+ # @param [String, Array<String>] messages
111
113
  # The message to present to the user
112
- # @param [Boolean] sticky
114
+ # @param [Hash] options
115
+ # @option [Boolean] sticky
113
116
  # Whether the message should be kept after it was fixed,
114
117
  # defaults to `false`.
115
- # @param [String] file
118
+ # @option [String] file
116
119
  # Optional. Path to the file that the message is for.
117
- # @param [String] line
120
+ # @option [String] line
118
121
  # Optional. The line in the file to present the message in.
119
122
  # @return [void]
120
123
  #
@@ -131,14 +134,15 @@ module Danger
131
134
  # @!group Core
132
135
  # Specifies a problem, but not critical
133
136
  #
134
- # @param [String, Array<String>] message
137
+ # @param [String, Array<String>] warnings
135
138
  # The message to present to the user
136
- # @param [Boolean] sticky
139
+ # @param options
140
+ # @option [Boolean] sticky
137
141
  # Whether the message should be kept after it was fixed,
138
142
  # defaults to `false`.
139
- # @param [String] file
143
+ # @option [String] file
140
144
  # Optional. Path to the file that the message is for.
141
- # @param [String] line
145
+ # @option [String] line
142
146
  # Optional. The line in the file to present the message in.
143
147
  # @return [void]
144
148
  #
@@ -157,14 +161,15 @@ module Danger
157
161
  # @!group Core
158
162
  # Declares a CI blocking error
159
163
  #
160
- # @param [String, Array<String>] message
164
+ # @param [String, Array<String>] failures
161
165
  # The message to present to the user
162
- # @param [Boolean] sticky
166
+ # @param options
167
+ # @option [Boolean] sticky
163
168
  # Whether the message should be kept after it was fixed,
164
169
  # defaults to `false`.
165
- # @param [String] file
170
+ # @option [String] file
166
171
  # Optional. Path to the file that the message is for.
167
- # @param [String] line
172
+ # @option [String] line
168
173
  # Optional. The line in the file to present the message in.
169
174
  # @return [void]
170
175
  #
@@ -14,14 +14,14 @@ module Danger
14
14
  Kramdown::Document.new(text, input: "GFM", smart_quotes: %w(apos apos quot quot))
15
15
  end
16
16
 
17
- # !@group Extension points
17
+ # @!group Extension points
18
18
  # Produces a markdown link to the file the message points to
19
19
  #
20
20
  # request_source implementations are invited to override this method with their
21
21
  # vendor specific link.
22
22
  #
23
23
  # @param [Violation or Markdown] message
24
- # @param [Bool] Should hide any generated link created
24
+ # @param [Bool] hide_link Should hide any generated link created
25
25
  #
26
26
  # @return [String] The Markdown compatible link
27
27
  def markdown_link_to_message(message, hide_link)
@@ -30,12 +30,12 @@ module Danger
30
30
  "#{message.file}#L#{message.line}"
31
31
  end
32
32
 
33
- # !@group Extension points
33
+ # @!group Extension points
34
34
  # Determine whether two messages are equivalent
35
35
  #
36
36
  # request_source implementations are invited to override this method.
37
37
  # This is mostly here to enable sources to detect when inlines change only in their
38
- # commit hash and not in content per-se. since the link is implementation dependant
38
+ # commit hash and not in content per-se. since the link is implementation dependent
39
39
  # so should be the comparison.
40
40
  #
41
41
  # @param [Violation or Markdown] m1
@@ -46,6 +46,8 @@ module Danger
46
46
  m1 == m2
47
47
  end
48
48
 
49
+ # @endgroup
50
+
49
51
  def process_markdown(violation, hide_link = false)
50
52
  message = violation.message
51
53
  message = "#{markdown_link_to_message(violation, hide_link)}#{message}" if violation.file && violation.line
@@ -1,7 +1,7 @@
1
1
  module Danger
2
2
  module Helpers
3
3
  module CommentsParsingHelper
4
- # !@group Extension points
4
+ # @!group Extension points
5
5
  # Produces a message-like from a row in a comment table
6
6
  #
7
7
  # @param [String] row
@@ -12,6 +12,8 @@ module Danger
12
12
  Violation.new(row, true)
13
13
  end
14
14
 
15
+ # @endgroup
16
+
15
17
  def parse_tables_from_comment(comment)
16
18
  comment.split("</table>")
17
19
  end
@@ -9,17 +9,24 @@ module Danger
9
9
  attr_accessor :pr_json
10
10
 
11
11
  def self.env_vars
12
+ ["DANGER_BITBUCKETCLOUD_UUID"]
13
+ end
14
+
15
+ # While it says "optional", one of these is required to run Danger on Bitbucket Cloud.
16
+ #
17
+ # - Both `DANGER_BITBUCKETCLOUD_OAUTH_KEY` and `DANGER_BITBUCKETCLOUD_OAUTH_SECRET`
18
+ # - Both `DANGER_BITBUCKETCLOUD_USERNAME` and `DANGER_BITBUCKETCLOUD_PASSWORD`
19
+ # - `DANGER_BITBUCKETCLOUD_REPO_ACCESSTOKEN`
20
+ def self.optional_env_vars
12
21
  [
22
+ "DANGER_BITBUCKETCLOUD_OAUTH_KEY",
23
+ "DANGER_BITBUCKETCLOUD_OAUTH_SECRET",
24
+ "DANGER_BITBUCKETCLOUD_REPO_ACCESSTOKEN",
13
25
  "DANGER_BITBUCKETCLOUD_USERNAME",
14
- "DANGER_BITBUCKETCLOUD_UUID",
15
26
  "DANGER_BITBUCKETCLOUD_PASSWORD"
16
27
  ]
17
28
  end
18
29
 
19
- def self.optional_env_vars
20
- ["DANGER_BITBUCKETCLOUD_OAUTH_KEY", "DANGER_BITBUCKETCLOUD_OAUTH_SECRET"]
21
- end
22
-
23
30
  def initialize(ci_source, environment)
24
31
  self.ci_source = ci_source
25
32
 
@@ -75,7 +82,7 @@ module Danger
75
82
  messages = update_inline_comments_for_kind!(:messages, messages, danger_id: danger_id)
76
83
  markdowns = update_inline_comments_for_kind!(:markdowns, markdowns, danger_id: danger_id)
77
84
 
78
- has_comments = (warnings.count.positive? || errors.count.positive? || messages.count.positive? || markdowns.count.positive?)
85
+ has_comments = warnings.count.positive? || errors.count.positive? || messages.count.positive? || markdowns.count.positive?
79
86
  if has_comments
80
87
  comment = generate_description(warnings: warnings, errors: errors, template: "bitbucket_server")
81
88
  comment += "\n\n"
@@ -39,6 +39,8 @@ module Danger
39
39
  end
40
40
 
41
41
  def credentials_given?
42
+ return true if @access_token
43
+
42
44
  @my_uuid && !@my_uuid.empty? &&
43
45
  @username && !@username.empty? &&
44
46
  @password && !@password.empty?
@@ -105,6 +107,12 @@ module Danger
105
107
  end
106
108
 
107
109
  def fetch_access_token(environment)
110
+ access_token = environment["DANGER_BITBUCKETCLOUD_REPO_ACCESSTOKEN"]
111
+ if access_token
112
+ @access_token = access_token
113
+ return access_token
114
+ end
115
+
108
116
  oauth_key = environment["DANGER_BITBUCKETCLOUD_OAUTH_KEY"]
109
117
  oauth_secret = environment["DANGER_BITBUCKETCLOUD_OAUTH_SECRET"]
110
118
  return nil if oauth_key.nil?
@@ -115,7 +115,7 @@ module Danger
115
115
  markdowns = main_violations[:markdowns] || []
116
116
  end
117
117
 
118
- has_comments = (warnings.count > 0 || errors.count > 0 || messages.count > 0 || markdowns.count > 0)
118
+ has_comments = warnings.count > 0 || errors.count > 0 || messages.count > 0 || markdowns.count > 0
119
119
  if has_comments
120
120
  comment = generate_description(warnings: warnings,
121
121
  errors: errors)
@@ -77,7 +77,24 @@ module Danger
77
77
  end
78
78
 
79
79
  def pr_diff
80
- @pr_diff ||= client.pull_request(ci_source.repo_slug, ci_source.pull_request_id, accept: "application/vnd.github.v3.diff")
80
+ # This is a hack to get the file patch into a format that parse-diff accepts
81
+ # as the GitHub API for listing pull request files is missing file names in the patch.
82
+ prefixed_patch = lambda do |file:|
83
+ <<~PATCH
84
+ diff --git a/#{file['filename']} b/#{file['filename']}
85
+ --- a/#{file['filename']}
86
+ +++ b/#{file['filename']}
87
+ #{file['patch']}
88
+ PATCH
89
+ end
90
+
91
+ files = client.pull_request_files(
92
+ ci_source.repo_slug,
93
+ ci_source.pull_request_id,
94
+ accept: "application/vnd.github.v3.diff"
95
+ )
96
+
97
+ @pr_diff ||= files.map { |file| prefixed_patch.call(file: file) }.join("\n")
81
98
  end
82
99
 
83
100
  def review
@@ -304,7 +304,7 @@ module Danger
304
304
 
305
305
  # @return [String] A URL to the specific file, ready to be downloaded
306
306
  def file_url(organisation: nil, repository: nil, ref: nil, branch: nil, path: nil)
307
- ref ||= (branch || "master")
307
+ ref ||= branch || "master"
308
308
  # According to GitLab Repositories API docs path and id(slug) should be encoded.
309
309
  path = URI.encode_www_form_component(path)
310
310
  repository = URI.encode_www_form_component(repository)
@@ -139,7 +139,7 @@ module Danger
139
139
  end
140
140
 
141
141
  def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
142
- # Avoid doing any fetchs if there's no inline comments
142
+ # Avoid doing any fetches if there's no inline comments
143
143
  return {} if (warnings + errors + messages + markdowns).select(&:inline?).empty?
144
144
 
145
145
  pr_threads = @api.fetch_last_comments
@@ -1,4 +1,4 @@
1
1
  module Danger
2
- VERSION = "9.4.3".freeze
2
+ VERSION = "9.5.0".freeze
3
3
  DESCRIPTION = "Like Unit Tests, but for your Team Culture.".freeze
4
4
  end
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: 9.4.3
4
+ version: 9.5.0
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: 2024-02-04 00:00:00.000000000 Z
12
+ date: 2024-08-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: claide
@@ -143,20 +143,6 @@ dependencies:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
145
  version: '1.0'
146
- - !ruby/object:Gem::Dependency
147
- name: no_proxy_fix
148
- requirement: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- type: :runtime
154
- prerelease: false
155
- version_requirements: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0'
160
146
  - !ruby/object:Gem::Dependency
161
147
  name: octokit
162
148
  requirement: !ruby/object:Gem::Requirement
@@ -344,7 +330,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
330
  - !ruby/object:Gem::Version
345
331
  version: '0'
346
332
  requirements: []
347
- rubygems_version: 3.3.7
333
+ rubygems_version: 3.5.14
348
334
  signing_key:
349
335
  specification_version: 4
350
336
  summary: Like Unit Tests, but for your Team Culture.