pact_broker 2.19.1 → 2.19.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +0 -1
  4. data/db/migrations/000002_create_versions_table.rb +1 -1
  5. data/db/migrations/20180311_optimise_head_matrix.rb +1 -0
  6. data/db/migrations/20180523_create_latest_verifications_for_consumer_version_tags.rb +50 -0
  7. data/db/migrations/20180524_create_latest_verifications_for_consumer_and_provider.rb +46 -0
  8. data/lib/pact_broker/api/decorators/webhook_execution_result_decorator.rb +7 -3
  9. data/lib/pact_broker/configuration.rb +6 -1
  10. data/lib/pact_broker/domain/webhook_request.rb +12 -11
  11. data/lib/pact_broker/index/service.rb +14 -84
  12. data/lib/pact_broker/matrix/aggregated_row.rb +71 -0
  13. data/lib/pact_broker/matrix/head_row.rb +23 -0
  14. data/lib/pact_broker/matrix/repository.rb +0 -1
  15. data/lib/pact_broker/matrix/row.rb +15 -20
  16. data/lib/pact_broker/pacts/content.rb +66 -0
  17. data/lib/pact_broker/pacts/create_formatted_diff.rb +0 -1
  18. data/lib/pact_broker/pacts/diff.rb +3 -2
  19. data/lib/pact_broker/pacts/generate_sha.rb +24 -0
  20. data/lib/pact_broker/pacts/parse.rb +11 -0
  21. data/lib/pact_broker/pacts/repository.rb +4 -4
  22. data/lib/pact_broker/pacts/service.rb +4 -2
  23. data/lib/pact_broker/pacts/sort_content.rb +54 -0
  24. data/lib/pact_broker/verifications/latest_verification_for_consumer_and_provider.rb +26 -0
  25. data/lib/pact_broker/verifications/latest_verification_for_consumer_version_tag.rb +23 -0
  26. data/lib/pact_broker/version.rb +1 -1
  27. data/lib/pact_broker/versions/repository.rb +12 -3
  28. data/script/run-with-ssl.rb +44 -0
  29. data/spec/features/base_equality_only_on_content_that_affects_verification_results_spec.rb +34 -0
  30. data/spec/lib/pact_broker/api/decorators/webhook_execution_result_decorator_spec.rb +6 -2
  31. data/spec/lib/pact_broker/domain/{index_items_spec.rb → index_item_spec.rb} +0 -0
  32. data/spec/lib/pact_broker/domain/webhook_request_spec.rb +16 -5
  33. data/spec/lib/pact_broker/matrix/aggregated_row_spec.rb +79 -0
  34. data/spec/lib/pact_broker/matrix/head_row_spec.rb +55 -0
  35. data/spec/lib/pact_broker/matrix/row_spec.rb +22 -2
  36. data/spec/lib/pact_broker/pacts/content_spec.rb +166 -0
  37. data/spec/lib/pact_broker/pacts/create_formatted_diff_spec.rb +0 -3
  38. data/spec/lib/pact_broker/pacts/generate_sha_spec.rb +92 -0
  39. data/spec/lib/pact_broker/pacts/repository_spec.rb +47 -4
  40. data/spec/lib/pact_broker/pacts/sort_content_spec.rb +44 -0
  41. data/spec/lib/pact_broker/versions/repository_spec.rb +13 -5
  42. data/spec/spec_helper.rb +1 -0
  43. data/spec/support/foo-bar.json +34 -0
  44. data/spec/support/rspec_match_hash.rb +14 -17
  45. data/spec/support/test_data_builder.rb +5 -4
  46. metadata +26 -8
  47. data/lib/pact_broker/matrix/latest_row.rb +0 -10
  48. data/lib/pact_broker/pacts/sort_verifiable_content.rb +0 -41
  49. data/spec/lib/pact_broker/pacts/sort_verifiable_content_spec.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f1619d6f62ce6aaa00ca73eca6b1cdf76ee0305
4
- data.tar.gz: c8fc20b4a52499d6bc259a4937d463c1a902567e
3
+ metadata.gz: 6596910c63bcfb8363e958de4bb4bb698df0613e
4
+ data.tar.gz: 929b0ac8d06300c3a6226f4afb3af099f97c7476
5
5
  SHA512:
6
- metadata.gz: 1a8ad6a1480b3a288c11e66f4473ba5af7890fe6ccab8f5ff81f69054dd0df7dc47d24ffc3e77b1888a202b5c9d73ef118794a9c8def6acce31e25590962f89c
7
- data.tar.gz: c7270ddcf9932fc5579263dbb0149de7b1e3da15f8e69be217f4b2ddbc47182b409b0fcb0fc3b2d232922b854caae41eb47f2fc08d4ec9ae84d5773b3152751e
6
+ metadata.gz: aaf62c676cffcc46679e3cba55afd4234d74208577869803d76e670805b314d7e8a742525de8d3599aafa5f5769d50b62b76ee8464796766c42c92dc73807b36
7
+ data.tar.gz: 3cb076e57dcb8f23450ddd50e1b8f92f4161f79fd4541799296a5fdc0ed4671e13d1ba409eb0dfeff60c41cc86cc3472a304fb6e1068b6219a2359b3365799e7
@@ -1,3 +1,23 @@
1
+ <a name="v2.19.2"></a>
2
+ ### v2.19.2 (2018-05-29)
3
+
4
+
5
+ #### Features
6
+
7
+ * load latest verification for consumer/provider via relationship rather than repository ([13b7c6e](/../../commit/13b7c6e))
8
+ * include more columns in latest_verifications_for_consumer_version_tags to avoid having to do extra queries for pact_versions and provider_versions ([e366af4](/../../commit/e366af4))
9
+ * optimise queries for index page with tags ([524e08d](/../../commit/524e08d))
10
+ * create view for latest verifications for consumer version tags ([eb67511](/../../commit/eb67511))
11
+ * log pact content as debug instead of info when publishing ([d116157](/../../commit/d116157))
12
+ * allow pact equality to be based only on the content that affects verification results ([bf8130f](/../../commit/bf8130f))
13
+
14
+
15
+ #### Bug Fixes
16
+
17
+ * temporarily redact webhook response body from UI for security purposes ([becf20c](/../../commit/becf20c))
18
+ * handle race condition causing unique constraint violation when creating pacticipant versions ([6c75ebd](/../../commit/6c75ebd))
19
+
20
+
1
21
  <a name="v2.19.1"></a>
2
22
  ### v2.19.1 (2018-05-18)
3
23
 
data/README.md CHANGED
@@ -4,7 +4,6 @@
4
4
  [![Code Climate](https://codeclimate.com/github/pact-foundation/pact_broker/badges/gpa.svg)](https://codeclimate.com/github/pact-foundation/pact_broker)
5
5
  [![Test Coverage](https://codeclimate.com/github/pact-foundation/pact_broker/badges/coverage.svg)](https://codeclimate.com/github/pact-foundation/pact_broker/coverage)
6
6
  [![Issue Count](https://codeclimate.com/github/pact-foundation/pact_broker/badges/issue_count.svg)](https://codeclimate.com/github/pact-foundation/pact_broker)
7
- [![Dependency Status](https://gemnasium.com/badges/github.com/pact-foundation/pact_broker.svg)](https://gemnasium.com/github.com/pact-foundation/pact_broker)
8
7
 
9
8
  The Pact Broker is an application for sharing for consumer driven contracts and verification results. It is optimised for use with "pacts" (contracts created by the [Pact][pact-docs] framework), but can be used for any type of contract that can be serialized to JSON.
10
9
 
@@ -6,7 +6,7 @@ Sequel.migration do
6
6
  String :number
7
7
  String :repository_ref
8
8
  foreign_key :pacticipant_id, :pacticipants, :null=>false
9
- index [:pacticipant_id, :number], :unique=>true
9
+ index [:pacticipant_id, :number], :unique => true
10
10
  end
11
11
  end
12
12
 
@@ -16,6 +16,7 @@ Sequel.migration do
16
16
  )
17
17
 
18
18
  # Add provider_version_order to original definition
19
+ # The most recent verification for each pact version
19
20
  v = :verifications
20
21
  create_or_replace_view(:latest_verifications,
21
22
  from(v)
@@ -0,0 +1,50 @@
1
+ Sequel.migration do
2
+ up do
3
+ # The latest verification id for each consumer version tag
4
+ create_view(:latest_verification_ids_for_consumer_version_tags,
5
+ "select
6
+ pv.pacticipant_id as provider_id,
7
+ lpp.consumer_id,
8
+ t.name as consumer_version_tag_name,
9
+ max(v.id) as latest_verification_id
10
+ from verifications v
11
+ join latest_pact_publications_by_consumer_versions lpp
12
+ on v.pact_version_id = lpp.pact_version_id
13
+ join tags t
14
+ on lpp.consumer_version_id = t.version_id
15
+ join versions pv
16
+ on v.provider_version_id = pv.id
17
+ group by pv.pacticipant_id, lpp.consumer_id, t.name")
18
+
19
+ # The most recent verification for each consumer/consumer version tag/provider
20
+ latest_verifications = from(:verifications)
21
+ .select(
22
+ Sequel[:lv][:consumer_id],
23
+ Sequel[:lv][:provider_id],
24
+ Sequel[:lv][:consumer_version_tag_name],
25
+ Sequel[:pv][:sha].as(:pact_version_sha),
26
+ Sequel[:prv][:number].as(:provider_version_number),
27
+ Sequel[:prv][:order].as(:provider_version_order),
28
+ )
29
+ .select_append{ verifications.* }
30
+ .join(:latest_verification_ids_for_consumer_version_tags,
31
+ {
32
+ Sequel[:verifications][:id] => Sequel[:lv][:latest_verification_id],
33
+ }, { table_alias: :lv })
34
+ .join(:versions,
35
+ {
36
+ Sequel[:verifications][:provider_version_id] => Sequel[:prv][:id]
37
+ }, { table_alias: :prv })
38
+ .join(:pact_versions,
39
+ {
40
+ Sequel[:verifications][:pact_version_id] => Sequel[:pv][:id]
41
+ }, { table_alias: :pv })
42
+
43
+ create_or_replace_view(:latest_verifications_for_consumer_version_tags, latest_verifications)
44
+ end
45
+
46
+ down do
47
+ drop_view(:latest_verifications_for_consumer_version_tags)
48
+ drop_view(:latest_verification_ids_for_consumer_version_tags)
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ Sequel.migration do
2
+ up do
3
+ # The latest verification id for each consumer version tag
4
+ create_view(:latest_verification_ids_for_consumer_and_provider,
5
+ "select
6
+ pv.pacticipant_id as provider_id,
7
+ lpp.consumer_id,
8
+ max(v.id) as latest_verification_id
9
+ from verifications v
10
+ join latest_pact_publications_by_consumer_versions lpp
11
+ on v.pact_version_id = lpp.pact_version_id
12
+ join versions pv
13
+ on v.provider_version_id = pv.id
14
+ group by pv.pacticipant_id, lpp.consumer_id")
15
+
16
+ # The most recent verification for each consumer/consumer version tag/provider
17
+ latest_verifications = from(:verifications)
18
+ .select(
19
+ Sequel[:lv][:consumer_id],
20
+ Sequel[:lv][:provider_id],
21
+ Sequel[:pv][:sha].as(:pact_version_sha),
22
+ Sequel[:prv][:number].as(:provider_version_number),
23
+ Sequel[:prv][:order].as(:provider_version_order),
24
+ )
25
+ .select_append{ verifications.* }
26
+ .join(:latest_verification_ids_for_consumer_and_provider,
27
+ {
28
+ Sequel[:verifications][:id] => Sequel[:lv][:latest_verification_id],
29
+ }, { table_alias: :lv })
30
+ .join(:versions,
31
+ {
32
+ Sequel[:verifications][:provider_version_id] => Sequel[:prv][:id]
33
+ }, { table_alias: :prv })
34
+ .join(:pact_versions,
35
+ {
36
+ Sequel[:verifications][:pact_version_id] => Sequel[:pv][:id]
37
+ }, { table_alias: :pv })
38
+
39
+ create_or_replace_view(:latest_verifications_for_consumer_and_provider, latest_verifications)
40
+ end
41
+
42
+ down do
43
+ drop_view(:latest_verifications_for_consumer_and_provider)
44
+ drop_view(:latest_verification_ids_for_consumer_and_provider)
45
+ end
46
+ end
@@ -34,11 +34,15 @@ module PactBroker
34
34
  represented.body
35
35
  end
36
36
  end
37
-
38
37
  end
39
38
 
40
- property :error, :extend => ErrorDecorator
41
- property :response, :extend => HTTPResponseDecorator
39
+ property :message, exec_context: :decorator
40
+ # property :error, :extend => ErrorDecorator
41
+ # property :response, :extend => HTTPResponseDecorator
42
+
43
+ def message
44
+ "Webhook response has been redacted temporarily for security purposes. Please see the Pact Broker application logs for the response body."
45
+ end
42
46
 
43
47
  link :webhook do | options |
44
48
  {
@@ -27,13 +27,14 @@ module PactBroker
27
27
  ]
28
28
 
29
29
  attr_accessor :log_dir, :database_connection, :auto_migrate_db, :use_hal_browser, :html_pact_renderer
30
- attr_accessor :validate_database_connection_config, :enable_diagnostic_endpoints, :version_parser
30
+ attr_accessor :validate_database_connection_config, :enable_diagnostic_endpoints, :version_parser, :sha_generator
31
31
  attr_accessor :use_case_sensitive_resource_names, :order_versions_by_date
32
32
  attr_accessor :check_for_potential_duplicate_pacticipant_names
33
33
  attr_accessor :semver_formats
34
34
  attr_accessor :enable_public_badge_access, :shields_io_base_url
35
35
  attr_accessor :webhook_retry_schedule
36
36
  attr_accessor :disable_ssl_verification
37
+ attr_accessor :base_equality_only_on_content_that_affects_verification_results
37
38
  attr_reader :api_error_reporters
38
39
  attr_writer :logger
39
40
 
@@ -51,6 +52,8 @@ module PactBroker
51
52
 
52
53
  def self.default_configuration
53
54
  require 'pact_broker/versions/parse_semantic_version'
55
+ require 'pact_broker/pacts/generate_sha'
56
+
54
57
  config = Configuration.new
55
58
  config.log_dir = File.expand_path("./log")
56
59
  config.auto_migrate_db = true
@@ -62,6 +65,8 @@ module PactBroker
62
65
  config.use_case_sensitive_resource_names = true
63
66
  config.html_pact_renderer = default_html_pact_render
64
67
  config.version_parser = PactBroker::Versions::ParseSemanticVersion
68
+ config.sha_generator = PactBroker::Pacts::GenerateSha
69
+ config.base_equality_only_on_content_that_affects_verification_results = false
65
70
  # Not recommended to set this to true unless there is no way to
66
71
  # consistently extract an orderable object from the consumer application version number.
67
72
  config.order_versions_by_date = false
@@ -114,18 +114,19 @@ module PactBroker
114
114
  execution_logger.info(" ")
115
115
  logger.info "Received response for webhook #{uuid} status=#{response.code}"
116
116
  execution_logger.info "HTTP/#{response.http_version} #{response.code} #{response.message}"
117
- response.each_header do | header |
118
- execution_logger.info "#{header.split("-").collect(&:capitalize).join('-')}: #{response[header]}"
119
- end
117
+ #response.each_header do | header |
118
+ # execution_logger.info "#{header.split("-").collect(&:capitalize).join('-')}: #{response[header]}"
119
+ #end
120
120
  logger.debug "body=#{response.body}"
121
- safe_body = nil
122
- if response.body
123
- safe_body = response.body.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
124
- if response.body != safe_body
125
- execution_logger.debug "Note that invalid UTF-8 byte sequences were removed from response body before saving the logs"
126
- end
127
- end
128
- execution_logger.info safe_body
121
+ # safe_body = nil
122
+ # if response.body
123
+ # safe_body = response.body.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
124
+ # if response.body != safe_body
125
+ # execution_logger.debug "Note that invalid UTF-8 byte sequences were removed from response body before saving the logs"
126
+ # end
127
+ # end
128
+ #execution_logger.info safe_body
129
+ execution_logger.info "Webhook response has been redacted temporarily for security purposes. Please see the Pact Broker application logs for the response body."
129
130
  end
130
131
 
131
132
  def log_completion_message options, execution_logger, success
@@ -1,8 +1,8 @@
1
1
  require 'pact_broker/repositories'
2
2
  require 'pact_broker/logging'
3
3
  require 'pact_broker/domain/index_item'
4
- require 'pact_broker/matrix/latest_row'
5
4
  require 'pact_broker/matrix/head_row'
5
+ require 'pact_broker/matrix/aggregated_row'
6
6
 
7
7
  module PactBroker
8
8
 
@@ -14,107 +14,37 @@ module PactBroker
14
14
  extend PactBroker::Logging
15
15
 
16
16
  def self.find_index_items options = {}
17
- rows = []
18
- overall_latest_publication_ids = nil
19
-
20
17
  rows = PactBroker::Matrix::HeadRow
21
18
  .select_all_qualified
22
19
  .eager(:latest_triggered_webhooks)
23
20
  .eager(:webhooks)
24
- .order(:consumer_name, :provider_name)
25
- .eager(:consumer_version_tags)
26
- .eager(:provider_version_tags)
27
21
 
28
22
  if !options[:tags]
29
- rows = rows.where(consumer_version_tag_name: nil).all
30
- overall_latest_publication_ids = rows.collect(&:pact_publication_id)
31
- end
32
-
33
- if options[:tags]
23
+ rows = rows.where(consumer_version_tag_name: nil)
24
+ else
34
25
  if options[:tags].is_a?(Array)
35
26
  rows = rows.where(consumer_version_tag_name: options[:tags]).or(consumer_version_tag_name: nil)
36
27
  end
37
-
38
- rows = rows.all
39
- overall_latest_publication_ids = rows.select{|r| !r[:consumer_version_tag_name] }.collect(&:pact_publication_id).uniq
40
-
41
- # Smoosh all the rows with matching pact publications together
42
- # and collect their consumer_head_tag_names
43
- rows = rows
44
- .group_by(&:pact_publication_id)
45
- .values
46
- .collect{|group| [group.last, group.collect{|r| r[:consumer_version_tag_name]}.compact] }
47
- .collect{ |(row, tag_names)| row.consumer_head_tag_names = tag_names; row }
28
+ rows = rows.eager(:consumer_version_tags)
29
+ .eager(:provider_version_tags)
30
+ .eager(:latest_verification_for_consumer_version_tag)
48
31
  end
32
+ rows = rows.all.group_by(&:pact_publication_id).values.collect{ | rows| Matrix::AggregatedRow.new(rows) }
49
33
 
50
- index_items = []
51
- rows.sort.each do | row |
52
- tag_names = []
53
- if options[:tags]
54
- tag_names = row.consumer_version_tags.collect(&:name)
55
- end
56
-
57
- overall_latest = overall_latest_publication_ids.include?(row.pact_publication_id)
58
- latest_verification = if overall_latest
59
- verification_repository.find_latest_verification_for row.consumer_name, row.provider_name
60
- else
61
- tag_names.collect do | tag_name |
62
- verification_repository.find_latest_verification_for row.consumer_name, row.provider_name, tag_name
63
- end.compact.sort do | v1, v2 |
64
- # Some provider versions have nil orders, not sure why
65
- # Sort by execution_date instead of order
66
- v1.execution_date <=> v2.execution_date
67
- end.last
68
- end
69
-
70
- index_items << PactBroker::Domain::IndexItem.create(
34
+ rows.sort.collect do | row |
35
+ # TODO simplify. Do we really need 3 layers of abstraction?
36
+ PactBroker::Domain::IndexItem.create(
71
37
  row.consumer,
72
38
  row.provider,
73
39
  row.pact,
74
- overall_latest,
75
- latest_verification,
40
+ row.overall_latest?,
41
+ row.latest_verification,
76
42
  row.webhooks,
77
43
  row.latest_triggered_webhooks,
78
- row.consumer_head_tag_names,
79
- row.provider_version_tags.select(&:latest?)
44
+ options[:tags] ? row.consumer_head_tag_names : [],
45
+ options[:tags] ? row.provider_version_tags.select(&:latest?) : []
80
46
  )
81
47
  end
82
-
83
- index_items
84
- end
85
-
86
- def self.tags_for(pact, options)
87
- if options[:tags] == true
88
- tag_service.find_all_tag_names_for_pacticipant(pact.consumer_name)
89
- elsif options[:tags].is_a?(Array)
90
- options[:tags]
91
- else
92
- []
93
- end
94
- end
95
-
96
- def self.build_index_item_rows(pact, tags)
97
- index_items = [build_latest_pact_index_item(pact, tags)]
98
- tags.each do | tag |
99
- index_items << build_index_item_for_tagged_pact(pact, tag)
100
- end
101
- index_items.compact
102
- end
103
-
104
- def self.build_latest_pact_index_item pact, tags
105
- latest_verification = verification_service.find_latest_verification_for(pact.consumer, pact.provider)
106
- webhooks = webhook_service.find_by_consumer_and_provider pact.consumer, pact.provider
107
- triggered_webhooks = webhook_service.find_latest_triggered_webhooks pact.consumer, pact.provider
108
- tag_names = pact.consumer_version_tag_names.select{ |name| tags.include?(name) }
109
- PactBroker::Domain::IndexItem.create pact.consumer, pact.provider, pact, true, latest_verification, webhooks, triggered_webhooks, tag_names
110
- end
111
-
112
- def self.build_index_item_for_tagged_pact latest_pact, tag
113
- pact = pact_service.find_latest_pact consumer_name: latest_pact.consumer_name, provider_name: latest_pact.provider_name, tag: tag
114
- return nil unless pact
115
- return nil if pact.id == latest_pact.id
116
- verification = verification_repository.find_latest_verification_for pact.consumer_name, pact.provider_name, tag
117
- PactBroker::Domain::IndexItem.create pact.consumer, pact.provider, pact, false, verification, [], [], [tag]
118
48
  end
119
49
  end
120
50
  end
@@ -0,0 +1,71 @@
1
+ require 'pact_broker/verifications/repository'
2
+
3
+ # A collection of matrix rows with the same pact publication id
4
+ # It's basically a normalised view of a denormalised view :(
5
+ # A pact publication may be the overall latest, and/or the latest for a tag
6
+ module PactBroker
7
+ module Matrix
8
+ class AggregatedRow
9
+ extend Forwardable
10
+
11
+ delegate [:consumer, :consumer_name, :consumer_version, :consumer_version_number, :consumer_version_order, :consumer_version_tags] => :first_row
12
+ delegate [:provider, :provider_name, :provider_version, :provider_version_number, :provider_version_order, :provider_version_tags] => :first_row
13
+ delegate [:pact, :pact_version, :pact_revision_number, :webhooks, :latest_triggered_webhooks, :'<=>'] => :first_row
14
+ delegate [:verification_id, :verification] => :first_row
15
+
16
+ def initialize matrix_rows
17
+ @matrix_rows = matrix_rows
18
+ @first_row = matrix_rows.first
19
+ end
20
+
21
+ def overall_latest?
22
+ !!matrix_rows.find{ | row| row.consumer_version_tag_name.nil? }
23
+ end
24
+
25
+ # If this comes back nil, it won't be "cached", but it's a reasonably
26
+ # quick query, so it's probably ok.
27
+ def latest_verification
28
+ @latest_verification ||= begin
29
+ verification = matrix_rows.collect do | row|
30
+ row.verification || latest_verification_for_consumer_version_tag(row)
31
+ end.compact.sort{ |v1, v2| v1.id <=> v2.id }.last
32
+
33
+ if !verification && overall_latest?
34
+ overall_latest_verification
35
+ else
36
+ verification
37
+ end
38
+ end
39
+ end
40
+
41
+ # The list of tag names for which this pact publication is the most recent with that tag
42
+ # There could, however, be a later consumer version that does't have a pact (perhaps because it was deleted)
43
+ # that has the same tag.
44
+ # TODO show a warning when the data is "corrupted" as above.
45
+ def consumer_head_tag_names
46
+ matrix_rows.collect(&:consumer_version_tag_name).compact
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :matrix_rows
52
+
53
+ def latest_verification_for_consumer_version_tag row
54
+ row.latest_verification_for_consumer_version_tag if row.consumer_version_tag_name
55
+ end
56
+
57
+ def overall_latest_verification
58
+ # not eager loaded because it shouldn't be called that often
59
+ first_row.latest_verification_for_consumer_and_provider
60
+ end
61
+
62
+ def first_row
63
+ @first_row
64
+ end
65
+
66
+ def consumer_version_tag_names
67
+ matrix_rows.collect(&:consumer_version_tag_name)
68
+ end
69
+ end
70
+ end
71
+ end