pact_broker 2.122.0 โ†’ 2.124.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/docs/CONFIGURATION.md +12 -0
  4. data/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +17 -1
  5. data/lib/pact_broker/api/contracts/utf_8_validation.rb +5 -1
  6. data/lib/pact_broker/api/decorators/pacts_for_verification_query_decorator.rb +1 -0
  7. data/lib/pact_broker/api/decorators/publish_contract_decorator.rb +4 -3
  8. data/lib/pact_broker/api/decorators/verification_decorator.rb +12 -2
  9. data/lib/pact_broker/api/decorators/verification_summary_decorator.rb +6 -0
  10. data/lib/pact_broker/api/resources/publish_contracts.rb +10 -6
  11. data/lib/pact_broker/config/runtime_configuration.rb +15 -0
  12. data/lib/pact_broker/doc/views/provider-pacts-for-verification.markdown +10 -2
  13. data/lib/pact_broker/domain/version.rb +9 -5
  14. data/lib/pact_broker/hash_refinements.rb +2 -2
  15. data/lib/pact_broker/locale/en.yml +4 -0
  16. data/lib/pact_broker/matrix/parse_query.rb +3 -1
  17. data/lib/pact_broker/matrix/service.rb +5 -0
  18. data/lib/pact_broker/pacts/generate_sha.rb +7 -4
  19. data/lib/pact_broker/pacts/pacts_for_verification_repository.rb +41 -21
  20. data/lib/pact_broker/pacts/repository.rb +1 -0
  21. data/lib/pact_broker/pacts/selected_pact.rb +4 -0
  22. data/lib/pact_broker/pacts/selector.rb +20 -2
  23. data/lib/pact_broker/pacts/selectors.rb +4 -0
  24. data/lib/pact_broker/ui/controllers/can_i_deploy.rb +1 -1
  25. data/lib/pact_broker/ui/controllers/matrix.rb +2 -2
  26. data/lib/pact_broker/verifications/repository.rb +1 -1
  27. data/lib/pact_broker/verifications/summary_for_consumer_version.rb +4 -0
  28. data/lib/pact_broker/version.rb +1 -1
  29. data/pact_broker.gemspec +1 -1
  30. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f515fab91233eb259d233ef8ff18b35014c55dfebb10a5cec183283bbe760d54
4
- data.tar.gz: 2f35f5f3f748d3df875df0ce5b8270f2b1e4b2cf8682804fda41def6d31b88a1
3
+ metadata.gz: 3a0262f7dd8bce1f0c350ce3ec9b2b6920e8e36ec672bc34c327cd24adfaaf6a
4
+ data.tar.gz: b3a306b5074d7370d6693f4c5d4219a0c4a069a304035226eff502d6bd9ed3f6
5
5
  SHA512:
6
- metadata.gz: 6eaefdf8cd5b2b9856bb076579bf034889b7fdab56cfe159c6e1ed56e74d72dca7929ca983290238e32f280e8d7e9b9bc53b9512b4d1562bde122de035e2f50b
7
- data.tar.gz: fe3c3008fa385b8585d7b2faa08e8196194e92fdfc496a813f0208373905abacdcded76e0eb4b8dc115b588b20be7e5cd32580ec78c4cbddfc7db15222936b73
6
+ metadata.gz: 6d363ce226b34c06e1cf6659d876c4699900598c6df64d9f5bc8c06a5a4b3d514431f4873307d71189fcfd5cd992f8caf582b26af92d2c5bc36cc61ad50a4a17
7
+ data.tar.gz: dcbce49bf421fb8f0294aba8bdf22be64d49e23f74a671531d9bf802717aefdbc58b154b4bfcc02a84e7fcd00709626e898a492f07c7cdd10e30b3d894398f38
data/CHANGELOG.md CHANGED
@@ -1,3 +1,56 @@
1
+ ## [2.124.0] _2026-11-11_
2
+
3
+ ### ๐Ÿš€ Features
4
+
5
+ - _(matrix)_ Add matrix_max_limit to cap the matrix query limit
6
+
7
+ ### ๐Ÿ› Bug Fixes
8
+
9
+ - _(matrix)_ Support an environment in a selector alongside a version or latest
10
+
11
+ ## [2.123.0] _2026-11-11_
12
+
13
+ ### ๐Ÿš€ Features
14
+
15
+ - Add fallbackToMainBranch consumer version selector option
16
+
17
+ ### ๐Ÿ› Bug Fixes
18
+
19
+ - Stop the consumer version verification summary querying per verification
20
+ - Apply fallbackTag per consumer
21
+ - Honour fallbackBranch in consumer version selectors
22
+
23
+ ### ๐Ÿšœ Refactor
24
+
25
+ - Name the fallback resolver for tags and branches
26
+
27
+ ### โšก๏ธ Performance
28
+
29
+ - Speed up publishing large contracts (PACT-7601) ([#1067](https://github.com/pact-foundation/pact_broker/issues/1067))
30
+ > **Migration note:** when contracts are published with /contracts/publish,
31
+ > the configured sha_generator is now called with a second argument,
32
+ - Exclude consumers in the fallback tag query
33
+ - Exclude consumers in the fallback branch query
34
+
35
+ ### ๐Ÿ“š Documentation
36
+
37
+ - Say fallbackTag applies per consumer
38
+ - Say fallbackBranch applies per consumer
39
+
40
+ ### ๐Ÿงช Testing
41
+
42
+ - Share one SQL query counter across feature specs
43
+ - Pin fallbackTag semantics in a table
44
+ - Cover fallbackTag dedup, branches and notices
45
+ - Pin fallbackBranch semantics in a table
46
+ - Cover the fallbackBranch request and notices end to end
47
+ - Pin fallbackToMainBranch semantics in the fallback branch table
48
+ - Cover the fallbackToMainBranch request and notices end to end
49
+
50
+ ### ๐Ÿ› ๏ธ Miscellaneous Tasks
51
+
52
+ - Drop a TODO the selector contract already resolves
53
+
1
54
  ## [2.122.0] _2026-10-01_
2
55
 
3
56
  ### ๐Ÿš€ Features
@@ -703,6 +703,18 @@ and at large numbers, the graph will not render due to browser performance issue
703
703
  **Default:** `150`<br/>
704
704
  **Allowed values:** A positive integer<br/>
705
705
 
706
+ ### matrix_max_limit
707
+
708
+ The maximum value accepted for the `limit` parameter of a matrix query (`/matrix`).
709
+ A request with a larger `limit` is rejected with a 400 response. Very large limits can cause expensive database queries.
710
+ When not set, there is no maximum. A matrix query without a `limit` uses a limit of 100,
711
+ so the value must be 100 or greater, or the Pact Broker will not start.
712
+
713
+ **Environment variable name:** `PACT_BROKER_MATRIX_MAX_LIMIT`<br/>
714
+ **YAML configuration key name:** `matrix_max_limit`<br/>
715
+ **Default:** No maximum<br/>
716
+ **Allowed values:** An integer of 100 or greater<br/>
717
+
706
718
  <br/>
707
719
 
708
720
  ## Miscellaneous
@@ -6,7 +6,7 @@ module PactBroker
6
6
  class ConsumerVersionSelectorContract < BaseContract
7
7
  option :parent # the parent hash in which the ConsumerVersionSelector is embedded
8
8
 
9
- BRANCH_KEYS = [:latest, :tag, :fallbackTag, :branch, :fallbackBranch, :matchingBranch, :mainBranch]
9
+ BRANCH_KEYS = [:latest, :tag, :fallbackTag, :branch, :fallbackBranch, :fallbackToMainBranch, :matchingBranch, :mainBranch]
10
10
  ENVIRONMENT_KEYS = [:environment, :deployed, :released, :deployedOrReleased]
11
11
  ALL_KEYS = BRANCH_KEYS + ENVIRONMENT_KEYS + [:consumer]
12
12
 
@@ -18,6 +18,7 @@ module PactBroker
18
18
  optional(:latest).filled(included_in?: [true, false])
19
19
  optional(:fallbackTag).filled(:str?)
20
20
  optional(:fallbackBranch).filled(:str?)
21
+ optional(:fallbackToMainBranch).filled(included_in?: [true])
21
22
  optional(:consumer).filled(:str?)
22
23
  optional(:deployed).filled(included_in?: [true])
23
24
  optional(:released).filled(included_in?: [true])
@@ -108,6 +109,21 @@ module PactBroker
108
109
  end
109
110
  end
110
111
 
112
+ # fallbackToMainBranch
113
+ rule(:fallbackToMainBranch, :fallbackBranch, :branch, :latest) do
114
+ if values[:fallbackToMainBranch] && !values[:branch]
115
+ base.failure(validation_message("pacts_for_verification_selector_fallback_to_main_branch_requires_branch"))
116
+ end
117
+
118
+ if values[:fallbackToMainBranch] && values[:fallbackBranch]
119
+ base.failure(validation_message("pacts_for_verification_selector_fallback_to_main_branch_and_fallback_branch_disallowed"))
120
+ end
121
+
122
+ if values[:fallbackToMainBranch] && values[:latest] == false
123
+ base.failure(validation_message("pacts_for_verification_selector_fallback_to_main_branch_and_latest_false_disallowed"))
124
+ end
125
+ end
126
+
111
127
  # branch/latest
112
128
  rule(:branch, :latest) do
113
129
  if values[:branch] && values[:latest] == false
@@ -5,7 +5,11 @@ module PactBroker
5
5
  extend self
6
6
 
7
7
  def fragment_before_invalid_utf_8_char(string)
8
- string.force_encoding("UTF-8").each_char.with_index do | char, index |
8
+ # Iterating over each char allocates a String per char, which is very slow for large strings,
9
+ # so only do it to find the location of the invalid char once we know there is one.
10
+ return nil if string.force_encoding("UTF-8").valid_encoding?
11
+
12
+ string.each_char.with_index do | char, index |
9
13
  if !char.valid_encoding?
10
14
  fragment = index < 100 ? string[0...index] : string[index-100...index]
11
15
  return index + 1, fragment
@@ -34,6 +34,7 @@ module PactBroker
34
34
  }
35
35
  property :fallback_tag
36
36
  property :fallback_branch
37
+ property :fallback_to_main_branch
37
38
  property :consumer
38
39
  property :environment_name, as: :environment
39
40
  property :currently_deployed, as: :deployed
@@ -11,10 +11,11 @@ module PactBroker
11
11
  property :provider_name
12
12
  property :specification
13
13
  property :content_type
14
- property :decoded_content, setter: -> (fragment:, represented:, user_options:, **) {
14
+ property :decoded_content, setter: -> (fragment:, represented:, user_options:, doc:, **) {
15
15
  represented.decoded_content = fragment
16
- # Set the pact version sha when we set the content
17
- represented.pact_version_sha = user_options.fetch(:sha_generator).call(fragment)
16
+ # Set the pact version sha when we set the content, reusing the content that has already been
17
+ # parsed in the resource, as parsing very large pacts is expensive
18
+ represented.pact_version_sha = user_options.fetch(:sha_generator).call(fragment, parsed_content: doc["decodedParsedContent"])
18
19
  }
19
20
  property :on_conflict, default: "overwrite"
20
21
 
@@ -24,7 +24,7 @@ module PactBroker
24
24
  end
25
25
 
26
26
  link :self do | options |
27
- pact = options[:pact] || represented.latest_pact_publication
27
+ pact = pact_for(options)
28
28
  {
29
29
  title: "Verification result",
30
30
  name: "Verification result #{represented.number} for #{pact.name}",
@@ -33,7 +33,7 @@ module PactBroker
33
33
  end
34
34
 
35
35
  link "pb:pact-version" do | options |
36
- pact = options[:pact] || represented.latest_pact_publication
36
+ pact = pact_for(options)
37
37
  {
38
38
  title: "Pact",
39
39
  name: pact.name,
@@ -47,6 +47,16 @@ module PactBroker
47
47
  href: verification_triggered_webhooks_url(represented, options.fetch(:base_url))
48
48
  }
49
49
  end
50
+
51
+ private
52
+
53
+ # A decorator rendering many verifications passes :pacts_by_pact_version_sha
54
+ # so each one avoids its own lookup.
55
+ def pact_for(options)
56
+ options[:pact] ||
57
+ options[:pacts_by_pact_version_sha]&.[](represented.pact_version_sha) ||
58
+ represented.latest_pact_publication
59
+ end
50
60
  end
51
61
  end
52
62
  end
@@ -25,6 +25,12 @@ module PactBroker
25
25
  }
26
26
  end
27
27
 
28
+ # representable passes through the kwargs from to_json as normal args
29
+ def to_hash(options)
30
+ user_options = options.fetch(:user_options, {}).merge(pacts_by_pact_version_sha: represented.pacts_by_pact_version_sha)
31
+ super(options.merge(user_options: user_options))
32
+ end
33
+
28
34
  def provider_summary
29
35
  OpenStruct.new(
30
36
  successful: represented.select(&:success).collect(&:provider_name),
@@ -56,16 +56,20 @@ module PactBroker
56
56
  @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params, { user_options: { sha_generator: PactBroker.configuration.sha_generator } } )
57
57
  end
58
58
 
59
+ # Memoized because decoding and parsing very large contracts is expensive,
60
+ # and this is called multiple times per request (validation, authorization and publication)
59
61
  def params
60
- p = super(default: {}, symbolize_names: false)
61
- if p["contracts"].is_a?(Array)
62
- p["contracts"].each do | contract |
63
- if contract.is_a?(Hash)
64
- decode_and_parse_content(contract)
62
+ @params_with_decoded_contracts ||= begin
63
+ p = super(default: {}, symbolize_names: false)
64
+ if p["contracts"].is_a?(Array)
65
+ p["contracts"].each do | contract |
66
+ if contract.is_a?(Hash)
67
+ decode_and_parse_content(contract)
68
+ end
65
69
  end
66
70
  end
71
+ p
67
72
  end
68
- p
69
73
  end
70
74
 
71
75
  def schema
@@ -20,6 +20,7 @@ require "pact_broker/config/runtime_configuration_basic_auth_methods"
20
20
  require "pact_broker/string_refinements"
21
21
  require "pact_broker/hash_refinements"
22
22
  require "pact_broker/error"
23
+ require "pact_broker/matrix/parse_query"
23
24
 
24
25
  module PactBroker
25
26
  module Config
@@ -97,9 +98,12 @@ module PactBroker
97
98
  semver_formats: ["%M.%m.%p%s%d", "%M.%m", "%M"],
98
99
  seed_example_data: true,
99
100
  network_diagram_max_pacticipants: 150,
101
+ matrix_max_limit: nil,
100
102
  features: {}
101
103
  )
102
104
 
105
+ on_load :validate_matrix_max_limit!
106
+
103
107
  def self.getter_and_setter_method_names
104
108
  extra_methods = [
105
109
  :warning_error_classes,
@@ -114,6 +118,7 @@ module PactBroker
114
118
  coerce_types(
115
119
  features: COERCE_FEATURES,
116
120
  network_diagram_max_pacticipants: :integer,
121
+ matrix_max_limit: :integer,
117
122
  webhook_certificates: COERCE_WEBHOOKS
118
123
  )
119
124
  sensitive_values(:database_url, :database_password)
@@ -226,6 +231,16 @@ module PactBroker
226
231
  end
227
232
  end
228
233
 
234
+ # A matrix query without a limit gets the default limit, so a lower matrix_max_limit
235
+ # would reject every request that does not set one.
236
+ # An empty or non-numeric value is coerced to 0, so it is rejected here too.
237
+ def validate_matrix_max_limit!
238
+ default_limit = PactBroker::Matrix::ParseQuery::DEFAULT_LIMIT
239
+ if matrix_max_limit && matrix_max_limit < default_limit
240
+ raise_validation_error("matrix_max_limit must be at least the default matrix limit (#{default_limit}), or not set")
241
+ end
242
+ end
243
+
229
244
  def raise_validation_error(msg)
230
245
  raise PactBroker::ConfigurationError, msg
231
246
  end
@@ -32,7 +32,15 @@ Example: This data structure represents the way a user might specify "I want to
32
32
 
33
33
  `consumerVersionSelectors.matchingBranch`: if the key is specified, can only be set to `true`. When true, returns the latest pact for any branch with the same name as the specified `providerVersionBranch`.
34
34
 
35
- `consumerVersionSelectors.fallbackBranch`: the name of the branch to fallback to if the specified `branch` does not exist. Use of this property is discouraged as it may allow a pact to pass on a feature branch while breaking backwards compatibility with the main branch, which is generally not desired. It is better to use two separate consumer version selectors, one with the main branch name, and one with the feature branch name, rather than use this property.
35
+ `consumerVersionSelectors.fallbackBranch`: the name of the branch to fall back to for each consumer that has no pact from the specified `branch`. Use of this property is discouraged as it may allow a pact to pass on a feature branch while breaking backwards compatibility with the main branch, which is generally not desired. It is better to use two separate consumer version selectors, one with the main branch name, and one with the feature branch name, rather than use this property.
36
+
37
+ `consumerVersionSelectors.fallbackToMainBranch`: if the key is specified, can only be set to `true`. Used with `branch`. For each consumer that has no pact for the specified `branch`, returns the latest pact from that consumer's configured `mainBranch` instead. Consumers without a configured `mainBranch` have no fallback. Use this instead of `fallbackBranch` when the consumers have different main branch names, or when a main branch may be renamed, so the provider does not need to know the name. It cannot be used together with `fallbackBranch`. The same caveat as `fallbackBranch` applies.
38
+
39
+ {
40
+ "consumerVersionSelectors": [
41
+ { "branch": "feat-x", "fallbackToMainBranch": true }
42
+ ]
43
+ }
36
44
 
37
45
  `consumerVersionSelectors.deployed`: if the key is specified, can only be set to `true`. Returns the pacts for all versions of the consumer that are currently deployed to any environment. Use of this selector requires that the deployment of the consumer application is recorded in the Pact Broker using the `pact-broker record-deployment` CLI.
38
46
 
@@ -48,7 +56,7 @@ Example: This data structure represents the way a user might specify "I want to
48
56
 
49
57
  `consumerVersionSelectors.tag`: the tag name(s) of the consumer versions to get the pacts for. *This field is still supported but it is recommended to use the `branch` in preference now.*
50
58
 
51
- `consumerVersionSelectors.fallbackTag`: the name of the tag to fallback to if the specified `tag` does not exist. This is useful when the consumer and provider use matching branch names to coordinate the development of new features. *This field is still supported but it is recommended to use two separate selectors - one with the main branch name and one with the feature branch name.*
59
+ `consumerVersionSelectors.fallbackTag`: the name of the tag to fall back to for each consumer that has no pact with the specified `tag`. This is useful when the consumer and provider use matching branch names to coordinate the development of new features. *This field is still supported but it is recommended to use two separate selectors - one with the main branch name and one with the feature branch name.*
52
60
 
53
61
  `providerVersionBranch`: the repository branch name for the provider application version that will be published with the verification results. This is used by the Broker to determine whether or not a particular pact is in pending state or not.
54
62
 
@@ -85,20 +85,24 @@ module PactBroker
85
85
  # end
86
86
  end
87
87
 
88
+ # Filters on a UNION of version ids so the versions dataset stays unwrapped for later criteria.
89
+ # An OR of two IN subqueries is equivalent, but cannot use the versions primary key index.
88
90
  def currently_in_environment(environment_name, pacticipant_name)
89
- currently_deployed_to_environment(environment_name, pacticipant_name).union(currently_supported_in_environment(environment_name, pacticipant_name))
91
+ version_ids = version_ids_currently_deployed_to_environment(environment_name, pacticipant_name)
92
+ .union(version_ids_currently_supported_in_environment(environment_name, pacticipant_name), from_self: false)
93
+ where(Sequel[:versions][:id] => version_ids)
90
94
  end
91
95
 
92
- def currently_deployed_to_environment(environment_name, pacticipant_name)
96
+ def version_ids_currently_deployed_to_environment(environment_name, pacticipant_name)
93
97
  deployed_version_query = PactBroker::Deployments::DeployedVersion.currently_deployed.for_environment_name(environment_name)
94
98
  deployed_version_query = deployed_version_query.for_pacticipant_name(pacticipant_name) if pacticipant_name
95
- where(id: deployed_version_query.select(:version_id))
99
+ deployed_version_query.select(:version_id)
96
100
  end
97
101
 
98
- def currently_supported_in_environment(environment_name, pacticipant_name)
102
+ def version_ids_currently_supported_in_environment(environment_name, pacticipant_name)
99
103
  supported_version_query = PactBroker::Deployments::ReleasedVersion.currently_supported.for_environment_name(environment_name)
100
104
  supported_version_query = supported_version_query.for_pacticipant_name(pacticipant_name) if pacticipant_name
101
- where(id: supported_version_query.select(:version_id))
105
+ supported_version_query.select(:version_id)
102
106
  end
103
107
 
104
108
  def currently_deployed
@@ -80,8 +80,8 @@ module PactBroker
80
80
  def symbolize_keys_private(params)
81
81
  case params
82
82
  when Hash
83
- params.inject({}) do |result, (key, value)|
84
- result.merge(key.to_sym => symbolize_keys_private(value))
83
+ params.each_with_object({}) do |(key, value), result|
84
+ result[key.to_sym] = symbolize_keys_private(value)
85
85
  end
86
86
  when Array
87
87
  params.collect { |value| symbolize_keys_private(value) }
@@ -97,6 +97,7 @@ en:
97
97
  cannot_set_currently_deployed_true: The currentlyDeployed property cannot be set back to true. Please record a new deployment.
98
98
  cannot_set_currently_supported_true: The currentlySupported property cannot be set back to true. Please record a new deployment.
99
99
  invalid_limit: The limit must be 1 or greater.
100
+ matrix_limit_exceeds_maximum: "The limit must be %{max_limit} or less."
100
101
  pacts_for_verification_selector_fallback_tag: "fallbackTag can only be set if latest is true"
101
102
  pacts_for_verification_selector_matching_branch_requires_provider_version_branch: the providerVersionBranch must be specified when the selector matchingBranch=true is used
102
103
  pacts_for_verification_selector_tag_and_branch_disallowed: "cannot specify both a tag and a branch"
@@ -105,6 +106,9 @@ en:
105
106
  pacts_for_verification_selector_branch_and_latest_false_disallowed: cannot specify a branch with latest=false
106
107
  pacts_for_verification_selector_main_branch_and_latest_false_disallowed: cannot specify mainBranch=true with latest=false
107
108
  pacts_for_verification_selector_fallback_branch_and_latest_false_disallowed: fallbackBranch can only be set if latest is true
109
+ pacts_for_verification_selector_fallback_to_main_branch_requires_branch: "a branch must be specified when fallbackToMainBranch=true is specified"
110
+ pacts_for_verification_selector_fallback_to_main_branch_and_fallback_branch_disallowed: cannot specify both fallbackBranch and fallbackToMainBranch=true
111
+ pacts_for_verification_selector_fallback_to_main_branch_and_latest_false_disallowed: fallbackToMainBranch can only be set if latest is true
108
112
  pacts_for_verification_selector_required_params_missing: must specify a value for environment or tag or branch, or specify mainBranch=true, matchingBranch=true, latest=true, deployed=true, released=true or deployedOrReleased=true
109
113
  pacts_for_verification_selector_main_branch_with_other_param_disallowed: cannot specify mainBranch=true with any other criteria apart from consumer
110
114
  pacts_for_verification_selector_matching_branch_with_other_param_disallowed: cannot specify matchingBranch=true with any other criteria apart from consumer
@@ -4,6 +4,8 @@ require "pact_broker/matrix/unresolved_selector"
4
4
  module PactBroker
5
5
  module Matrix
6
6
  class ParseQuery
7
+ DEFAULT_LIMIT = 100
8
+
7
9
  # rubocop: disable Metrics/CyclomaticComplexity
8
10
  # rubocop: disable Metrics/MethodLength
9
11
  def self.call query
@@ -34,7 +36,7 @@ module PactBroker
34
36
  if params.key?("limit") && params["limit"] != ""
35
37
  options[:limit] = params["limit"]
36
38
  else
37
- options[:limit] = "100"
39
+ options[:limit] = DEFAULT_LIMIT.to_s
38
40
  end
39
41
 
40
42
  if params.key?("latest") && params["latest"] != ""
@@ -140,6 +140,11 @@ module PactBroker
140
140
  error_messages << message("errors.validation.invalid_limit")
141
141
  end
142
142
 
143
+ max_limit = PactBroker.configuration.matrix_max_limit
144
+ if max_limit && options[:limit] && options[:limit].to_i > max_limit
145
+ error_messages << message("errors.validation.matrix_limit_exceeds_maximum", max_limit: max_limit)
146
+ end
147
+
143
148
  error_messages
144
149
  end
145
150
  # rubocop: enable Metrics/CyclomaticComplexity
@@ -11,17 +11,20 @@ module PactBroker
11
11
  include PactBroker::Logging
12
12
 
13
13
  # @param [String] json_content
14
- def self.call(json_content, _options = {})
14
+ # @param [Hash] options
15
+ # @option options [Hash] :parsed_content the result of parsing the json_content, if it has already been parsed.
16
+ # Used to avoid parsing very large pacts again. Must be the parsed json_content, or the sha will be wrong.
17
+ def self.call(json_content, options = {})
15
18
  content_for_sha = if PactBroker.configuration.base_equality_only_on_content_that_affects_verification_results
16
- extract_verifiable_content_for_sha(json_content)
19
+ extract_verifiable_content_for_sha(json_content, options[:parsed_content])
17
20
  else
18
21
  json_content
19
22
  end
20
23
  measure_info("Generating SHA1 hexdigest for pact", payload: { length: content_for_sha.length } ){ Digest::SHA1.hexdigest(content_for_sha) }
21
24
  end
22
25
 
23
- def self.extract_verifiable_content_for_sha(json_content)
24
- objects = Content.from_json(json_content)
26
+ def self.extract_verifiable_content_for_sha(json_content, parsed_content = nil)
27
+ objects = parsed_content.is_a?(Hash) ? Content.from_hash(parsed_content) : Content.from_json(json_content)
25
28
  sorted_content = measure_info("Sorting content", payload: { length: json_content.length }){ objects.sort }
26
29
  sorted_content.content_that_affects_verification_results.to_json
27
30
  end
@@ -28,7 +28,9 @@ module PactBroker
28
28
  # @return [VerifiablePact] an array of VerifiablePact objects
29
29
  def find(provider_name, consumer_version_selectors)
30
30
  selected_pacts = find_pacts_by_selector(provider_name, consumer_version_selectors)
31
- selected_pacts = selected_pacts + find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors)
31
+ selected_pacts = selected_pacts +
32
+ find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) +
33
+ find_pacts_for_fallback_branches(selected_pacts, provider_name, consumer_version_selectors)
32
34
  merge_selected_pacts(selected_pacts)
33
35
  end
34
36
 
@@ -178,22 +180,32 @@ module PactBroker
178
180
  end
179
181
  end
180
182
 
183
+ # The fallback is applied per consumer, so a consumer without a pact for the tag
184
+ # still gets its fallback pact when another consumer does have a pact for the tag.
181
185
  def find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors)
182
- # TODO at the moment, the validation doesn't stop fallback being used with 'all' selectors
183
- selectors_with_fallback_tags = consumer_version_selectors.select(&:fallback_tag?)
184
- selectors_missing_a_pact = selectors_with_fallback_tags.reject do | selector |
185
- selected_pacts.any? do | selected_pact |
186
- selected_pact.latest_for_tag?(selector.tag)
186
+ consumer_version_selectors.select(&:fallback_tag?).flat_map do | selector |
187
+ consumer_ids_with_a_pact_for_the_tag = consumer_ids_of(selected_pacts.select { | selected_pact | selected_pact.latest_for_tag?(selector.tag) })
188
+ find_fallback_pacts(provider_name, selector, consumer_ids_with_a_pact_for_the_tag) do | query |
189
+ query.for_latest_consumer_versions_with_tag(selector.fallback_tag)
187
190
  end
188
191
  end
192
+ end
189
193
 
190
- if selectors_missing_a_pact.any?
191
- find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors_missing_a_pact)
192
- else
193
- []
194
+ # The fallback is applied per consumer, so a consumer without a pact for the branch
195
+ # still gets its fallback pact when another consumer does have a pact for the branch.
196
+ def find_pacts_for_fallback_branches(selected_pacts, provider_name, consumer_version_selectors)
197
+ consumer_version_selectors.select { | selector | selector.fallback_branch? || selector.fallback_to_main_branch? }.flat_map do | selector |
198
+ consumer_ids_with_a_pact_for_the_branch = consumer_ids_of(selected_pacts.select { | selected_pact | selected_pact.latest_for_branch?(selector.branch) })
199
+ find_fallback_pacts(provider_name, selector, consumer_ids_with_a_pact_for_the_branch) do | query |
200
+ selector.fallback_to_main_branch? ? query.latest_for_main_branches : query.latest_for_consumer_branch(selector.fallback_branch)
201
+ end
194
202
  end
195
203
  end
196
204
 
205
+ def consumer_ids_of(selected_pacts)
206
+ selected_pacts.collect { | selected_pact | selected_pact.pact.consumer.id }.uniq
207
+ end
208
+
197
209
  def find_pacts_by_selector(provider_name, consumer_version_selectors)
198
210
  provider = pacticipant_repository.find_by_name(provider_name)
199
211
 
@@ -248,17 +260,25 @@ module PactBroker
248
260
  selectors
249
261
  end
250
262
 
251
- def find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors)
252
- selectors.collect do | selector |
253
- query = scope_for(PactPublication).eager_for_domain_with_content.for_provider_name(provider_name).for_latest_consumer_versions_with_tag(selector.fallback_tag)
254
- query = query.for_consumer_name(selector.consumer) if selector.consumer
255
- query.all.collect do | pact_publication |
256
- SelectedPact.new(
257
- pact_publication.to_domain,
258
- Selectors.new(selector.resolve_for_fallback(pact_publication.consumer_version))
259
- )
260
- end
261
- end.flatten
263
+ # Yields the provider's pacts so the caller can narrow them to the latest for the fallback.
264
+ def find_fallback_pacts(provider_name, selector, consumer_ids_to_exclude)
265
+ query = yield scope_for(PactPublication).eager_for_domain_with_content.for_provider_name(provider_name)
266
+ query = query.for_consumer_name(selector.consumer) if selector.consumer
267
+ query = query.exclude(Sequel[:pact_publications][:consumer_id] => consumer_ids_to_exclude) if consumer_ids_to_exclude.any?
268
+ query.all.collect do | pact_publication |
269
+ SelectedPact.new(
270
+ pact_publication.to_domain,
271
+ Selectors.new(resolve_selector_for_fallback(selector, pact_publication))
272
+ )
273
+ end
274
+ end
275
+
276
+ def resolve_selector_for_fallback(selector, pact_publication)
277
+ if selector.fallback_to_main_branch?
278
+ selector.resolve_for_fallback_to_main_branch(pact_publication.consumer_version, pact_publication.consumer.main_branch)
279
+ else
280
+ selector.resolve_for_fallback(pact_publication.consumer_version)
281
+ end
262
282
  end
263
283
 
264
284
  def find_provider_tags_for_which_pact_publication_id_is_pending(pact_publication, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
@@ -202,6 +202,7 @@ module PactBroker
202
202
  def find_by_consumer_version consumer_name, consumer_version_number
203
203
  scope_for(PactPublication)
204
204
  .eager_for_domain_with_content
205
+ .eager(pact_version: :latest_verification)
205
206
  .for_consumer_name_and_maybe_version_number(consumer_name, consumer_version_number)
206
207
  .remove_overridden_revisions_from_complete_query
207
208
  .all
@@ -57,6 +57,10 @@ module PactBroker
57
57
  selectors.latest_for_tag?(potential_tag)
58
58
  end
59
59
 
60
+ def latest_for_branch? potential_branch = nil
61
+ selectors.latest_for_branch?(potential_branch)
62
+ end
63
+
60
64
  def consumer_version_order
61
65
  pact.consumer_version.order
62
66
  end
@@ -6,7 +6,7 @@ module PactBroker
6
6
  class Selector < Hash
7
7
  using PactBroker::HashRefinements
8
8
 
9
- PROPERTY_NAMES = [:latest, :tag, :branch, :consumer, :consumer_version, :environment_name, :fallback_tag, :fallback_branch, :main_branch, :matching_branch, :currently_supported, :currently_deployed]
9
+ PROPERTY_NAMES = [:latest, :tag, :branch, :consumer, :consumer_version, :environment_name, :fallback_tag, :fallback_branch, :fallback_to_main_branch, :main_branch, :matching_branch, :currently_supported, :currently_deployed]
10
10
 
11
11
  def initialize(properties = {})
12
12
  properties.without(*PROPERTY_NAMES).tap { |it| warn("WARN: Unsupported property for #{self.class.name}: #{it.keys.join(", ")} at #{caller[0..3]}") if it.any? }
@@ -14,13 +14,19 @@ module PactBroker
14
14
  end
15
15
 
16
16
  def resolve(consumer_version)
17
- ResolvedSelector.new(self.to_h.without(:fallback_tag, :fallback_branch), consumer_version)
17
+ ResolvedSelector.new(self.to_h.without(:fallback_tag, :fallback_branch, :fallback_to_main_branch), consumer_version)
18
18
  end
19
19
 
20
20
  def resolve_for_fallback(consumer_version)
21
21
  ResolvedSelector.new(self.to_h, consumer_version)
22
22
  end
23
23
 
24
+ # Records the consumer's main branch as the fallback branch, so the verification
25
+ # notice names the branch that was actually used.
26
+ def resolve_for_fallback_to_main_branch(consumer_version, main_branch)
27
+ ResolvedSelector.new(self.to_h.without(:fallback_to_main_branch).merge(fallback_branch: main_branch), consumer_version)
28
+ end
29
+
24
30
  def resolve_for_environment(consumer_version, environment, target = nil)
25
31
  ResolvedSelector.new(self.to_h.merge({ environment: environment, target: target }.compact), consumer_version)
26
32
  end
@@ -87,6 +93,10 @@ module PactBroker
87
93
  self[:fallback_branch] = fallback_branch
88
94
  end
89
95
 
96
+ def fallback_to_main_branch= fallback_to_main_branch
97
+ self[:fallback_to_main_branch] = fallback_to_main_branch
98
+ end
99
+
90
100
  def fallback_tag
91
101
  self[:fallback_tag]
92
102
  end
@@ -95,6 +105,10 @@ module PactBroker
95
105
  self[:fallback_branch]
96
106
  end
97
107
 
108
+ def fallback_to_main_branch
109
+ self[:fallback_to_main_branch]
110
+ end
111
+
98
112
  def consumer= consumer
99
113
  self[:consumer] = consumer
100
114
  end
@@ -235,6 +249,10 @@ module PactBroker
235
249
  !!fallback_branch
236
250
  end
237
251
 
252
+ def fallback_to_main_branch?
253
+ !!fallback_to_main_branch
254
+ end
255
+
238
256
  def main_branch
239
257
  self[:main_branch]
240
258
  end
@@ -59,6 +59,10 @@ module PactBroker
59
59
  any? { | selector | selector.latest_for_tag?(potential_tag) }
60
60
  end
61
61
 
62
+ def latest_for_branch? potential_branch = nil
63
+ any? { | selector | selector.latest_for_branch?(potential_branch) }
64
+ end
65
+
62
66
  def tag_names_of_selectors_for_all_pacts
63
67
  select(&:all_for_tag?).collect(&:tag).uniq
64
68
  end
@@ -18,7 +18,7 @@ module PactBroker
18
18
  get "/:pacticipant_name/latest-version/:tag/can-i-deploy/to/:environment_tag" do
19
19
  # selector and options must be in sync with lib/pact_broker/api/resources/can_i_deploy_pacticipant_version_by_tag_to_tag_badge.rb
20
20
  selectors = [ PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: params[:pacticipant_name], latest: true, tag: params[:tag]) ]
21
- options = { latestby: "cvp", limit: 100, tag: params[:to] }
21
+ options = { latestby: "cvp", limit: PactBroker::Matrix::ParseQuery::DEFAULT_LIMIT, tag: params[:to] }
22
22
  result = matrix_service.find(selectors, options)
23
23
  lines = PactBroker::UI::ViewModels::MatrixLines.new(result, base_url: base_url)
24
24
  locals = {
@@ -17,7 +17,7 @@ module PactBroker
17
17
 
18
18
  get "/" do
19
19
  selectors = [OpenStruct.new, OpenStruct.new]
20
- options = { limit: 100, latestby: nil }
20
+ options = { limit: PactBroker::Matrix::ParseQuery::DEFAULT_LIMIT, latestby: nil }
21
21
  locals = {
22
22
  lines: [],
23
23
  selectors: create_selector_objects(selectors),
@@ -50,7 +50,7 @@ module PactBroker
50
50
  PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: params[:consumer_name]),
51
51
  PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: params[:provider_name])
52
52
  ]
53
- options = { latestby: "cvpv", limit: 100 }
53
+ options = { latestby: "cvpv", limit: PactBroker::Matrix::ParseQuery::DEFAULT_LIMIT }
54
54
  lines = matrix_service.find(selectors, options)
55
55
  lines = PactBroker::UI::ViewModels::MatrixLines.new(lines, base_url: base_url)
56
56
  locals = {
@@ -99,7 +99,7 @@ module PactBroker
99
99
  .remove_verifications_for_overridden_consumer_versions
100
100
  .for_consumer_name_and_consumer_version_number(consumer_name, consumer_version_number)
101
101
  .latest_by_pact_version
102
- .eager(:provider)
102
+ .eager(:provider, :consumer, :provider_version, :pact_version)
103
103
  .all
104
104
  .sort_by { | v | v.provider_name.downcase }
105
105
  end
@@ -13,6 +13,10 @@ module PactBroker
13
13
  successful.count == pacts.count
14
14
  end
15
15
 
16
+ def pacts_by_pact_version_sha
17
+ pacts.to_h { | pact | [pact.pact_version_sha, pact] }
18
+ end
19
+
16
20
  def provider_summary
17
21
  OpenStruct.new(
18
22
  successful: successful,
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = "2.122.0"
2
+ VERSION = "2.124.0"
3
3
  end
data/pact_broker.gemspec CHANGED
@@ -64,7 +64,7 @@ Gem::Specification.new do |gem|
64
64
  gem.add_runtime_dependency "rack-protection", "~> 4.1"
65
65
  gem.add_runtime_dependency "table_print", "~> 1.5"
66
66
  gem.add_runtime_dependency "semantic_logger", "~> 5.0"
67
- gem.add_runtime_dependency "sanitize", "~> 6.0"
67
+ gem.add_runtime_dependency "sanitize", "~> 7.0"
68
68
  gem.add_runtime_dependency "wisper", "~> 2.0"
69
69
  gem.add_runtime_dependency "anyway_config", "~> 2.1"
70
70
  gem.add_runtime_dependency "request_store", "~> 1.5"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pact_broker
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.122.0
4
+ version: 2.124.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bethany Skurrie
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2026-10-01 00:00:00.000000000 Z
13
+ date: 2026-11-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: json
@@ -228,14 +228,14 @@ dependencies:
228
228
  requirements:
229
229
  - - "~>"
230
230
  - !ruby/object:Gem::Version
231
- version: '6.0'
231
+ version: '7.0'
232
232
  type: :runtime
233
233
  prerelease: false
234
234
  version_requirements: !ruby/object:Gem::Requirement
235
235
  requirements:
236
236
  - - "~>"
237
237
  - !ruby/object:Gem::Version
238
- version: '6.0'
238
+ version: '7.0'
239
239
  - !ruby/object:Gem::Dependency
240
240
  name: wisper
241
241
  requirement: !ruby/object:Gem::Requirement