pact_broker 2.119.0 → 2.120.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Gemfile +2 -0
  4. data/README.md +1 -1
  5. data/db/migrations/000015_create_pact_version_content.rb +1 -1
  6. data/db/migrations/000023_create_pact_versions_table.rb +1 -1
  7. data/db/migrations/000033_create_config_table.rb +1 -1
  8. data/db/migrations/000036_create_webhook_execution.rb +1 -1
  9. data/db/migrations/20180108_create_certificates_table.rb +1 -1
  10. data/db/migrations/migration_helper.rb +5 -1
  11. data/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb +4 -0
  12. data/lib/pact_broker/api/decorators/version_decorator.rb +23 -3
  13. data/lib/pact_broker/api/resources/branch_versions.rb +5 -1
  14. data/lib/pact_broker/api/resources/can_i_deploy_pacticipant_version_by_branch_to_environment.rb +4 -1
  15. data/lib/pact_broker/api/resources/can_i_deploy_pacticipant_version_by_branch_to_environment_badge.rb +1 -0
  16. data/lib/pact_broker/api/resources/clean.rb +9 -1
  17. data/lib/pact_broker/api/resources/pacticipants_for_label.rb +29 -1
  18. data/lib/pact_broker/api/resources/tag_versions.rb +5 -1
  19. data/lib/pact_broker/api/resources/version.rb +5 -1
  20. data/lib/pact_broker/api/resources/versions.rb +5 -1
  21. data/lib/pact_broker/db/clean/branch_selector.rb +40 -0
  22. data/lib/pact_broker/db/clean.rb +45 -0
  23. data/lib/pact_broker/db/clean_incremental.rb +77 -9
  24. data/lib/pact_broker/deployments/released_version_service.rb +14 -0
  25. data/lib/pact_broker/matrix/parse_can_i_deploy_query.rb +5 -0
  26. data/lib/pact_broker/pacticipants/repository.rb +1 -1
  27. data/lib/pact_broker/tasks/clean_task.rb +23 -1
  28. data/lib/pact_broker/version.rb +1 -1
  29. data/lib/pact_broker/versions/branch_repository.rb +1 -1
  30. data/lib/pact_broker/versions/branch_service.rb +1 -1
  31. data/lib/pact_broker/versions/branch_version_repository.rb +1 -0
  32. data/pact_broker.gemspec +6 -5
  33. metadata +25 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e28c13c74efb9e4cd894da6d14e18a28ea6d15c3240f4defc2b2fe246d1fafb0
4
- data.tar.gz: 6194f39dd67344eed51782de55bea6d95ee937716bd59afbe8b6ee2cfe473b08
3
+ metadata.gz: ac9a9b4f3ed5b352518c0d50229fa8348042ff711c25f099bd1b8d8e02dab7b4
4
+ data.tar.gz: 71567e868b7e8dd466acb52911e69656a15a80a9dc73d7435afe8cb34e6fbc9b
5
5
  SHA512:
6
- metadata.gz: 40138782cf26259e00e0f2417d9243f5066f4b6dd77ad076869f12f47adcfb7f4a85481e4b9f3cff4ba9ba9bf03881bbb69e71fa7b269bd868a472ed74c2b91e
7
- data.tar.gz: 056be7ffcb8273b41d7e1292307b495a6277b71248b685515185f0239b477aa8fb6ff6646ef9ac22e86cfb87aa10bfe969ff93a1868ef10e1e81a3ae793f381a
6
+ metadata.gz: b36c2d3066419de3ab741c0ed9333a50acf2b1ee0e1af39f36cb04b3457ba9f48859aca60925f832a52f8c12465e27a7249a0687ceba9dd4389edad51f53d6a4
7
+ data.tar.gz: ff6ab08cf0c0b513a1f04239376839d7643563a8c57ed991e1e623e0ca20adf42acdb4679735a6add5b96d9de337776f7f5ed45ac059419c3f44b284bb7b1718
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ <a name="v2.120.0"></a>
2
+ ### v2.120.0 (2026-07-14)
3
+
4
+ #### Features
5
+
6
+ * expose released environments on branch/tag version list endpoints (#969) ([bea68a7d](/../../commit/bea68a7d))
7
+ * expose released environments on version API (#963) ([d6f7f17b](/../../commit/d6f7f17b))
8
+ * return more webhooks attributes in logs decorator (#961) ([4e2d1b13](/../../commit/4e2d1b13))
9
+ * paginate pacticipants_for_label ([088ff444](/../../commit/088ff444))
10
+ * add limit to stale branch delete (#933) ([406c20f3](/../../commit/406c20f3))
11
+ * change branch sort order, most recently updated first (#929) ([b5215a39](/../../commit/b5215a39))
12
+
13
+ * **clean**
14
+ * expose branch_deletion_limit on CleanTask (#938) ([540afa3d](/../../commit/540afa3d))
15
+ * add stale branch lifecycle and automated cleanup (#912) ([1ba6354f](/../../commit/1ba6354f))
16
+
17
+ * **PACT-6253**
18
+ * add json_schemer as explicit gem dependency (#916) ([dc8910ce](/../../commit/dc8910ce))
19
+
20
+ #### Bug Fixes
21
+
22
+ * **spec**
23
+ * rebuild success-after-latestby fixture with distinct provider versions ([778d25d2](/../../commit/778d25d2))
24
+ * rebuild latestby filtering fixtures to genuinely pin the ordering contract ([9276aa79](/../../commit/9276aa79))
25
+
26
+ * broken SQL in pacticipants_for_label resource (#944) ([bf37cdfc](/../../commit/bf37cdfc))
27
+ * use cvpv when checking can-i-deploy against an environment (#903) (#934) ([7163652d](/../../commit/7163652d))
28
+ * remove deadcode, improve testing with UTC (#924) ([76c2407a](/../../commit/76c2407a))
29
+ * join with pacticipant when query main branch (#921) ([3dae89be](/../../commit/3dae89be))
30
+
1
31
  <a name="v2.119.0"></a>
2
32
  ### v2.119.0 (2026-04-02)
3
33
 
data/Gemfile CHANGED
@@ -21,6 +21,8 @@ end
21
21
 
22
22
  group :test do
23
23
  gem "simplecov", :require => false
24
+ gem "simplecov-cobertura", :require => false
25
+ gem "rspec_junit_formatter", :require => false
24
26
  if ENV["X_PACT_DEVELOPMENT"] == "true"
25
27
  gem "pact", path: "../pact-ruby"
26
28
  gem "pact-ffi", path: "../pact-ruby-ffi"
data/README.md CHANGED
@@ -170,7 +170,7 @@ Please read the [UPGRADING.md](UPGRADING.md) documentation before upgrading your
170
170
 
171
171
  The Pact Broker follows the [semantic versioning](https://semver.org/) scheme.
172
172
 
173
- [decouple]: https://www.rea-group.com/blog/enter-the-pact-matrix-or-how-to-decouple-the-release-cycles-of-your-microservices/
173
+ [decouple]: https://docs.pact.io/pact_broker/can_i_deploy
174
174
  [pact]: https://github.com/pact-foundation/pact-ruby
175
175
  [nerf]: https://github.com/pact-foundation/pact_broker/wiki/pact-broker-ci-nerf-gun
176
176
  [different-teams]: https://github.com/pact-foundation/pact-ruby/wiki/Using-pact-where-the-consumer-team-is-different-from-the-provider-team
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  change do
6
6
  create_table(:pact_version_contents, charset: "utf8") do
7
7
  String :sha, primary_key: true, null: false, primary_key_constraint_name: "pk_pact_version_contents"
8
- String :content, type: PactBroker::MigrationHelper.large_text_type
8
+ column :content, PactBroker::MigrationHelper.large_text_type
9
9
  DateTime :created_at, null: false
10
10
  DateTime :updated_at, null: false
11
11
  end
@@ -7,7 +7,7 @@ Sequel.migration do
7
7
  foreign_key :consumer_id, :pacticipants
8
8
  foreign_key :provider_id, :pacticipants
9
9
  String :sha, null: false
10
- String :content, type: PactBroker::MigrationHelper.large_text_type
10
+ column :content, PactBroker::MigrationHelper.large_text_type
11
11
  index [:consumer_id, :provider_id, :sha], unique: true, name: "unq_pvc_con_prov_sha"
12
12
  DateTime :created_at, null: false
13
13
  end
@@ -6,7 +6,7 @@ Sequel.migration do
6
6
  primary_key :id
7
7
  String :name, null: false
8
8
  String :type, null: false
9
- String :value, type: PactBroker::MigrationHelper.large_text_type
9
+ column :value, PactBroker::MigrationHelper.large_text_type
10
10
  DateTime :created_at, null: false
11
11
  DateTime :updated_at, null: false
12
12
  index [:name], unique: true, name: "unq_config_name"
@@ -9,7 +9,7 @@ Sequel.migration do
9
9
  foreign_key :consumer_id, :pacticipants, null: false
10
10
  foreign_key :provider_id, :pacticipants, null: false
11
11
  Boolean :success, null: false
12
- String :logs, type: PactBroker::MigrationHelper.large_text_type
12
+ column :logs, PactBroker::MigrationHelper.large_text_type
13
13
  DateTime :created_at, null: false
14
14
  end
15
15
  end
@@ -6,7 +6,7 @@ Sequel.migration do
6
6
  primary_key :id
7
7
  String :uuid, null: false, unique: true, unique_constraint_name: "uq_certificate_uuid"
8
8
  String :description, null: true
9
- String :content, null: false, type: PactBroker::MigrationHelper.large_text_type
9
+ column :content, PactBroker::MigrationHelper.large_text_type, null: false
10
10
  DateTime :created_at, null: false
11
11
  DateTime :updated_at, null: false
12
12
  end
@@ -10,7 +10,7 @@ module PactBroker
10
10
  end
11
11
 
12
12
  def large_text_type
13
- if postgres?
13
+ if postgres? || sqlite?
14
14
  :text
15
15
  else
16
16
  # Assume mysql
@@ -32,6 +32,10 @@ module PactBroker
32
32
  adapter =~ /postgres/
33
33
  end
34
34
 
35
+ def sqlite?
36
+ adapter =~ /sqlite/
37
+ end
38
+
35
39
  def adapter
36
40
  Sequel::Model.db.adapter_scheme.to_s
37
41
  end
@@ -11,6 +11,10 @@ module PactBroker
11
11
  end
12
12
 
13
13
 
14
+ property :event_name, as: :eventName
15
+ property :consumer_name, as: :consumerName
16
+ property :provider_name, as: :providerName
17
+
14
18
  nested :triggeredWebhook, embedded: true do
15
19
  property :uuid
16
20
  end
@@ -74,9 +74,12 @@ module PactBroker
74
74
  end
75
75
 
76
76
  links :'pb:deployed-environments' do | context |
77
- # I couldn't another way to check if call is from a collection or a single item 🤷‍♂️
78
- # Also not sure why the heck .to_a?(Hash) doesn't work, I mean what...
79
- deployed_versions = context[:deployed_versions].class.to_s == "Hash" ? context[:deployed_versions][represented.id] : context[:deployed_versions]
77
+ # NB: use fully-qualified ::Hash. The tins gem shadows the bare `Hash`
78
+ # constant in this scope, so `is_a?(Hash)` returns false even for a real
79
+ # Hash (this is the collection-vs-single-item branch: the collection
80
+ # resource passes a version_id-keyed Hash, the single resource an Array).
81
+ deployed_versions = context.dig(:deployed_versions)
82
+ deployed_versions = deployed_versions[represented.id] if deployed_versions.is_a?(::Hash)
80
83
  deployed_versions&.collect do | deployed_version |
81
84
  {
82
85
  title: "Version deployed to #{deployed_version.environment.display_name}",
@@ -89,6 +92,23 @@ module PactBroker
89
92
  end
90
93
  end
91
94
 
95
+ links :'pb:released-environments' do | context |
96
+ # NB: use fully-qualified ::Hash. The tins gem shadows the bare `Hash`
97
+ # constant in this scope, so `is_a?(Hash)` returns false even for a real
98
+ # Hash (this is the collection-vs-single-item branch: the collection
99
+ # resource passes a version_id-keyed Hash, the single resource an Array).
100
+ released_versions = context.dig(:released_versions)
101
+ released_versions = released_versions[represented.id] if released_versions.is_a?(::Hash)
102
+ released_versions&.collect do | released_version |
103
+ {
104
+ title: "Version released to #{released_version.environment.display_name}",
105
+ name: released_version.environment.display_name,
106
+ href: released_version_url(released_version, context.fetch(:base_url)),
107
+ currently_supported: released_version.currently_supported
108
+ }
109
+ end
110
+ end
111
+
92
112
  links :'pb:record-deployment' do | context |
93
113
  context[:environments]&.collect do | environment |
94
114
  {
@@ -27,7 +27,7 @@ module PactBroker
27
27
  end
28
28
 
29
29
  def to_json
30
- decorator_class(:versions_decorator).new(versions).to_json(**decorator_options(identifier_from_path.merge(resource_title: resource_title, deployed_versions: deployed_versions)))
30
+ decorator_class(:versions_decorator).new(versions).to_json(**decorator_options(identifier_from_path.merge(resource_title: resource_title, deployed_versions: deployed_versions, released_versions: released_versions)))
31
31
  end
32
32
 
33
33
  def versions
@@ -38,6 +38,10 @@ module PactBroker
38
38
  @deployed_versions ||= deployed_version_service.find_deployed_versions_for_versions(versions)
39
39
  end
40
40
 
41
+ def released_versions
42
+ @released_versions ||= released_version_service.find_released_versions_for_versions(versions)
43
+ end
44
+
41
45
  def policy_name
42
46
  :'versions::versions'
43
47
  end
@@ -37,8 +37,11 @@ module PactBroker
37
37
  end
38
38
 
39
39
  def options
40
+ # "cvpv" (not "cvp") so that every version of an integrated application that is currently
41
+ # released/deployed to the environment is evaluated independently, rather than collapsing
42
+ # to just the latest provider version (which hides still-live incompatible versions). See issue #903.
40
43
  @options ||= {
41
- latestby: "cvp",
44
+ latestby: "cvpv",
42
45
  environment_name: identifier_from_path[:environment_name]
43
46
  }
44
47
  end
@@ -1,4 +1,5 @@
1
1
  require "pact_broker/api/resources/can_i_deploy_pacticipant_version_by_branch_to_environment"
2
+ require "pact_broker/api/resources/badge_methods"
2
3
 
3
4
  module PactBroker
4
5
  module Api
@@ -1,5 +1,6 @@
1
1
  require "pact_broker/api/resources/base_resource"
2
2
  require "pact_broker/db/clean"
3
+ require "pact_broker/db/clean/branch_selector"
3
4
  require "pact_broker/matrix/unresolved_selector"
4
5
 
5
6
  # Not exposed yet as we'd need to support administrator auth first
@@ -22,7 +23,14 @@ module PactBroker
22
23
  PactBroker::DB::Clean::Selector.new(hash)
23
24
  end
24
25
 
25
- result = PactBroker::DB::Clean.call(Sequel::Model.db, { keep: keep_selectors })
26
+ keep_branch_selectors = (params[:keep_branches] || []).collect do | hash |
27
+ PactBroker::DB::Clean::BranchSelector.from_hash(hash)
28
+ end
29
+
30
+ result = PactBroker::DB::Clean.call(Sequel::Model.db, {
31
+ keep: keep_selectors,
32
+ keep_branches: keep_branch_selectors.empty? ? nil : keep_branch_selectors
33
+ })
26
34
  response.body = result.to_json
27
35
  else
28
36
  415
@@ -1,10 +1,12 @@
1
1
  require "pact_broker/api/resources/base_resource"
2
2
  require "pact_broker/api/decorators/pacticipants_decorator"
3
+ require "pact_broker/api/resources/pagination_methods"
3
4
 
4
5
  module PactBroker
5
6
  module Api
6
7
  module Resources
7
8
  class PacticipantsForLabel < BaseResource
9
+ include PaginationMethods
8
10
 
9
11
  def content_types_provided
10
12
  [["application/hal+json", :to_json]]
@@ -14,8 +16,18 @@ module PactBroker
14
16
  ["GET", "OPTIONS"]
15
17
  end
16
18
 
19
+ def malformed_request?
20
+ if super
21
+ true
22
+ elsif request.get? && validation_errors_for_schema?(schema, request.query)
23
+ true
24
+ else
25
+ false
26
+ end
27
+ end
28
+
17
29
  def to_json
18
- generate_json(pacticipant_service.find identifier_from_path )
30
+ generate_json(pacticipant_service.find_all_pacticipants(filter_options, pagination_options, eager_load_associations))
19
31
  end
20
32
 
21
33
  def generate_json pacticipants
@@ -25,6 +37,22 @@ module PactBroker
25
37
  def policy_name
26
38
  :'pacticipants::pacticipants'
27
39
  end
40
+
41
+ private
42
+
43
+ def schema
44
+ PactBroker::Api::Contracts::PaginationQueryParamsSchema
45
+ end
46
+
47
+ def eager_load_associations
48
+ decorator_class(:pacticipants_decorator).eager_load_associations
49
+ end
50
+
51
+ def filter_options
52
+ options = { label_name: identifier_from_path[:label_name] }
53
+ options[:query_string] = request.query["q"] if request.query.has_key?("q")
54
+ options
55
+ end
28
56
  end
29
57
  end
30
58
  end
@@ -30,7 +30,7 @@ module PactBroker
30
30
 
31
31
  def to_json
32
32
  decorator_class(:versions_decorator).new(tag_versions)
33
- .to_json(**decorator_options(deployed_versions: deployed_versions))
33
+ .to_json(**decorator_options(deployed_versions: deployed_versions, released_versions: released_versions))
34
34
  end
35
35
 
36
36
  private
@@ -39,6 +39,10 @@ module PactBroker
39
39
  @deployed_versions ||= deployed_version_service.find_deployed_versions_for_versions(tag_versions)
40
40
  end
41
41
 
42
+ def released_versions
43
+ @released_versions ||= released_version_service.find_released_versions_for_versions(tag_versions)
44
+ end
45
+
42
46
  def tag_versions
43
47
  @versions ||= version_service.find_by_ids_in_reverse_order(@tags.select_map(:version_id), pagination_options, decorator_class(:versions_decorator).eager_load_associations)
44
48
  end
@@ -54,7 +54,7 @@ module PactBroker
54
54
  end
55
55
 
56
56
  def to_json
57
- decorator_class(:version_decorator).new(version).to_json(**decorator_options(environments: environments, deployed_versions: deployed_versions))
57
+ decorator_class(:version_decorator).new(version).to_json(**decorator_options(environments: environments, deployed_versions: deployed_versions, released_versions: released_versions))
58
58
  end
59
59
 
60
60
  def delete_resource
@@ -87,6 +87,10 @@ module PactBroker
87
87
  @deployed_versions ||= deployed_version_service.find_deployed_versions_for_version(version)
88
88
  end
89
89
 
90
+ def released_versions
91
+ @released_versions ||= released_version_service.find_released_versions_for_version(version)
92
+ end
93
+
90
94
  def version
91
95
  @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path)
92
96
  end
@@ -27,7 +27,7 @@ module PactBroker
27
27
  end
28
28
 
29
29
  def to_json
30
- decorator_class(:versions_decorator).new(versions).to_json(**decorator_options(identifier_from_path.merge(deployed_versions: deployed_versions)))
30
+ decorator_class(:versions_decorator).new(versions).to_json(**decorator_options(identifier_from_path.merge(deployed_versions: deployed_versions, released_versions: released_versions)))
31
31
  end
32
32
 
33
33
  def versions
@@ -38,6 +38,10 @@ module PactBroker
38
38
  @deployed_versions ||= deployed_version_service.find_deployed_versions_for_versions(versions)
39
39
  end
40
40
 
41
+ def released_versions
42
+ @released_versions ||= released_version_service.find_released_versions_for_versions(versions)
43
+ end
44
+
41
45
  def policy_name
42
46
  :'versions::versions'
43
47
  end
@@ -0,0 +1,40 @@
1
+ require "pact_broker/hash_refinements"
2
+
3
+ module PactBroker
4
+ module DB
5
+ class Clean
6
+ class BranchSelector
7
+ using PactBroker::HashRefinements
8
+
9
+ ATTRIBUTES = [:max_age, :branch]
10
+
11
+ attr_accessor(*ATTRIBUTES)
12
+
13
+ def initialize(attributes = {})
14
+ attributes.each do | (name, value) |
15
+ instance_variable_set("@#{name}", value) if respond_to?(name)
16
+ end
17
+ @source_hash = attributes[:source_hash]
18
+ end
19
+
20
+ def self.from_hash(hash)
21
+ standard_hash = hash.symbolize_keys.snakecase_keys
22
+ new_hash = standard_hash.slice(*ATTRIBUTES)
23
+ new_hash[:source_hash] = hash
24
+ new(new_hash.compact)
25
+ end
26
+
27
+ def to_hash
28
+ ATTRIBUTES.each_with_object({}) do | key, hash |
29
+ hash[key] = send(key)
30
+ end.compact
31
+ end
32
+ alias_method :to_h, :to_hash
33
+
34
+ def to_json(_opts = nil)
35
+ (@source_hash || to_hash).to_json
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,6 +2,7 @@ require "sequel"
2
2
  require "pact_broker/project_root"
3
3
  require "pact_broker/logging"
4
4
  require "pact_broker/db/clean/selector"
5
+ require "pact_broker/db/clean/branch_selector"
5
6
 
6
7
  module PactBroker
7
8
  module DB
@@ -130,6 +131,9 @@ module PactBroker
130
131
  delete_orphan_tags
131
132
  delete_orphan_versions
132
133
 
134
+ deleted_counts[:stale_branches] = delete_stale_branches
135
+ kept_counts[:stale_branches] = db[:branches].count
136
+
133
137
  { kept: kept_counts, deleted: deleted_counts }
134
138
  end
135
139
 
@@ -180,6 +184,47 @@ module PactBroker
180
184
  db[:versions].where(id: referenced_version_ids).invert.delete
181
185
  end
182
186
 
187
+ def delete_stale_branches
188
+ return 0 unless keep_branches && !keep_branches.empty?
189
+
190
+ db[:branches].where(id: stale_branch_ids_to_delete.from_self.select(:id)).delete
191
+ end
192
+
193
+ def stale_branch_ids_to_delete
194
+ db[:branches]
195
+ .select(Sequel[:branches][:id])
196
+ .left_outer_join(stale_branch_ids_to_keep, { Sequel[:branches][:id] => Sequel[:keep_branches][:id] }, table_alias: :keep_branches)
197
+ .where(Sequel[:keep_branches][:id] => nil)
198
+ end
199
+
200
+ def stale_branch_ids_to_keep
201
+ queries = [main_branch_ids_to_keep]
202
+
203
+ keep_branches.each do | selector |
204
+ if selector.max_age
205
+ cutoff = Date.today - selector.max_age
206
+ queries << db[:branches].where { updated_at >= cutoff }.select(:id)
207
+ end
208
+ if selector.branch
209
+ queries << db[:branches].where(name: Array(selector.branch)).select(:id)
210
+ end
211
+ end
212
+
213
+ queries.reduce(:union)
214
+ end
215
+
216
+ def main_branch_ids_to_keep
217
+ db[:branches]
218
+ .join(:pacticipants, id: Sequel[:branches][:pacticipant_id])
219
+ .exclude(Sequel[:pacticipants][:main_branch] => nil)
220
+ .where(Sequel[:branches][:name] => Sequel[:pacticipants][:main_branch])
221
+ .select(Sequel[:branches][:id])
222
+ end
223
+
224
+ def keep_branches
225
+ @keep_branches ||= options[:keep_branches]&.collect { | selector_hash | BranchSelector.from_hash(selector_hash.to_hash) }
226
+ end
227
+
183
228
  def delete_overwritten_verifications
184
229
  verification_ids = db[:verifications].select(:id).where(id: db[:latest_verification_id_for_pact_version_and_provider_version].select(:verification_id)).invert
185
230
  deleted_counts = { verification_results: verification_ids.count }
@@ -2,6 +2,7 @@ require "pact_broker/logging"
2
2
  require "pact_broker/matrix/unresolved_selector"
3
3
  require "pact_broker/date_helper"
4
4
  require "pact_broker/db/clean/selector"
5
+ require "pact_broker/db/clean/branch_selector"
5
6
 
6
7
  module PactBroker
7
8
  module DB
@@ -14,7 +15,10 @@ module PactBroker
14
15
  PactBroker::DB::Clean::Selector.new(released: true),
15
16
  PactBroker::DB::Clean::Selector.new(max_age: 90)
16
17
  ]
17
- TABLES = [:versions, :pact_publications, :pact_versions, :verifications, :triggered_webhooks, :webhook_executions]
18
+ DEFAULT_KEEP_BRANCH_SELECTORS = [
19
+ PactBroker::DB::Clean::BranchSelector.new(max_age: 90)
20
+ ]
21
+ TABLES = [:versions, :pact_publications, :pact_versions, :verifications, :triggered_webhooks, :webhook_executions, :branches]
18
22
 
19
23
  def self.call database_connection, options = {}
20
24
  new(database_connection, options).call
@@ -44,6 +48,7 @@ module PactBroker
44
48
  before_counts = current_counts
45
49
  PactBroker::Domain::Version.where(id: versions_to_delete.from_self.select_map(:id)).delete
46
50
  delete_orphan_pact_versions
51
+ delete_stale_branches
47
52
  after_counts = current_counts
48
53
 
49
54
  TABLES.each_with_object({}) do | table_name, comparison_counts |
@@ -68,10 +73,22 @@ module PactBroker
68
73
  end
69
74
  end
70
75
 
76
+ def keep_branches
77
+ @keep_branches ||= if options.key?(:keep_branches)
78
+ options[:keep_branches]&.collect { | selector_hash | PactBroker::DB::Clean::BranchSelector.from_hash(selector_hash.to_hash) }
79
+ else
80
+ DEFAULT_KEEP_BRANCH_SELECTORS
81
+ end
82
+ end
83
+
71
84
  def limit
72
85
  options[:limit] || 1000
73
86
  end
74
87
 
88
+ def branch_limit
89
+ options[:branch_limit] || limit
90
+ end
91
+
75
92
  def versions_to_delete(columns = [:id])
76
93
  fully_qualified_columns = columns.collect { |col| Sequel[:versions][col] }
77
94
  PactBroker::Domain::Version
@@ -96,6 +113,47 @@ module PactBroker
96
113
  options[:dry_run]
97
114
  end
98
115
 
116
+ def delete_stale_branches
117
+ return unless keep_branches && !keep_branches.empty?
118
+
119
+ # A direct subquery here fails in MySQL, so wrap the dataset in a
120
+ # derived table with `from_self` before selecting the ids for deletion.
121
+ PactBroker::Versions::Branch.where(id: stale_branch_ids_to_delete.from_self.select(:id)).delete
122
+ end
123
+
124
+ def stale_branch_ids_to_delete
125
+ PactBroker::Versions::Branch
126
+ .select(Sequel[:branches][:id])
127
+ .left_outer_join(stale_branch_ids_to_keep, { Sequel[:branches][:id] => Sequel[:keep_branches][:id] }, table_alias: :keep_branches)
128
+ .where(Sequel[:keep_branches][:id] => nil)
129
+ .limit(branch_limit)
130
+ end
131
+
132
+ def stale_branch_ids_to_keep
133
+ # Always keep main branches for each pacticipant
134
+ queries = [main_branch_ids_to_keep]
135
+
136
+ keep_branches.each do | selector |
137
+ if selector.max_age
138
+ cutoff = Date.today - selector.max_age
139
+ queries << PactBroker::Versions::Branch.where { updated_at >= cutoff }.select(Sequel[:branches][:id])
140
+ end
141
+ if selector.branch
142
+ queries << PactBroker::Versions::Branch.where(name: Array(selector.branch)).select(Sequel[:branches][:id])
143
+ end
144
+ end
145
+
146
+ queries.reduce(&:union)
147
+ end
148
+
149
+ def main_branch_ids_to_keep
150
+ PactBroker::Versions::Branch
151
+ .join(:pacticipants, id: Sequel[:branches][:pacticipant_id])
152
+ .exclude(Sequel[:pacticipants][:main_branch] => nil)
153
+ .where(Sequel[:branches][:name] => Sequel[:pacticipants][:main_branch])
154
+ .select(Sequel[:branches][:id])
155
+ end
156
+
99
157
  def delete_orphan_pact_versions
100
158
  db[:pact_versions].where(id: orphan_pact_versions).delete
101
159
  rescue Sequel::DatabaseError => e
@@ -142,19 +200,29 @@ module PactBroker
142
200
  }
143
201
  end
144
202
 
203
+ {
204
+ "counts" => dry_run_counts(kept_per_selector),
205
+ "versionSummary" => pacticipant_results
206
+ }
207
+ end
208
+
209
+ def dry_run_counts(kept_per_selector)
145
210
  total_versions_count = PactBroker::Domain::Version.count
146
211
  versions_to_keep_count = version_ids_to_keep.count
147
212
  versions_to_delete_count = versions_to_delete.count
148
213
 
214
+ total_branches_count = PactBroker::Versions::Branch.count
215
+ branches_to_delete_count = keep_branches && !keep_branches.empty? ? stale_branch_ids_to_delete.count : 0
216
+
149
217
  {
150
- "counts" => {
151
- "totalVersions" => total_versions_count,
152
- "versionsToDelete" => versions_to_delete_count,
153
- "versionsNotToKeep" => total_versions_count - versions_to_keep_count,
154
- "versionsToKeep" => versions_to_keep_count,
155
- "versionsToKeepBySelector" => kept_per_selector,
156
- },
157
- "versionSummary" => pacticipant_results
218
+ "totalVersions" => total_versions_count,
219
+ "versionsToDelete" => versions_to_delete_count,
220
+ "versionsNotToKeep" => total_versions_count - versions_to_keep_count,
221
+ "versionsToKeep" => versions_to_keep_count,
222
+ "versionsToKeepBySelector" => kept_per_selector,
223
+ "totalBranches" => total_branches_count,
224
+ "branchesToDelete" => branches_to_delete_count,
225
+ "branchesToKeep" => total_branches_count - branches_to_delete_count
158
226
  }
159
227
  end
160
228
 
@@ -46,6 +46,20 @@ module PactBroker
46
46
  .single_record
47
47
  end
48
48
 
49
+ def self.find_released_versions_for_version(version)
50
+ scope_for(ReleasedVersion)
51
+ .where(version_id: version.id)
52
+ .eager(:environment)
53
+ .all
54
+ end
55
+
56
+ def self.find_released_versions_for_versions(versions_array)
57
+ scope_for(ReleasedVersion)
58
+ .where(version_id: versions_array.map(&:id))
59
+ .eager(:environment)
60
+ .to_hash_groups(:version_id)
61
+ end
62
+
49
63
  def self.record_version_support_ended(released_version)
50
64
  released_version.record_support_ended
51
65
  end
@@ -26,6 +26,11 @@ module PactBroker
26
26
 
27
27
  if params[:environment].is_a?(String)
28
28
  options[:environment_name] = params[:environment]
29
+ # Multiple versions of an integrated application can be simultaneously released to an
30
+ # environment (record-release), or deployed to it on different targets (record-deployment).
31
+ # "cvp" collapses these down to the latest provider version, hiding incompatible versions
32
+ # that are still live. "cvpv" evaluates every co-resident version independently. See issue #903.
33
+ options[:latestby] = "cvpv"
29
34
  end
30
35
 
31
36
  if params[:ignore].is_a?(Array)
@@ -41,7 +41,7 @@ module PactBroker
41
41
  return [] if query.empty?
42
42
 
43
43
  query = query.select_all_qualified
44
- query = query.filter(:name, options[:query_string]) if options[:query_string]
44
+ query = query.filter(Sequel[:pacticipants][:name], options[:query_string]) if options[:query_string]
45
45
  query = query.label(options[:label_name]) if options[:label_name]
46
46
  query.order_ignore_case(Sequel[:pacticipants][:name]).eager(*eager_load_associations).all_with_pagination_options(pagination_options)
47
47
  end
@@ -10,7 +10,9 @@ module PactBroker
10
10
 
11
11
  attr_accessor :database_connection
12
12
  attr_reader :keep_version_selectors
13
+ attr_reader :keep_branch_selectors
13
14
  attr_accessor :version_deletion_limit
15
+ attr_accessor :branch_deletion_limit
14
16
  attr_accessor :logger
15
17
  attr_accessor :dry_run
16
18
  attr_accessor :use_lock # allow disabling of postgres lock if it is causing problems
@@ -21,6 +23,7 @@ module PactBroker
21
23
  @dry_run = false
22
24
  @use_lock = true
23
25
  @keep_version_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_SELECTORS
26
+ @keep_branch_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_BRANCH_SELECTORS
24
27
  rake_task(&block)
25
28
  end
26
29
 
@@ -31,10 +34,17 @@ module PactBroker
31
34
  end
32
35
  end
33
36
 
37
+ def keep_branch_selectors=(keep_branch_selectors)
38
+ require "pact_broker/db/clean/branch_selector"
39
+ @keep_branch_selectors = keep_branch_selectors.nil? ? nil : [*keep_branch_selectors].collect do | hash |
40
+ PactBroker::DB::Clean::BranchSelector.from_hash(hash)
41
+ end
42
+ end
43
+
34
44
  def rake_task &block
35
45
  namespace :pact_broker do
36
46
  namespace :db do
37
- desc "Clean unnecessary pacts and verifications from database"
47
+ desc "Clean unnecessary pacts, verifications and stale branches from database"
38
48
  task :clean do | _t, _args |
39
49
  instance_eval(&block)
40
50
 
@@ -61,10 +71,14 @@ module PactBroker
61
71
  output "Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash)
62
72
  end
63
73
 
74
+ output_keep_branch_selectors
75
+
64
76
  start_time = Time.now
65
77
  results = PactBroker::DB::CleanIncremental.call(database_connection,
66
78
  keep: keep_version_selectors,
79
+ keep_branches: keep_branch_selectors,
67
80
  limit: version_deletion_limit,
81
+ branch_limit: branch_deletion_limit,
68
82
  logger: logger,
69
83
  dry_run: dry_run
70
84
  )
@@ -109,6 +123,14 @@ module PactBroker
109
123
  logger ? logger.info("#{prefix}#{string}", payload) : puts("#{prefix}#{string} #{payload.to_json}")
110
124
  end
111
125
 
126
+ def output_keep_branch_selectors
127
+ if keep_branch_selectors.nil? || keep_branch_selectors.empty?
128
+ output "Stale branch cleanup is disabled (no keep_branch_selectors configured)"
129
+ else
130
+ output "Deleting stale branches that do not match the configured selectors (main branches are always kept)", keep_branch_selectors.collect(&:to_hash)
131
+ end
132
+ end
133
+
112
134
  def add_defaults_to_keep_selectors
113
135
  if keep_version_selectors.none?(&:currently_deployed?)
114
136
  selector = PactBroker::DB::Clean::Selector.new(deployed: true)
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = "2.119.0"
2
+ VERSION = "2.120.0"
3
3
  end
@@ -13,7 +13,7 @@ module PactBroker
13
13
  query = scope_for(Branch).where(pacticipant_id: pacticipant.id).select_all_qualified
14
14
  query = query.filter(:name, filter_options[:query_string]) if filter_options[:query_string]
15
15
  query
16
- .order(Sequel.desc(:created_at), Sequel.desc(:id))
16
+ .order(Sequel.desc(:updated_at), Sequel.desc(:id))
17
17
  .eager(*eager_load_associations)
18
18
  .all_with_pagination_options(pagination_options)
19
19
  end
@@ -21,7 +21,7 @@ module PactBroker
21
21
  # @return [Array<PactBroker::Contracts::Notice>]
22
22
  def branch_deletion_notices(pacticipant, exclude:)
23
23
  count = branch_repository.count_branches_to_delete(pacticipant, exclude: exclude)
24
- remaining = branch_repository.remaining_branches_after_future_deletion(pacticipant, exclude: exclude).sort_by(&:created_at).collect(&:name).join(", ")
24
+ remaining = branch_repository.remaining_branches_after_future_deletion(pacticipant, exclude: exclude).sort_by(&:updated_at).collect(&:name).join(", ")
25
25
  [PactBroker::Contracts::Notice.success(message("messages.branch.bulk_delete", count: count, pacticipant_name: pacticipant.name, remaining: remaining))]
26
26
  end
27
27
  end
@@ -34,6 +34,7 @@ module PactBroker
34
34
  branch_version = PactBroker::Versions::BranchVersion.new(version: version, branch: branch, auto_created: auto_created).insert_ignore
35
35
  PactBroker::Versions::BranchHead.new(branch: branch, branch_version: branch_version).upsert
36
36
  end
37
+ branch.update(updated_at: Sequel.datetime_class.now)
37
38
  pacticipant_service.maybe_set_main_branch(version.pacticipant, branch_name)
38
39
  branch_version
39
40
  end
data/pact_broker.gemspec CHANGED
@@ -49,17 +49,17 @@ Gem::Specification.new do |gem|
49
49
  gem.require_paths = ["lib"]
50
50
  gem.license = "MIT"
51
51
 
52
- gem.add_runtime_dependency "json", "~> 2.3"
52
+ gem.add_runtime_dependency "json", ">= 2.19.9"
53
53
  gem.add_runtime_dependency "psych", "~> 5.0"
54
54
  gem.add_runtime_dependency "roar", "~> 1.1"
55
55
  gem.add_runtime_dependency "dry-validation", "~> 1.8"
56
56
  gem.add_runtime_dependency "reform", "~> 2.6"
57
57
  gem.add_runtime_dependency "sequel", "~> 5.90"
58
- gem.add_runtime_dependency "webmachine", ">= 2.0.0.beta", "< 3.0"
58
+ gem.add_runtime_dependency "webmachine", "~> 2.0"
59
59
  gem.add_runtime_dependency "semver2", "~> 3.4.2"
60
60
  gem.add_runtime_dependency "rack", "~> 3.2"
61
- gem.add_runtime_dependency "redcarpet", ">= 3.5.1", "~>3.5"
62
- gem.add_runtime_dependency "pact-support" , ">= 1.21.2", "~> 1.21"
61
+ gem.add_runtime_dependency "redcarpet", "~> 3.5"
62
+ gem.add_runtime_dependency "pact-support", "~> 1.21"
63
63
  gem.add_runtime_dependency "haml", "~>5.0"
64
64
  gem.add_runtime_dependency "sucker_punch", "~>3.0"
65
65
  gem.add_runtime_dependency "rack-protection", "~> 4.1"
@@ -74,6 +74,7 @@ Gem::Specification.new do |gem|
74
74
  gem.add_runtime_dependency "mutex_m", "~> 0.3" # until as-notifications -> webmachine requires it
75
75
  gem.add_runtime_dependency "csv", "~> 3.0" # lib/pact_broker/api/decorators/relationships_csv_decorator.rb
76
76
  # dropped in ruby 3.5 stdlib
77
- gem.add_runtime_dependency "ostruct", "~> 0.5"
77
+ gem.add_runtime_dependency "ostruct", "~> 0.5"
78
+ gem.add_runtime_dependency "json_schemer", "~> 2.5"
78
79
  gem.add_development_dependency "pstore", "~> 0.1" # until webmachine requires it
79
80
  end
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.119.0
4
+ version: 2.120.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bethany Skurrie
@@ -15,16 +15,16 @@ dependencies:
15
15
  name: json
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '2.3'
20
+ version: 2.19.9
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '2.3'
27
+ version: 2.19.9
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: psych
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -99,22 +99,16 @@ dependencies:
99
99
  name: webmachine
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - ">="
103
- - !ruby/object:Gem::Version
104
- version: 2.0.0.beta
105
- - - "<"
102
+ - - "~>"
106
103
  - !ruby/object:Gem::Version
107
- version: '3.0'
104
+ version: '2.0'
108
105
  type: :runtime
109
106
  prerelease: false
110
107
  version_requirements: !ruby/object:Gem::Requirement
111
108
  requirements:
112
- - - ">="
113
- - !ruby/object:Gem::Version
114
- version: 2.0.0.beta
115
- - - "<"
109
+ - - "~>"
116
110
  - !ruby/object:Gem::Version
117
- version: '3.0'
111
+ version: '2.0'
118
112
  - !ruby/object:Gem::Dependency
119
113
  name: semver2
120
114
  requirement: !ruby/object:Gem::Requirement
@@ -147,9 +141,6 @@ dependencies:
147
141
  name: redcarpet
148
142
  requirement: !ruby/object:Gem::Requirement
149
143
  requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: 3.5.1
153
144
  - - "~>"
154
145
  - !ruby/object:Gem::Version
155
146
  version: '3.5'
@@ -157,9 +148,6 @@ dependencies:
157
148
  prerelease: false
158
149
  version_requirements: !ruby/object:Gem::Requirement
159
150
  requirements:
160
- - - ">="
161
- - !ruby/object:Gem::Version
162
- version: 3.5.1
163
151
  - - "~>"
164
152
  - !ruby/object:Gem::Version
165
153
  version: '3.5'
@@ -167,9 +155,6 @@ dependencies:
167
155
  name: pact-support
168
156
  requirement: !ruby/object:Gem::Requirement
169
157
  requirements:
170
- - - ">="
171
- - !ruby/object:Gem::Version
172
- version: 1.21.2
173
158
  - - "~>"
174
159
  - !ruby/object:Gem::Version
175
160
  version: '1.21'
@@ -177,9 +162,6 @@ dependencies:
177
162
  prerelease: false
178
163
  version_requirements: !ruby/object:Gem::Requirement
179
164
  requirements:
180
- - - ">="
181
- - !ruby/object:Gem::Version
182
- version: 1.21.2
183
165
  - - "~>"
184
166
  - !ruby/object:Gem::Version
185
167
  version: '1.21'
@@ -365,6 +347,20 @@ dependencies:
365
347
  - - "~>"
366
348
  - !ruby/object:Gem::Version
367
349
  version: '0.5'
350
+ - !ruby/object:Gem::Dependency
351
+ name: json_schemer
352
+ requirement: !ruby/object:Gem::Requirement
353
+ requirements:
354
+ - - "~>"
355
+ - !ruby/object:Gem::Version
356
+ version: '2.5'
357
+ type: :runtime
358
+ prerelease: false
359
+ version_requirements: !ruby/object:Gem::Requirement
360
+ requirements:
361
+ - - "~>"
362
+ - !ruby/object:Gem::Version
363
+ version: '2.5'
368
364
  - !ruby/object:Gem::Dependency
369
365
  name: pstore
370
366
  requirement: !ruby/object:Gem::Requirement
@@ -836,6 +832,7 @@ files:
836
832
  - lib/pact_broker/db.rb
837
833
  - lib/pact_broker/db/advisory_lock.rb
838
834
  - lib/pact_broker/db/clean.rb
835
+ - lib/pact_broker/db/clean/branch_selector.rb
839
836
  - lib/pact_broker/db/clean/selector.rb
840
837
  - lib/pact_broker/db/clean_incremental.rb
841
838
  - lib/pact_broker/db/data_migrations/create_branches.rb
@@ -1295,7 +1292,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1295
1292
  - !ruby/object:Gem::Version
1296
1293
  version: '0'
1297
1294
  requirements: []
1298
- rubygems_version: 4.0.10
1295
+ rubygems_version: 4.0.16
1299
1296
  specification_version: 4
1300
1297
  summary: See description
1301
1298
  test_files: []