pact_broker 2.120.0 → 2.121.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +552 -482
  3. data/Gemfile +0 -6
  4. data/README.md +0 -1
  5. data/docs/CONFIGURATION.md +29 -2
  6. data/docs/matrix-optimisation/README.md +85 -0
  7. data/lib/pact/doc/interaction_view_model.rb +2 -1
  8. data/lib/pact/doc/markdown/consumer_contract_renderer.rb +3 -1
  9. data/lib/pact/doc.rb +5 -0
  10. data/lib/pact_broker/api/contracts/put_pact_params_contract.rb +2 -0
  11. data/lib/pact_broker/api/decorators/provider_states_decorator.rb +1 -1
  12. data/lib/pact_broker/api/decorators/version_decorator.rb +7 -2
  13. data/lib/pact_broker/api/pact_broker_urls.rb +4 -0
  14. data/lib/pact_broker/api/resources/deployed_or_released_versions.rb +78 -0
  15. data/lib/pact_broker/api/resources/environment_versions.rb +89 -0
  16. data/lib/pact_broker/api/resources/tag_versions.rb +1 -1
  17. data/lib/pact_broker/api.rb +2 -0
  18. data/lib/pact_broker/app.rb +0 -1
  19. data/lib/pact_broker/config/runtime_configuration.rb +19 -0
  20. data/lib/pact_broker/configuration.rb +15 -2
  21. data/lib/pact_broker/dataset.rb +4 -23
  22. data/lib/pact_broker/db/clean_incremental.rb +4 -8
  23. data/lib/pact_broker/db.rb +0 -7
  24. data/lib/pact_broker/domain/version.rb +6 -2
  25. data/lib/pact_broker/initializers/database_connection.rb +5 -0
  26. data/lib/pact_broker/integrations/repository.rb +1 -1
  27. data/lib/pact_broker/integrations/service.rb +1 -3
  28. data/lib/pact_broker/logging/open_telemetry_appender_setup.rb +73 -0
  29. data/lib/pact_broker/logging/trace_aware_json_formatter.rb +38 -0
  30. data/lib/pact_broker/matrix/matrix_row_dataset_module.rb +29 -11
  31. data/lib/pact_broker/matrix/matrix_row_verification_dataset_module.rb +23 -12
  32. data/lib/pact_broker/matrix/selector_resolver.rb +30 -2
  33. data/lib/pact_broker/pacticipants/repository.rb +4 -1
  34. data/lib/pact_broker/pacts/service.rb +2 -2
  35. data/lib/pact_broker/tags/service.rb +4 -5
  36. data/lib/pact_broker/verifications/repository.rb +1 -1
  37. data/lib/pact_broker/version.rb +1 -1
  38. data/lib/pact_broker/versions/branch_version_repository.rb +3 -3
  39. data/lib/pact_broker/versions/repository.rb +4 -13
  40. data/lib/pact_broker/webhooks/render.rb +1 -1
  41. data/lib/sequel/plugins/upsert.rb +1 -8
  42. data/pact_broker.gemspec +2 -2
  43. metadata +15 -6
@@ -12,6 +12,7 @@ module PactBroker
12
12
 
13
13
  plugin :timestamps, update_on_create: true
14
14
  plugin :age
15
+ plugin :insert_ignore, identifying_columns: [:pacticipant_id, :number]
15
16
  plugin :upsert, { identifying_columns: [:pacticipant_id, :number], ignore_columns_on_update: [:id, :created_at, :order] }
16
17
 
17
18
  one_to_many :pact_publications, order: :revision_number, class: "PactBroker::Pacts::PactPublication", key: :consumer_version_id
@@ -241,9 +242,11 @@ module PactBroker
241
242
  # @return Sequel::Dataset<PactBroker::Domain::Version>
242
243
  def ids_for_selectors(unresolved_selectors)
243
244
  # Need the select at the start and at the end to stop extra columns being returned (eg. branch name, environment name)
245
+ # from_self: false keeps this a single flat UNION. Sequel's default would wrap the
246
+ # accumulated dataset in a new subquery for every selector, nesting it N-1 deep.
244
247
  unresolved_selectors
245
248
  .collect{ |selector| self.select(Sequel[:versions][:id]).for_selector(selector).select(:id) }
246
- .reduce(&:union)
249
+ .reduce { |all_ids, ids| all_ids.union(ids, from_self: false) }
247
250
  end
248
251
 
249
252
  def pacticipants_set
@@ -272,10 +275,11 @@ module PactBroker
272
275
  end
273
276
 
274
277
  # Isn't called on upsert when the record is updated with Sqlite
275
- # Is called with Postgres/MySQL
278
+ # Is called with Postgres
276
279
  # Haven't had time to dig into why
277
280
  def after_create
278
281
  super
282
+ return unless id
279
283
  OrderVersions.(self) unless self.order
280
284
  refresh
281
285
  end
@@ -1,9 +1,14 @@
1
1
  require "sequel"
2
2
  require "pact_broker/db/log_quietener"
3
3
  require "fileutils"
4
+ require "pact_broker/error"
4
5
 
5
6
  module PactBroker
6
7
  def self.create_database_connection(config, logger = nil)
8
+ if config[:adapter].to_s =~ /mysql/
9
+ raise PactBroker::Error.new("MySQL is no longer supported. Please use PostgreSQL.")
10
+ end
11
+
7
12
  logger&.info("Connecting to database:", config.merge(password: "*****"))
8
13
 
9
14
  sequel_config = config.dup
@@ -74,7 +74,7 @@ module PactBroker
74
74
  def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider)
75
75
  consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | { consumer_id: object.consumer&.id, provider_id: object.provider.id }.compact }.uniq
76
76
 
77
- # MySQL doesn't support an UPDATE with a subquery. FFS. Really need to do a major version release and delete the support code.
77
+ # Not all supported databases handle an UPDATE with a subquery, so branch on SKIP LOCKED support.
78
78
  criteria = if Integration.dataset.supports_skip_locked?
79
79
  integration_ids_to_update = Integration
80
80
  .select(:id)
@@ -55,9 +55,7 @@ module PactBroker
55
55
  model.truncate(cascade: true)
56
56
  else
57
57
  logger.info("Deleting all from ", model.table_name)
58
- # Mysql adapter needs to support cascade truncate
59
- # https://travis-ci.org/pact-foundation/pact_broker/jobs/633050220#L841
60
- # https://travis-ci.org/pact-foundation/pact_broker/jobs/633053228#L849
58
+ # SQLite does not support cascade truncate, so delete instead
61
59
  model.dataset.delete
62
60
  end
63
61
  end
@@ -0,0 +1,73 @@
1
+ require "semantic_logger"
2
+ require "pact_broker/error"
3
+
4
+ module PactBroker
5
+ module Logging
6
+ # Adds semantic_logger's :open_telemetry appender when configured, without
7
+ # taking a hard dependency on the OpenTelemetry gems.
8
+ class OpenTelemetryAppenderSetup
9
+ def self.call(runtime_configuration)
10
+ new(runtime_configuration).call
11
+ end
12
+
13
+ def initialize(runtime_configuration)
14
+ @runtime_configuration = runtime_configuration
15
+ end
16
+
17
+ # Returns the added (or already-present) appender, or nil if none was added.
18
+ def call
19
+ case @runtime_configuration.log_otel_enabled
20
+ when false
21
+ nil
22
+ when true
23
+ unless otel_available?
24
+ raise PactBroker::ConfigurationError,
25
+ "log_otel_enabled is set to true but the opentelemetry-logs-sdk gem could not be loaded. " \
26
+ "Add it to your Gemfile, or set log_otel_enabled to auto/false."
27
+ end
28
+ add_appender(warn_if_no_provider: true)
29
+ else # :auto
30
+ add_appender(warn_if_no_provider: false) if otel_available? && otel_provider_configured?
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def add_appender(warn_if_no_provider:)
37
+ existing = existing_otel_appender
38
+ return existing if existing
39
+
40
+ if warn_if_no_provider && !otel_provider_configured?
41
+ SemanticLogger["pact-broker"].warn(
42
+ "log_otel_enabled is true but no OpenTelemetry LoggerProvider is configured; " \
43
+ "OTel log records may be dropped."
44
+ )
45
+ end
46
+
47
+ SemanticLogger.add_appender(appender: :open_telemetry)
48
+ end
49
+
50
+ def existing_otel_appender
51
+ return nil unless defined?(SemanticLogger::Appender::OpenTelemetry)
52
+
53
+ SemanticLogger.appenders.find { |appender| appender.is_a?(SemanticLogger::Appender::OpenTelemetry) }
54
+ end
55
+
56
+ def otel_available?
57
+ require "opentelemetry-logs-sdk"
58
+ defined?(OpenTelemetry::Logs) ? true : false
59
+ rescue LoadError
60
+ false
61
+ end
62
+
63
+ def otel_provider_configured?
64
+ return false unless defined?(OpenTelemetry)
65
+
66
+ # A no-op / proxy provider means no real logs pipeline is configured.
67
+ OpenTelemetry.logger_provider.class.name.to_s.start_with?("OpenTelemetry::SDK::Logs")
68
+ rescue StandardError
69
+ false
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,38 @@
1
+ require "semantic_logger"
2
+ require "semantic_logger/formatters/json"
3
+
4
+ module PactBroker
5
+ module Logging
6
+ # A JSON log formatter that adds the standard OpenTelemetry log-correlation
7
+ # fields (trace_id, span_id, trace_flags) when a valid span is active.
8
+ # Degrades to plain JSON when OTel is not loaded or there is no active span,
9
+ # so it is safe for the open-source Pact Broker.
10
+ class TraceAwareJsonFormatter < SemanticLogger::Formatters::Json
11
+ def call(log, logger)
12
+ json = super
13
+ context = otel_trace_context
14
+ return json if context.empty?
15
+
16
+ # `hash` was populated by Raw#call during `super`; reuse it.
17
+ SemanticLogger::Utils.to_json(hash.merge(context))
18
+ end
19
+
20
+ private
21
+
22
+ def otel_trace_context
23
+ return {} unless defined?(OpenTelemetry::Trace)
24
+
25
+ span_context = OpenTelemetry::Trace.current_span.context
26
+ return {} unless span_context.valid?
27
+
28
+ {
29
+ trace_id: span_context.hex_trace_id,
30
+ span_id: span_context.hex_span_id,
31
+ trace_flags: span_context.trace_flags.sampled? ? "01" : "00"
32
+ }
33
+ rescue StandardError
34
+ {}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -61,8 +61,13 @@ module PactBroker
61
61
  end
62
62
 
63
63
  # consumer
64
- pact_publication_matching_consumer = select_pact_columns_with_aliases.most_recent(limit).from_self(alias: :p).inner_join_versions_for_selectors_as_consumer(resolved_selectors)
65
- rows_where_selector_matches_consumer = pact_publication_matching_consumer.left_outer_join_verifications.select_all_columns_after_join
64
+ pact_publication_matching_consumer = select_pact_columns_with_aliases
65
+ .most_recent(limit)
66
+ .from_self(alias: :p)
67
+ .where_consumer_version_matches_selectors(resolved_selectors)
68
+ rows_where_selector_matches_consumer = pact_publication_matching_consumer
69
+ .left_outer_join_verifications
70
+ .select_all_columns_after_join
66
71
 
67
72
  # provider
68
73
  verifications_matching_provider = verification_dataset.matching_selectors_as_provider_for_any_consumer(resolved_selectors)
@@ -143,24 +148,37 @@ module PactBroker
143
148
 
144
149
  if resolved_selectors_with_versions_specified.any?
145
150
  select_pact_columns_with_aliases
146
- .inner_join_versions_for_selectors_as_consumer(resolved_selectors_with_versions_specified)
151
+ .where_consumer_version_matches_selectors(resolved_selectors_with_versions_specified)
147
152
  .where(provider_id: all_pacticipant_ids)
148
153
  .most_recent(limit)
149
154
  end
150
155
  end
151
156
 
157
+ # Name-only selectors carry no version ID — matching_one_selector_for_either_consumer_or_provider
158
+ # passes its selectors unfiltered — so they filter on the pacticipant instead. That
159
+ # identifies the same rows, because a pact publication's consumer_id is the
160
+ # pacticipant_id of its consumer_version.
152
161
  # @private
153
- def inner_join_versions_for_selectors_as_consumer(resolved_selectors)
154
- # get the UnresolvedSelector objects back out of the resolved_selectors because the Version.for_selector() method uses the UnresolvedSelector
155
- unresolved_selectors = resolved_selectors.collect(&:original_selector).uniq
156
- versions = PactBroker::Domain::Version.ids_for_selectors(unresolved_selectors)
157
- inner_join_versions_dataset(versions)
162
+ # @param [Array<PactBroker::Matrix::ResolvedSelector>] resolved_selectors
163
+ def where_consumer_version_matches_selectors(resolved_selectors)
164
+ if resolved_selectors.empty?
165
+ raise ArgumentError.new("resolved_selectors must not be empty")
166
+ end
167
+
168
+ criteria = consumer_criteria_for_selectors(resolved_selectors)
169
+ where(criteria.size > 1 ? Sequel.|(*criteria) : criteria.first)
158
170
  end
159
171
 
160
172
  # @private
161
- def inner_join_versions_dataset(versions)
162
- versions_join = { Sequel[:p][:consumer_version_id] => Sequel[:versions][:id] }
163
- join(versions, versions_join, table_alias: :versions)
173
+ # @param [Array<PactBroker::Matrix::ResolvedSelector>] resolved_selectors
174
+ # @return [Array<Hash>]
175
+ def consumer_criteria_for_selectors(resolved_selectors)
176
+ name_only, with_versions = resolved_selectors.partition(&:only_pacticipant_name_specified?)
177
+
178
+ criteria = []
179
+ criteria << { Sequel[:p][:consumer_id] => name_only.collect(&:pacticipant_id).uniq } if name_only.any?
180
+ criteria << { Sequel[:p][:consumer_version_id] => with_versions.collect(&:pacticipant_version_id).uniq } if with_versions.any?
181
+ criteria
164
182
  end
165
183
 
166
184
  # @private
@@ -21,7 +21,7 @@ module PactBroker
21
21
  # @return [Sequel::Dataset<Verification>, nil]
22
22
  def matching_selectors_as_provider_for_any_consumer(resolved_selectors)
23
23
  select_verification_columns_with_aliases
24
- .inner_join_versions_for_selectors_as_provider(resolved_selectors)
24
+ .where_provider_version_matches_selectors(resolved_selectors)
25
25
  end
26
26
 
27
27
  # Return verifications where the provider is described by any of the resolved_selectors *that only specify the pacticipant NAME*,
@@ -51,32 +51,43 @@ module PactBroker
51
51
  # @param [Array<PactBroker::Matrix::ResolvedSelector>] resolved_selectors
52
52
  # @return [Sequel::Dataset<Verification>, nil]
53
53
  def matching_only_selectors_as_provider_where_not_only_pacticipant_name_in_selector(resolved_selectors)
54
- # get the UnresolvedSelector objects back out of the resolved_selectors because the Version.for_selector() method uses the UnresolvedSelector
55
54
  all_pacticipant_ids = resolved_selectors.collect(&:pacticipant_id).uniq
56
55
  resolved_selectors_with_versions_specified = resolved_selectors.reject(&:only_pacticipant_name_specified?)
57
56
 
58
57
  if resolved_selectors_with_versions_specified.any?
59
58
  select_verification_columns_with_aliases
60
- .inner_join_versions_for_selectors_as_provider(resolved_selectors_with_versions_specified)
59
+ .where_provider_version_matches_selectors(resolved_selectors_with_versions_specified)
61
60
  .where(consumer_id: all_pacticipant_ids)
62
61
  end
63
62
  end
64
63
 
65
- # Don't think it's worth splitting this into 2 different queries for selectors with only pacticipant name/selectors with version properties,
66
- # as it's unlikely for there ever to be a query through the UI or CLI that results in 1 selector which only has a pacticipant name in it.
64
+ # Name-only selectors carry no version id — matching_selectors_as_provider_for_any_consumer
65
+ # passes its selectors unfiltered — so they filter on the provider pacticipant instead. That
66
+ # identifies the same rows, because a verification's provider_id is the
67
+ # pacticipant_id of its provider_version.
67
68
  # @private
68
69
  # @param [Array<PactBroker::Matrix::ResolvedSelector>] resolved_selectors
69
70
  # @return [Sequel::Dataset<Verification>]
70
- def inner_join_versions_for_selectors_as_provider(resolved_selectors)
71
- # get the UnresolvedSelector objects back out of the resolved_selectors because the Version.for_selector() method uses the UnresolvedSelector
72
- unresolved_selectors = resolved_selectors.collect(&:original_selector).uniq
73
- versions = PactBroker::Domain::Version.ids_for_selectors(unresolved_selectors)
74
- join_versions_dataset(versions)
71
+ def where_provider_version_matches_selectors(resolved_selectors)
72
+ if resolved_selectors.empty?
73
+ raise ArgumentError.new("resolved_selectors must not be empty")
74
+ end
75
+
76
+ criteria = provider_criteria_for_selectors(resolved_selectors)
77
+ where(criteria.size > 1 ? Sequel.|(*criteria) : criteria.first)
75
78
  end
76
79
 
77
80
  # @private
78
- def join_versions_dataset(versions_dataset)
79
- join(versions_dataset, { Sequel[self.model.table_name][:provider_version_id] => Sequel[:versions][:id] }, table_alias: :versions)
81
+ # @param [Array<PactBroker::Matrix::ResolvedSelector>] resolved_selectors
82
+ # @return [Array<Hash>]
83
+ def provider_criteria_for_selectors(resolved_selectors)
84
+ name_only, with_versions = resolved_selectors.partition(&:only_pacticipant_name_specified?)
85
+ table = self.model.table_name
86
+
87
+ criteria = []
88
+ criteria << { Sequel[table][:provider_id] => name_only.collect(&:pacticipant_id).uniq } if name_only.any?
89
+ criteria << { Sequel[table][:provider_version_id] => with_versions.collect(&:pacticipant_version_id).uniq } if with_versions.any?
90
+ criteria
80
91
  end
81
92
  end
82
93
  end
@@ -73,15 +73,43 @@ module PactBroker
73
73
 
74
74
  private :resolve_versions_and_add_ids
75
75
 
76
- # Return a Hash of the pacticipant names used in the selectors, where the key is the name and the value is the pacticipant
76
+ # Return a Hash of the pacticipant names used in the selectors, where the key is the name
77
+ # as specified in the selector (not necessarily the pacticipant's stored name, when
78
+ # use_case_sensitive_resource_names is false), and the value is the pacticipant.
77
79
  # @return [Hash<String, PactBroker::Domain::Pacticipant>]
78
80
  def find_pacticipants_for_selectors(unresolved_selectors)
79
81
  names = unresolved_selectors.collect(&:pacticipant_name)
80
- PactBroker::Domain::Pacticipant.where(name: names).all.group_by(&:name).transform_values(&:first)
82
+ pacticipants = pacticipants_matching_names(names)
83
+
84
+ names.uniq.each_with_object({}) do | name, hash |
85
+ matching = pacticipants.select { | candidate | pacticipant_name_matches?(candidate.name, name) }
86
+ pacticipant_repository.handle_multiple_pacticipants_found(name, matching) if matching.size > 1
87
+ hash[name] = matching.first if matching.any?
88
+ end
81
89
  end
82
90
 
83
91
  private :find_pacticipants_for_selectors
84
92
 
93
+ # @param [Array<String>] names
94
+ # @return [Array<PactBroker::Domain::Pacticipant>]
95
+ def pacticipants_matching_names(names)
96
+ return [] if names.empty?
97
+
98
+ PactBroker::Domain::Pacticipant.where(Sequel.|(*names.collect { | name | Sequel.name_like(:name, name) })).all
99
+ end
100
+
101
+ private :pacticipants_matching_names
102
+
103
+ def pacticipant_name_matches?(stored_name, requested_name)
104
+ if PactBroker.configuration.use_case_sensitive_resource_names
105
+ stored_name == requested_name
106
+ else
107
+ stored_name.casecmp?(requested_name)
108
+ end
109
+ end
110
+
111
+ private :pacticipant_name_matches?
112
+
85
113
  def build_selectors(pacticipants_hash, unresolved_selector, selector_type, selector_ignorer)
86
114
  selector_builder = ResolvedSelectorBuilder.new(unresolved_selector, selector_type, selector_ignorer)
87
115
  selector_builder.pacticipant = pacticipants_hash[unresolved_selector.pacticipant_name]
@@ -1,3 +1,4 @@
1
+ require "pact_broker/dataset"
1
2
  require "pact_broker/domain/pacticipant"
2
3
  require "pact_broker/error"
3
4
  require "pact_broker/repositories/scopes"
@@ -47,6 +48,8 @@ module PactBroker
47
48
  end
48
49
 
49
50
  def find_by_name_or_create name
51
+ raise PactBroker::Error, "Cannot create a pacticipant with a blank name" if name.to_s.strip.empty?
52
+
50
53
  pacticipant = find_by_name(name)
51
54
  pacticipant ? pacticipant : create(name: name)
52
55
  end
@@ -104,7 +107,7 @@ module PactBroker
104
107
  end
105
108
 
106
109
  def search_by_name(pacticipant_name)
107
- terms = pacticipant_name.split.map { |v| v.gsub("_", "\\_") }
110
+ terms = pacticipant_name.split.map { |v| PactBroker::Dataset::Helpers.escape_wildcards(v) }
108
111
  columns = [:name, :display_name]
109
112
  string_match_query = Sequel.|(
110
113
  *terms.map do |term|
@@ -182,7 +182,7 @@ module PactBroker
182
182
  # Modifing pacts is strongly discouraged now, and support for it will be dropped in the next major version of the Pact Broker
183
183
  def create_pact_revision params, existing_pact
184
184
  logger.info("Updating existing pact publication", params.without(:json_content))
185
- logger.debug("Content #{params[:json_content]}")
185
+ logger.debug { "Content #{params[:json_content]}" }
186
186
  pact_version_sha = params.fetch(:pact_version_sha)
187
187
  json_content = add_interaction_ids(params[:json_content])
188
188
  update_params = { pact_version_sha: pact_version_sha, json_content: json_content }
@@ -211,7 +211,7 @@ module PactBroker
211
211
  # When no publication for the given consumer/provider/consumer version number exists
212
212
  def create_pact(params, version, provider)
213
213
  logger.debug("Creating new pact publication", params.without(:json_content))
214
- logger.debug("Content #{params[:json_content]}")
214
+ logger.debug { "Content #{params[:json_content]}" }
215
215
  json_content = add_interaction_ids(params[:json_content])
216
216
  pact = pact_repository.create(
217
217
  version_id: version.id,
@@ -24,17 +24,16 @@ module PactBroker
24
24
  end
25
25
 
26
26
  def delete args
27
- version = version_repository.find_by_pacticipant_name_and_number args.fetch(:pacticipant_name), args.fetch(:pacticipant_version_number)
28
- connection = PactBroker::Domain::Tag.new.db
29
- connection.run("delete from tags where name = '#{args.fetch(:tag_name)}' and version_id = '#{version.id}'")
27
+ version = version_repository.find_by_pacticipant_name_and_number(args.fetch(:pacticipant_name), args.fetch(:pacticipant_version_number))
28
+ PactBroker::Domain::Tag.where(name: args.fetch(:tag_name), version_id: version.id).delete
30
29
  end
31
30
 
32
31
  def find_all_tag_names_for_pacticipant pacticipant_name
33
- tag_repository.find_all_tag_names_for_pacticipant pacticipant_name
32
+ tag_repository.find_all_tag_names_for_pacticipant(pacticipant_name)
34
33
  end
35
34
 
36
35
  def find_all_by_pacticipant_name_and_tag pacticipant_name:, tag_name:
37
- tag_repository.find_all_by_pacticipant_name_and_tag pacticipant_name, tag_name
36
+ tag_repository.find_all_by_pacticipant_name_and_tag(pacticipant_name, tag_name)
38
37
  end
39
38
  end
40
39
  end
@@ -10,7 +10,7 @@ module PactBroker
10
10
  include PactBroker::Repositories
11
11
  include PactBroker::Repositories::Scopes
12
12
 
13
- # Ideally this would just be a sequence, but Sqlite and MySQL don't support sequences
13
+ # Ideally this would just be a sequence, but Sqlite doesn't support sequences
14
14
  # in the way we need to use them ie. determining what the next number will be before we
15
15
  # create the record, because Webmachine wants to set the URL of the resource that is about
16
16
  # to be created *before* we actually create it.
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = "2.120.0"
2
+ VERSION = "2.121.0"
3
3
  end
@@ -27,14 +27,14 @@ module PactBroker
27
27
  branch = find_or_create_branch(version.pacticipant, branch_name)
28
28
  branch_version = version.branch_version_for_branch(branch)
29
29
  if branch_version
30
- PactBroker.logger.info("Updating branch version #{branch_version.inspect}, time: #{Time.now}")
31
- branch_version.update(updated_at: Sequel.datetime_class.now)
30
+ PactBroker.logger.debug("Branch version already exists #{branch_version.inspect}")
32
31
  else
33
32
  PactBroker.logger.info("Creating branch version time: #{Time.now}")
34
33
  branch_version = PactBroker::Versions::BranchVersion.new(version: version, branch: branch, auto_created: auto_created).insert_ignore
35
34
  PactBroker::Versions::BranchHead.new(branch: branch, branch_version: branch_version).upsert
36
35
  end
37
- branch.update(updated_at: Sequel.datetime_class.now)
36
+ now = Sequel.datetime_class.now
37
+ Branch.where(id: branch.id).where(Sequel[:updated_at] < now - Rational(60, 86400)).update(updated_at: now)
38
38
  pacticipant_service.maybe_set_main_branch(version.pacticipant, branch_name)
39
39
  branch_version
40
40
  end
@@ -89,22 +89,13 @@ module PactBroker
89
89
  end
90
90
 
91
91
  def create_or_update(pacticipant, version_number, open_struct_version)
92
- saved_version = PactBroker::Domain::Version.where(pacticipant_id: pacticipant.id, number: version_number).single_record
93
92
  params = open_struct_version.to_h
94
93
  tags = params.delete(:tags)
95
94
  branch_name = params.delete(:branch)
96
- if saved_version
97
- saved_version.update(params)
98
- else
99
- # Upsert is only for race conditions
100
- # Upsert blanks out any fields that are not provided
101
- saved_version = PactBroker::Domain::Version.new(
102
- params.merge(
103
- pacticipant_id: pacticipant.id,
104
- number: version_number
105
- ).compact
106
- ).upsert
107
- end
95
+ insert_params = params.merge(pacticipant_id: pacticipant.id, number: version_number).compact
96
+ saved_version = PactBroker::Domain::Version.new(insert_params).insert_ignore
97
+ update_params = params.reject { |k, _| [:created_at, :order].include?(k) }
98
+ saved_version.update(update_params) if update_params.any?
108
99
 
109
100
  branch_version_repository.add_branch(saved_version, branch_name) if branch_name
110
101
  replace_tags(saved_version, tags) if tags
@@ -3,7 +3,7 @@ require "pact_broker/webhooks/pact_and_verification_parameters"
3
3
  module PactBroker
4
4
  module Webhooks
5
5
  class Render
6
- TEMPLATE_PARAMETER_REGEXP = /\$\{pactbroker\.[^\}]+\}/
6
+ TEMPLATE_PARAMETER_REGEXP = /\$\{pactbroker\.[^\}]++\}/
7
7
  DEFAULT_ESCAPER = lambda { |it| it }
8
8
 
9
9
  def self.includes_parameter?(value)
@@ -19,7 +19,7 @@ module Sequel
19
19
 
20
20
  def upsert(opts = {})
21
21
  @upsert_plugin_upserting = true
22
- if postgres? || mysql?
22
+ if postgres?
23
23
  save(opts)
24
24
  else
25
25
  manual_upsert(opts)
@@ -92,9 +92,6 @@ module Sequel
92
92
  if upsert_plugin_upserting
93
93
  if postgres?
94
94
  super.insert_conflict(update: values_to_update, target: self.class.upsert_plugin_identifying_columns)
95
- elsif mysql?
96
- columns_to_update = values_to_update.keys - self.class.upsert_plugin_identifying_columns
97
- super.on_duplicate_key_update(*columns_to_update)
98
95
  else
99
96
  super
100
97
  end
@@ -103,10 +100,6 @@ module Sequel
103
100
  end
104
101
  end
105
102
 
106
- def mysql?
107
- model.db.adapter_scheme.to_s =~ /mysql/
108
- end
109
-
110
103
  def postgres?
111
104
  model.db.adapter_scheme.to_s =~ /postgres/
112
105
  end
data/pact_broker.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |gem|
14
14
  gem.summary = %q{See description}
15
15
  gem.homepage = "https://github.com/pact-foundation/pact_broker"
16
16
 
17
- gem.required_ruby_version = ">= 2.7.0"
17
+ gem.required_ruby_version = ">= 3.2.0"
18
18
 
19
19
  gem.files = begin
20
20
  if Dir.exist?(".git")
@@ -64,7 +64,7 @@ Gem::Specification.new do |gem|
64
64
  gem.add_runtime_dependency "sucker_punch", "~>3.0"
65
65
  gem.add_runtime_dependency "rack-protection", "~> 4.1"
66
66
  gem.add_runtime_dependency "table_print", "~> 1.5"
67
- gem.add_runtime_dependency "semantic_logger", "~> 4.11"
67
+ gem.add_runtime_dependency "semantic_logger", "~> 5.0"
68
68
  gem.add_runtime_dependency "sanitize", "~> 6.0"
69
69
  gem.add_runtime_dependency "wisper", "~> 2.0"
70
70
  gem.add_runtime_dependency "anyway_config", "~> 2.1"
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pact_broker
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.120.0
4
+ version: 2.121.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bethany Skurrie
8
8
  - Sergei Matheson
9
9
  - Warner Godfrey
10
+ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 1980-01-02 00:00:00.000000000 Z
13
+ date: 2026-09-04 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: json
@@ -227,14 +228,14 @@ dependencies:
227
228
  requirements:
228
229
  - - "~>"
229
230
  - !ruby/object:Gem::Version
230
- version: '4.11'
231
+ version: '5.0'
231
232
  type: :runtime
232
233
  prerelease: false
233
234
  version_requirements: !ruby/object:Gem::Requirement
234
235
  requirements:
235
236
  - - "~>"
236
237
  - !ruby/object:Gem::Version
237
- version: '4.11'
238
+ version: '5.0'
238
239
  - !ruby/object:Gem::Dependency
239
240
  name: sanitize
240
241
  requirement: !ruby/object:Gem::Requirement
@@ -591,6 +592,8 @@ files:
591
592
  - docs/developer/design_pattern_for_eager_loading_collections.md
592
593
  - docs/developer/matrix.md
593
594
  - docs/developer/rack.md
595
+ - docs/matrix-optimisation/README.md
596
+ - lib/pact/doc.rb
594
597
  - lib/pact/doc/doc_file.rb
595
598
  - lib/pact/doc/generate.rb
596
599
  - lib/pact/doc/generator.rb
@@ -735,9 +738,11 @@ files:
735
738
  - lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb
736
739
  - lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb
737
740
  - lib/pact_broker/api/resources/dashboard.rb
741
+ - lib/pact_broker/api/resources/deployed_or_released_versions.rb
738
742
  - lib/pact_broker/api/resources/deployed_version.rb
739
743
  - lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb
740
744
  - lib/pact_broker/api/resources/environment.rb
745
+ - lib/pact_broker/api/resources/environment_versions.rb
741
746
  - lib/pact_broker/api/resources/environments.rb
742
747
  - lib/pact_broker/api/resources/error_handler.rb
743
748
  - lib/pact_broker/api/resources/error_handling_methods.rb
@@ -967,6 +972,8 @@ files:
967
972
  - lib/pact_broker/locale/en.yml
968
973
  - lib/pact_broker/logging.rb
969
974
  - lib/pact_broker/logging/default_formatter.rb
975
+ - lib/pact_broker/logging/open_telemetry_appender_setup.rb
976
+ - lib/pact_broker/logging/trace_aware_json_formatter.rb
970
977
  - lib/pact_broker/matrix/deployment_status_summary.rb
971
978
  - lib/pact_broker/matrix/every_row.rb
972
979
  - lib/pact_broker/matrix/integration.rb
@@ -1278,6 +1285,7 @@ homepage: https://github.com/pact-foundation/pact_broker
1278
1285
  licenses:
1279
1286
  - MIT
1280
1287
  metadata: {}
1288
+ post_install_message:
1281
1289
  rdoc_options: []
1282
1290
  require_paths:
1283
1291
  - lib
@@ -1285,14 +1293,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
1285
1293
  requirements:
1286
1294
  - - ">="
1287
1295
  - !ruby/object:Gem::Version
1288
- version: 2.7.0
1296
+ version: 3.2.0
1289
1297
  required_rubygems_version: !ruby/object:Gem::Requirement
1290
1298
  requirements:
1291
1299
  - - ">="
1292
1300
  - !ruby/object:Gem::Version
1293
1301
  version: '0'
1294
1302
  requirements: []
1295
- rubygems_version: 4.0.16
1303
+ rubygems_version: 3.4.19
1304
+ signing_key:
1296
1305
  specification_version: 4
1297
1306
  summary: See description
1298
1307
  test_files: []