pact_broker 2.69.0 → 2.70.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +62 -0
  3. data/CHANGELOG.md +16 -0
  4. data/DEVELOPER_SETUP.md +6 -0
  5. data/Dockerfile +4 -3
  6. data/Gemfile +4 -0
  7. data/ISSUES.md +23 -0
  8. data/config/database.yml +14 -0
  9. data/docker-compose-issue-repro.yml +11 -7
  10. data/lib/pact_broker/api/resources/clean.rb +36 -0
  11. data/lib/pact_broker/api/resources/group.rb +5 -1
  12. data/lib/pact_broker/db/clean.rb +4 -1
  13. data/lib/pact_broker/db/clean_incremental.rb +78 -0
  14. data/lib/pact_broker/domain/tag.rb +69 -8
  15. data/lib/pact_broker/domain/verification.rb +6 -0
  16. data/lib/pact_broker/domain/version.rb +35 -1
  17. data/lib/pact_broker/logging.rb +7 -0
  18. data/lib/pact_broker/matrix/unresolved_selector.rb +8 -0
  19. data/lib/pact_broker/pacts/all_pact_publications.rb +1 -3
  20. data/lib/pact_broker/pacts/pact_publication.rb +7 -1
  21. data/lib/pact_broker/pacts/repository.rb +4 -2
  22. data/lib/pact_broker/tasks/clean_task.rb +28 -12
  23. data/lib/pact_broker/test/http_test_data_builder.rb +52 -5
  24. data/lib/pact_broker/version.rb +1 -1
  25. data/lib/pact_broker/webhooks/triggered_webhook.rb +6 -0
  26. data/pact_broker.gemspec +1 -1
  27. data/script/docker/db-start.sh +1 -1
  28. data/script/reproduce-issue.rb +38 -15
  29. data/spec/fixtures/approvals/modifiable_resources.approved.json +3 -0
  30. data/spec/lib/pact_broker/api/resources/group_spec.rb +9 -7
  31. data/spec/lib/pact_broker/db/clean_incremental_spec.rb +93 -0
  32. data/spec/lib/pact_broker/domain/tag_spec.rb +46 -0
  33. data/spec/lib/pact_broker/domain/verification_spec.rb +13 -0
  34. data/spec/lib/pact_broker/domain/version_spec.rb +23 -0
  35. data/spec/lib/pact_broker/pacts/pact_publication_spec.rb +3 -1
  36. data/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb +14 -1
  37. metadata +20 -6
@@ -29,6 +29,12 @@ module PactBroker
29
29
  # Beware that when columns with the same name exist in both datasets
30
30
  # you may get the wrong column back in your model.
31
31
 
32
+ def delete
33
+ require 'pact_broker/webhooks/triggered_webhook'
34
+ PactBroker::Webhooks::TriggeredWebhook.where(verification: self).delete
35
+ super
36
+ end
37
+
32
38
  def consumer consumer_name
33
39
  where(name_like(:consumer_name, consumer_name))
34
40
  end
@@ -5,6 +5,21 @@ require 'pact_broker/tags/tag_with_latest_flag'
5
5
 
6
6
  module PactBroker
7
7
  module Domain
8
+
9
+ # Same attributes as PactBroker::Tags::TagWithLatestFlag
10
+ class EagerTagWithLatestFlag < SimpleDelegator
11
+ attr_reader :latest
12
+
13
+ def initialize(tag, latest)
14
+ super(tag)
15
+ @latest = latest
16
+ end
17
+
18
+ def latest?
19
+ latest
20
+ end
21
+ end
22
+
8
23
  class Version < Sequel::Model
9
24
  plugin :timestamps, update_on_create: true
10
25
  plugin :insert_ignore, identifying_columns: [:pacticipant_id, :number]
@@ -13,7 +28,21 @@ module PactBroker
13
28
  one_to_many :pact_publications, order: :revision_number, class: "PactBroker::Pacts::PactPublication", key: :consumer_version_id
14
29
  associate(:many_to_one, :pacticipant, :class => "PactBroker::Domain::Pacticipant", :key => :pacticipant_id, :primary_key => :id)
15
30
  one_to_many :tags, :reciprocal => :version, order: :created_at
16
- one_to_many :tags_with_latest_flag, class: "PactBroker::Tags::TagWithLatestFlag", key: :version_id, primary_key: :id
31
+
32
+ one_to_many :tags_with_latest_flag, :class => "PactBroker::Tags::TagWithLatestFlag", primary_keys: [:id], key: [:version_id], :eager_loader=>(proc do |eo_opts|
33
+ tags_for_versions = PactBroker::Domain::Tag.where(version_id: eo_opts[:key_hash][:id].keys)
34
+ latest_tag_for_pacticipants = PactBroker::Domain::Tag.latest_tags_for_pacticipant_ids(eo_opts[:rows].collect(&:pacticipant_id)).all
35
+
36
+ eo_opts[:rows].each{|row| row.associations[:tags_with_latest_flag] = [] }
37
+
38
+ tags_for_versions.each do | tag |
39
+ latest = latest_tag_for_pacticipants.any? { |latest_tag| latest_tag.name == tag.name && latest_tag.version_id == tag.version_id }
40
+ eo_opts[:id_map][tag.version_id].each do | version |
41
+ version.associations[:tags_with_latest_flag] << EagerTagWithLatestFlag.new(tag, latest)
42
+ end
43
+ end
44
+ end)
45
+
17
46
 
18
47
  dataset_module do
19
48
  include PactBroker::Repositories::Helpers
@@ -50,6 +79,11 @@ module PactBroker
50
79
  end
51
80
 
52
81
  def delete
82
+ require 'pact_broker/pacts/pact_publication'
83
+ require 'pact_broker/domain/verification'
84
+ require 'pact_broker/domain/tag'
85
+
86
+ PactBroker::Domain::Verification.where(provider_version: self).delete
53
87
  PactBroker::Pacts::PactPublication.where(consumer_version: self).delete
54
88
  PactBroker::Domain::Tag.where(version: self).delete
55
89
  super
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'semantic_logger'
2
3
 
3
4
  module PactBroker
@@ -50,3 +51,9 @@ module PactBroker
50
51
 
51
52
  include Logging
52
53
  end
54
+
55
+ # Hide the annoying warnings from Declarative gem. Need to upgrade Representable.
56
+ def Warning.warn(w)
57
+ # super calls the original behavior, which is printing to $stderr
58
+ super unless (w.start_with?("[Declarative] Defaults#merge!") || w.include?("URI.unescape is obsolete"))
59
+ end
@@ -1,10 +1,18 @@
1
+ require 'pact_broker/hash_refinements'
2
+
1
3
  module PactBroker
2
4
  module Matrix
3
5
  class UnresolvedSelector < Hash
6
+ using PactBroker::HashRefinements
7
+
4
8
  def initialize(params = {})
5
9
  merge!(params)
6
10
  end
7
11
 
12
+ def self.from_hash(hash)
13
+ new(hash.symbolize_keys.snakecase_keys.slice(:pacticipant_name, :pacticipant_version_number, :latest, :tag, :max_age))
14
+ end
15
+
8
16
  def pacticipant_name
9
17
  self[:pacticipant_name]
10
18
  end
@@ -112,9 +112,7 @@ module PactBroker
112
112
  end
113
113
 
114
114
  def head_tag_names
115
- # Avoid circular dependency
116
- require 'pact_broker/pacts/latest_tagged_pact_publications'
117
- @head_tag_names ||= LatestTaggedPactPublications.where(id: id).select(:tag_name).collect{|t| t[:tag_name]}
115
+ @head_tag_names ||= PactBroker::Domain::Tag.head_tags_for_pact_publication(self).collect(&:name)
118
116
  end
119
117
 
120
118
  def to_domain_with_content
@@ -101,6 +101,12 @@ module PactBroker
101
101
  self
102
102
  end
103
103
  end
104
+
105
+ def delete
106
+ require 'pact_broker/webhooks/triggered_webhook'
107
+ PactBroker::Webhooks::TriggeredWebhook.where(pact_publication: self).delete
108
+ super
109
+ end
104
110
  end
105
111
 
106
112
  def before_create
@@ -111,7 +117,7 @@ module PactBroker
111
117
  # The names of the tags for which this pact is the latest pact with that tag
112
118
  # (ie. it is not necessarily the pact for the latest consumer version with the given tag)
113
119
  def head_tag_names
114
- LatestTaggedPactPublications.where(id: id).select(:tag_name).collect{|t| t[:tag_name]}
120
+ @head_tag_names ||= PactBroker::Domain::Tag.head_tags_for_pact_publication(self).collect(&:name)
115
121
  end
116
122
 
117
123
  def consumer_version_tags
@@ -17,6 +17,7 @@ require 'pact_broker/repositories/helpers'
17
17
  require 'pact_broker/pacts/selected_pact'
18
18
  require 'pact_broker/pacts/selector'
19
19
  require 'pact_broker/pacts/selectors'
20
+ require 'pact_broker/feature_toggle'
20
21
 
21
22
  module PactBroker
22
23
  module Pacts
@@ -214,14 +215,15 @@ module PactBroker
214
215
  .where(name: provider_tags_names)
215
216
  .all
216
217
 
217
- wip_pacts.collect do | pact|
218
+ provider_version_count = scope_for(PactBroker::Domain::Version).where(pacticipant: provider).count
218
219
 
220
+ wip_pacts.collect do | pact|
219
221
  pending_tag_names = find_provider_tags_for_which_pact_publication_id_is_pending(pact, successfully_verified_head_pact_publication_ids_for_each_provider_tag)
220
222
  pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact, provider_tag_collection)
221
223
 
222
224
  pre_existing_pending_tags = pending_tag_names & pre_existing_tag_names
223
225
 
224
- if pre_existing_pending_tags.any?
226
+ if pre_existing_pending_tags.any? || (PactBroker.feature_enabled?(:experimental_no_provider_versions_makes_all_head_pacts_wip) && provider_version_count == 0)
225
227
  selectors = Selectors.create_for_latest_of_each_tag(pact.head_tag_names)
226
228
  VerifiablePact.new(pact.to_domain, selectors, true, pre_existing_pending_tags, [], true)
227
229
  end
@@ -3,12 +3,23 @@ module PactBroker
3
3
  class CleanTask < ::Rake::TaskLib
4
4
 
5
5
  attr_accessor :database_connection
6
- attr_accessor :keep
6
+ attr_reader :keep_version_selectors
7
+ attr_accessor :version_deletion_limit
7
8
 
8
9
  def initialize &block
10
+ require 'pact_broker/db/clean_incremental'
11
+ @version_deletion_limit = 1000
12
+ @keep_version_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_SELECTORS
9
13
  rake_task &block
10
14
  end
11
15
 
16
+ def keep_version_selectors=(keep_version_selectors)
17
+ require 'pact_broker/matrix/unresolved_selector'
18
+ @keep_version_selectors = [*keep_version_selectors].collect do | hash |
19
+ PactBroker::Matrix::UnresolvedSelector.from_hash(hash)
20
+ end
21
+ end
22
+
12
23
  def rake_task &block
13
24
  namespace :pact_broker do
14
25
  namespace :db do
@@ -17,23 +28,28 @@ module PactBroker
17
28
 
18
29
  instance_eval(&block)
19
30
 
20
- require 'pact_broker/db/clean'
21
- require 'pact_broker/matrix/unresolved_selector'
31
+ require 'pact_broker/db/clean_incremental'
32
+ require 'pact_broker/error'
22
33
  require 'yaml'
34
+ require 'benchmark'
23
35
 
24
- keep_selectors = nil
25
- if keep
26
- keep_selectors = [*keep].collect do | hash |
27
- PactBroker::Matrix::UnresolvedSelector.new(hash)
28
- end
36
+ raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit
37
+
38
+ if keep_version_selectors.nil? || keep_version_selectors.empty?
39
+ raise PactBroker::Error.new("You must specify which versions to keep")
40
+ else
41
+ puts "Deleting oldest #{version_deletion_limit} versions, keeping versions that match the following selectors: #{keep_version_selectors}..."
29
42
  end
30
- # TODO time it
31
- results = PactBroker::DB::Clean.call(database_connection, keep: keep_selectors)
32
- puts results.to_yaml
43
+
44
+ start_time = Time.now
45
+ results = PactBroker::DB::CleanIncremental.call(database_connection, keep: keep_version_selectors, limit: version_deletion_limit)
46
+ end_time = Time.now
47
+ elapsed_seconds = (end_time - start_time).to_i
48
+ puts results.to_yaml.gsub("---", "\nResults (#{elapsed_seconds} seconds)\n-------")
33
49
  end
34
50
  end
35
51
  end
36
52
  end
37
53
  end
38
54
  end
39
- end
55
+ end
@@ -14,8 +14,10 @@ module PactBroker
14
14
  @client = Faraday.new(url: pact_broker_base_url) do |faraday|
15
15
  faraday.request :json
16
16
  faraday.response :json, :content_type => /\bjson$/
17
- faraday.response :logger, ::Logger.new($stdout), headers: false do | logger |
18
- logger.filter(/(Authorization: ).*/,'\1[REMOVED]')
17
+ if ENV['DEBUG'] == 'true'
18
+ faraday.response :logger, ::Logger.new($stdout), headers: false do | logger |
19
+ logger.filter(/(Authorization: ).*/,'\1[REMOVED]')
20
+ end
19
21
  end
20
22
  # faraday.headers['Authorization'] = "Bearer #{pactflow_api_token}"
21
23
  faraday.adapter Faraday.default_adapter
@@ -27,6 +29,10 @@ module PactBroker
27
29
  self
28
30
  end
29
31
 
32
+ def separate
33
+ puts "\n=============================================================\n\n"
34
+ end
35
+
30
36
  def create_tagged_pacticipant_version(pacticipant:, version:, tag:)
31
37
  [*tag].each do | tag |
32
38
  create_tag(pacticipant: pacticipant, version: version, tag: tag)
@@ -40,6 +46,13 @@ module PactBroker
40
46
  self
41
47
  end
42
48
 
49
+ def deploy_to_prod(pacticipant:, version:)
50
+ puts "Deploying #{pacticipant} version #{version} to prod"
51
+ create_tag(pacticipant: pacticipant, version: version, tag: "prod")
52
+ separate
53
+ self
54
+ end
55
+
43
56
  def publish_pact(consumer: last_consumer_name, consumer_version:, provider: last_provider_name, content_id:, tag:)
44
57
  @last_consumer_name = consumer
45
58
  @last_provider_name = provider
@@ -49,11 +62,11 @@ module PactBroker
49
62
  create_tag(pacticipant: consumer, version: consumer_version, tag: tag)
50
63
  end
51
64
 
52
-
53
65
  content = generate_content(consumer, provider, content_id)
54
66
  puts "Publishing pact for consumer #{consumer} version #{consumer_version} and provider #{provider}"
55
67
  pact_path = "/pacts/provider/#{encode(provider)}/consumer/#{encode(consumer)}/version/#{encode(consumer_version)}"
56
68
  @publish_pact_response = client.put(pact_path, content)
69
+ separate
57
70
  self
58
71
  end
59
72
 
@@ -65,13 +78,19 @@ module PactBroker
65
78
  includePendingStatus: enable_pending,
66
79
  includeWipPactsSince: include_wip_pacts_since
67
80
  }
81
+ puts body.to_yaml
82
+ puts ""
68
83
  @pacts_for_verification_response = client.post("/pacts/provider/#{encode(provider)}/for-verification", body)
84
+
85
+ print_pacts_for_verification
86
+ separate
69
87
  self
70
88
  end
71
89
 
72
90
  def print_pacts_for_verification
73
- puts "Pacts for verification:"
74
- @pacts_for_verification_response.body["_embedded"]["pacts"].each do | pact |
91
+ pacts = @pacts_for_verification_response.body["_embedded"]["pacts"]
92
+ puts "Pacts for verification (#{pacts.count}):"
93
+ pacts.each do | pact |
75
94
  puts({
76
95
  "url" => pact["_links"]["self"]["href"],
77
96
  "wip" => pact["verificationProperties"]["wip"],
@@ -81,13 +100,41 @@ module PactBroker
81
100
  self
82
101
  end
83
102
 
103
+ def verify_pact(index: 0, success:, provider: last_provider_name, provider_version_tag: , provider_version: )
104
+ pact_to_verify = @pacts_for_verification_response.body["_embedded"]["pacts"][index]
105
+ raise "No pact found to verify at index #{index}" unless pact_to_verify
106
+ url_of_pact_to_verify = pact_to_verify["_links"]["self"]["href"]
107
+ pact_response = client.get(url_of_pact_to_verify)
108
+ verification_results_url = pact_response.body["_links"]["pb:publish-verification-results"]["href"]
109
+
110
+ results = {
111
+ success: success,
112
+ testResults: [],
113
+ providerApplicationVersion: provider_version
114
+ }
115
+ puts "\nPublishing verification"
116
+ response = client.post(verification_results_url, results.to_json)
117
+ separate
118
+ self
119
+ end
120
+
84
121
  def print_pacts_for_verification_response
85
122
  puts @pacts_for_verification_response.body
86
123
  self
87
124
  end
88
125
 
126
+ def can_i_deploy(pacticipant:, version:, to:)
127
+ can_i_deploy_response = client.get("/can-i-deploy", { pacticipant: pacticipant, version: version, to: to} )
128
+ can = !!(can_i_deploy_response.body['summary'] || {})['deployable']
129
+ puts "can-i-deploy #{pacticipant} version #{version} to #{to}: #{can ? 'yes' : 'no'}"
130
+ puts (can_i_deploy_response.body['summary'] || can_i_deploy_response.body).to_yaml
131
+ separate
132
+ self
133
+ end
134
+
89
135
  def delete_integration(consumer:, provider:)
90
136
  client.delete("/integrations/provider/#{encode(provider)}/consumer/#{encode(consumer)}")
137
+ separate
91
138
  self
92
139
  end
93
140
 
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = '2.69.0'
2
+ VERSION = '2.70.0'
3
3
  end
@@ -21,6 +21,12 @@ module PactBroker
21
21
  dataset_module do
22
22
  include PactBroker::Repositories::Helpers
23
23
 
24
+ def delete
25
+ require 'pact_broker/webhooks/execution'
26
+ PactBroker::Webhooks::Execution.where(triggered_webhook: self).delete
27
+ super
28
+ end
29
+
24
30
  def retrying
25
31
  where(status: STATUS_RETRYING)
26
32
  end
@@ -53,7 +53,7 @@ Gem::Specification.new do |gem|
53
53
  gem.add_runtime_dependency 'semver2', '~> 3.4.2'
54
54
  gem.add_runtime_dependency 'rack', '>= 2.2.3', '~> 2.2'
55
55
  gem.add_runtime_dependency 'redcarpet', '>=3.3.2', '~>3.3'
56
- gem.add_runtime_dependency 'pact-support', '1.15.0'
56
+ gem.add_runtime_dependency 'pact-support' , '~> 1.16', '>= 1.16.4'
57
57
  gem.add_runtime_dependency 'padrino-core', '>= 0.14.3', '~> 0.14'
58
58
  gem.add_runtime_dependency 'sinatra', '>= 2.0.8.1', '< 3.0'
59
59
  gem.add_runtime_dependency 'haml', '~>5.0'
@@ -4,4 +4,4 @@ docker run --name pact-broker-postgres \
4
4
  -e POSTGRES_PASSWORD=postgres \
5
5
  -p 5432:5432 \
6
6
  -v $PWD:/data \
7
- -d postgres:10
7
+ -d postgres:12
@@ -2,19 +2,42 @@
2
2
 
3
3
  $LOAD_PATH << "#{Dir.pwd}/lib"
4
4
 
5
- require 'pact_broker/test/http_test_data_builder'
5
+ begin
6
+
7
+ require 'pact_broker/test/http_test_data_builder'
8
+ base_url = ENV['PACT_BROKER_BASE_URL'] || 'http://localhost:9292'
9
+
10
+ td = PactBroker::Test::HttpTestDataBuilder.new(base_url, { })
11
+ td.delete_integration(consumer: "MyConsumer", provider: "MyProvider")
12
+ .can_i_deploy(pacticipant: "MyProvider", version: "1", to: "prod")
13
+ .can_i_deploy(pacticipant: "MyConsumer", version: "1", to: "prod")
14
+ .publish_pact(consumer: "MyConsumer", consumer_version: "1", provider: "MyProvider", content_id: "111", tag: "feature/a")
15
+ .can_i_deploy(pacticipant: "MyProvider", version: "1", to: "prod")
16
+ .get_pacts_for_verification(
17
+ enable_pending: true,
18
+ provider_version_tag: "main",
19
+ include_wip_pacts_since: "2020-01-01",
20
+ consumer_version_selectors: [{ tag: "main", latest: true }])
21
+ .verify_pact(
22
+ index: 0,
23
+ provider_version_tag: "main",
24
+ provider_version: "1",
25
+ success: false
26
+ )
27
+ .get_pacts_for_verification(
28
+ enable_pending: true,
29
+ provider_version_tag: "main",
30
+ include_wip_pacts_since: "2020-01-01",
31
+ consumer_version_selectors: [{ tag: "main", latest: true }])
32
+ .can_i_deploy(pacticipant: "MyProvider", version: "1", to: "prod")
33
+ .can_i_deploy(pacticipant: "MyConsumer", version: "1", to: "prod")
34
+ .deploy_to_prod(pacticipant: "MyProvider", version: "1")
35
+ .can_i_deploy(pacticipant: "MyConsumer", version: "1", to: "prod")
36
+ .deploy_to_prod(pacticipant: "MyConsumer", version: "1")
37
+
38
+ rescue StandardError => e
39
+ puts "#{e.class} #{e.message}"
40
+ puts e.backtrace
41
+ exit 1
42
+ end
6
43
 
7
- td = PactBroker::Test::HttpTestDataBuilder.new('http://pact-broker:9292', { })
8
- td.delete_integration(consumer: "Foo", provider: "Bar")
9
- .create_tagged_pacticipant_version(pacticipant: "Bar", version: "1", tag: "master")
10
- .sleep
11
- .publish_pact(consumer: "Foo", consumer_version: "1", provider: "Bar", content_id: "111", tag: "master")
12
- .sleep
13
- .publish_pact(consumer_version: "2", content_id: "222", tag: "feat-x")
14
- .sleep
15
- .get_pacts_for_verification(
16
- enable_pending: true,
17
- provider_version_tag: "master",
18
- include_wip_pacts_since: "2020-01-01",
19
- consumer_version_selectors: [{ tag: "master", latest: true }])
20
- .print_pacts_for_verification
@@ -3,6 +3,9 @@
3
3
  {
4
4
  "resource_class_name": "PactBroker::Api::Resources::AllWebhooks"
5
5
  },
6
+ {
7
+ "resource_class_name": "PactBroker::Api::Resources::Clean"
8
+ },
6
9
  {
7
10
  "resource_class_name": "PactBroker::Api::Resources::ErrorTest"
8
11
  },