diff --git a/lib/pact_broker/deployments/deployed_version.rb b/lib/pact_broker/deployments/deployed_version.rb index 4d6267f76..8178133ef 100644 --- a/lib/pact_broker/deployments/deployed_version.rb +++ b/lib/pact_broker/deployments/deployed_version.rb @@ -47,6 +47,10 @@ def order_by_date_desc def record_undeployed update(currently_deployed: false, undeployed_at: Sequel.datetime_class.now) end + + def version_number + version.number + end end end end diff --git a/lib/pact_broker/deployments/deployed_version_service.rb b/lib/pact_broker/deployments/deployed_version_service.rb index ec94f997a..c855c4d6b 100644 --- a/lib/pact_broker/deployments/deployed_version_service.rb +++ b/lib/pact_broker/deployments/deployed_version_service.rb @@ -35,6 +35,15 @@ def self.find_deployed_versions_for_environment(environment) .all end + def self.find_currently_deployed_versions_for_pacticipant(pacticipant) + DeployedVersion + .currently_deployed + .where(pacticipant_id: pacticipant.id) + .eager(:version) + .eager(:environment) + .all + end + def self.record_previous_version_undeployed(pacticipant, environment) DeployedVersion.last_deployed_version(pacticipant, environment)&.record_undeployed end diff --git a/lib/pact_broker/domain/webhook.rb b/lib/pact_broker/domain/webhook.rb index df187d5ab..fe587311f 100644 --- a/lib/pact_broker/domain/webhook.rb +++ b/lib/pact_broker/domain/webhook.rb @@ -3,6 +3,7 @@ require 'pact_broker/logging' require 'pact_broker/api/contracts/webhook_contract' require 'pact_broker/webhooks/http_request_with_redacted_headers' +require 'pact_broker/webhooks/pact_and_verification_parameters' module PactBroker module Domain @@ -96,6 +97,10 @@ def trigger_on_provider_verification_failed? events.any?(&:provider_verification_failed?) end + def expand_currently_deployed_provider_versions? + request.uses_parameter?(PactBroker::Webhooks::PactAndVerificationParameters::CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER) + end + private def execute_request(webhook_request) diff --git a/lib/pact_broker/test/http_test_data_builder.rb b/lib/pact_broker/test/http_test_data_builder.rb index 53367c3d3..1a7d4e5f0 100644 --- a/lib/pact_broker/test/http_test_data_builder.rb +++ b/lib/pact_broker/test/http_test_data_builder.rb @@ -60,6 +60,22 @@ def deploy_to_prod(pacticipant:, version:) self end + def record_deployment(pacticipant:, version:, environment_name:) + puts "Recoding deployment of #{pacticipant} version #{version} to #{environment_name}" + version_body = client.get("/pacticipants/#{encode(pacticipant)}/versions/#{encode(version)}").tap { |response| check_for_error(response) }.body + environment_relation = version_body["_links"]["pb:record-deployment"].find { |relation| relation["name"] == environment_name } + client.post(environment_relation["href"], { replacedPreviousDeployedVersion: true }).tap { |response| check_for_error(response) } + separate + self + end + + def create_environment(name:, production: false) + puts "Creating environment #{name}" + client.post("/environments", { name: name, displayName: name, production: production }).tap { |response| check_for_error(response) } + separate + self + end + def create_pacticipant(name) puts "Creating pacticipant with name #{name}" client.post("pacticipants", { name: name }).tap { |response| check_for_error(response) } @@ -157,7 +173,10 @@ def create_global_webhook_for_contract_changed(uuid: nil, url: "https://postman- }], "request" => { "method" => "POST", - "url" => url + "url" => url, + "body" => { + "deployedProviderVersion" => "${pactbroker.currentlyDeployedProviderVersionNumber}" + } } } path = "webhooks/#{uuid}" diff --git a/lib/pact_broker/webhooks/pact_and_verification_parameters.rb b/lib/pact_broker/webhooks/pact_and_verification_parameters.rb index 83976565b..db1892a86 100644 --- a/lib/pact_broker/webhooks/pact_and_verification_parameters.rb +++ b/lib/pact_broker/webhooks/pact_and_verification_parameters.rb @@ -14,6 +14,7 @@ class PactAndVerificationParameters CONSUMER_LABELS = 'pactbroker.consumerLabels' PROVIDER_LABELS = 'pactbroker.providerLabels' EVENT_NAME = 'pactbroker.eventName' + CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER = 'pactbroker.currentlyDeployedProviderVersionNumber' ALL = [ CONSUMER_NAME, @@ -28,7 +29,8 @@ class PactAndVerificationParameters BITBUCKET_VERIFICATION_STATUS, CONSUMER_LABELS, PROVIDER_LABELS, - EVENT_NAME + EVENT_NAME, + CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER ] def initialize(pact, trigger_verification, webhook_context) @@ -52,7 +54,8 @@ def to_hash BITBUCKET_VERIFICATION_STATUS => bitbucket_verification_status, CONSUMER_LABELS => pacticipant_labels(pact && pact.consumer), PROVIDER_LABELS => pacticipant_labels(pact && pact.provider), - EVENT_NAME => event_name + EVENT_NAME => event_name, + CURRENTLY_DEPLOYED_PROVIDER_VERSION_NUMBER => currently_deployed_provider_version_number } end @@ -123,6 +126,10 @@ def pacticipant_labels pacticipant def event_name webhook_context.fetch(:event_name) end + + def currently_deployed_provider_version_number + webhook_context[:currently_deployed_provider_version_number] || "" + end end end end diff --git a/lib/pact_broker/webhooks/service.rb b/lib/pact_broker/webhooks/service.rb index 39aa33da3..cc7c0ae52 100644 --- a/lib/pact_broker/webhooks/service.rb +++ b/lib/pact_broker/webhooks/service.rb @@ -14,6 +14,7 @@ require 'pact_broker/webhooks/execution_configuration' require 'pact_broker/messages' require 'pact_broker/webhooks/pact_and_verification_parameters' +require 'pact_broker/feature_toggle' module PactBroker module Webhooks @@ -128,28 +129,39 @@ def self.trigger_webhooks pact, verification, event_name, event_context, options webhook_execution_configuration = options.fetch(:webhook_execution_configuration).with_webhook_context(event_name: event_name) # bit messy to merge in base_url here, but easier than a big refactor base_url = options.fetch(:webhook_execution_configuration).webhook_context.fetch(:base_url) - run_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration)) + + run_webhooks_later(webhooks, pact, verification, event_name, event_context.merge(event_name: event_name, base_url: base_url), options.merge(webhook_execution_configuration: webhook_execution_configuration)) else logger.info "No enabled webhooks found for consumer \"#{pact.consumer.name}\" and provider \"#{pact.provider.name}\" and event #{event_name}" end end - def self.run_later webhooks, pact, verification, event_name, event_context, options - trigger_uuid = next_uuid + def self.run_webhooks_later webhooks, pact, verification, event_name, event_context, options webhooks.each do | webhook | - begin - triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, webhook, pact, verification, RESOURCE_CREATION, event_name, event_context) - logger.info "Scheduling job for webhook with uuid #{webhook.uuid}" - logger.debug "Schedule webhook with options #{options}" - job_data = { triggered_webhook: triggered_webhook }.deep_merge(options) - # Delay slightly to make sure the request transaction has finished before we execute the webhook - Job.perform_in(5, job_data) - rescue StandardError => e - logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e) + if PactBroker.feature_enabled?(:expand_currently_deployed_provider_versions) && webhook.expand_currently_deployed_provider_versions? + deployed_version_service.find_currently_deployed_versions_for_pacticipant(pact.provider).collect(&:version_number).uniq.each_with_index do | version_number, index | + schedule_webhook(webhook, pact, verification, event_name, event_context.merge(currently_deployed_provider_version_number: version_number), options, index * 5) + end + else + schedule_webhook(webhook, pact, verification, event_name, event_context, options) end end end + def self.schedule_webhook(webhook, pact, verification, event_name, event_context, options, extra_delay = 0) + begin + trigger_uuid = next_uuid + triggered_webhook = webhook_repository.create_triggered_webhook(trigger_uuid, webhook, pact, verification, RESOURCE_CREATION, event_name, event_context) + logger.info "Scheduling job for webhook with uuid #{webhook.uuid}, context: #{event_context}" + logger.debug "Schedule webhook with options #{options}" + job_data = { triggered_webhook: triggered_webhook }.deep_merge(options) + # Delay slightly to make sure the request transaction has finished before we execute the webhook + Job.perform_in(5 + extra_delay, job_data) + rescue StandardError => e + logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e) + end + end + def self.find_latest_triggered_webhooks_for_pact pact webhook_repository.find_latest_triggered_webhooks_for_pact pact end diff --git a/lib/pact_broker/webhooks/webhook_request_template.rb b/lib/pact_broker/webhooks/webhook_request_template.rb index 73c44b99d..6a0114413 100644 --- a/lib/pact_broker/webhooks/webhook_request_template.rb +++ b/lib/pact_broker/webhooks/webhook_request_template.rb @@ -62,6 +62,13 @@ def headers= headers @headers = Rack::Utils::HeaderHash.new(headers) end + def uses_parameter?(parameter_name) + !!body_string&.include?("${" + parameter_name + "}") + end + + def body_string + String === body ? body : body&.to_json + end def to_s "#{method.upcase} #{url}, username=#{username}, password=#{display_password}, headers=#{redacted_headers}, body=#{body}" diff --git a/script/reproduce-issue-expand-currently-deployed.rb b/script/reproduce-issue-expand-currently-deployed.rb new file mode 100755 index 000000000..c3137e17c --- /dev/null +++ b/script/reproduce-issue-expand-currently-deployed.rb @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby +begin + + $LOAD_PATH << "#{Dir.pwd}/lib" + require 'pact_broker/test/http_test_data_builder' + base_url = ENV['PACT_BROKER_BASE_URL'] || 'http://localhost:9292' + + td = PactBroker::Test::HttpTestDataBuilder.new(base_url) + td.delete_integration(consumer: "Foo", provider: "Bar") + .delete_integration(consumer: "foo-consumer", provider: "bar-provider") + .create_environment(name: "test") + .create_environment(name: "prod", production: true) + .publish_pact(consumer: "foo-consumer", consumer_version: "1", provider: "bar-provider", content_id: "111", tag: "main") + .get_pacts_for_verification( + enable_pending: true, + provider_version_tag: "main", + include_wip_pacts_since: "2020-01-01", + consumer_version_selectors: [{ tag: "main", latest: true }]) + .verify_pact( + index: 0, + provider_version_tag: "main", + provider_version: "1", + success: true + ) + .record_deployment(pacticipant: "bar-provider", version: "1", environment_name: "test") + .record_deployment(pacticipant: "bar-provider", version: "1", environment_name: "prod") + .record_deployment(pacticipant: "foo-consumer", version: "1", environment_name: "prod") + .get_pacts_for_verification( + enable_pending: true, + provider_version_tag: "main", + include_wip_pacts_since: "2020-01-01", + consumer_version_selectors: [{ tag: "main", latest: true }]) + .verify_pact( + index: 0, + provider_version_tag: "main", + provider_version: "2", + success: true + ) + .record_deployment(pacticipant: "bar-provider", version: "2", environment_name: "test") + .create_global_webhook_for_contract_changed(uuid: "7a5da39c-8e50-4cc9-ae16-dfa5be043e8c") + .publish_pact(consumer: "foo-consumer", consumer_version: "2", provider: "bar-provider", content_id: "222", tag: "main") + +rescue StandardError => e + puts "#{e.class} #{e.message}" + puts e.backtrace + exit 1 +end diff --git a/spec/lib/pact_broker/webhooks/service_spec.rb b/spec/lib/pact_broker/webhooks/service_spec.rb index 1b409c554..6c5595449 100644 --- a/spec/lib/pact_broker/webhooks/service_spec.rb +++ b/spec/lib/pact_broker/webhooks/service_spec.rb @@ -178,7 +178,11 @@ module Webhooks let(:consumer_version) { PactBroker::Domain::Version.new(number: '1.2.3') } let(:consumer) { PactBroker::Domain::Pacticipant.new(name: 'Consumer') } let(:provider) { PactBroker::Domain::Pacticipant.new(name: 'Provider') } - let(:webhooks) { [instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244')]} + let(:webhooks) { [webhook]} + let(:webhook) do + instance_double(PactBroker::Domain::Webhook, description: 'description', uuid: '1244', expand_currently_deployed_provider_versions?: expand_currently_deployed) + end + let(:expand_currently_deployed) { false } let(:triggered_webhook) { instance_double(PactBroker::Webhooks::TriggeredWebhook) } let(:webhook_execution_configuration) { double('webhook_execution_configuration', webhook_context: webhook_context) } let(:webhook_context) { { base_url: "http://example.org" } } @@ -206,8 +210,8 @@ module Webhooks end context "when webhooks are found" do - it "executes the webhook" do - expect(Service).to receive(:run_later).with(webhooks, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context, options) + it "schedules the webhook" do + expect(Service).to receive(:run_webhooks_later).with(webhooks, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context, options) subject end @@ -215,12 +219,39 @@ module Webhooks expect(webhook_execution_configuration).to receive(:with_webhook_context).with(event_name: PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED) subject end + + context "when there should be a webhook triggered for each currently deployed version" do + before do + allow(Service).to receive(:deployed_version_service).and_return(deployed_version_service) + allow(deployed_version_service).to receive(:find_currently_deployed_versions_for_pacticipant).and_return(currently_deployed_versions) + end + let(:expand_currently_deployed) { true } + let(:deployed_version_service) { class_double("PactBroker::Deployments::DeployedVersionService").as_stubbed_const } + let(:currently_deployed_version_1) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } + let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "2") } + let(:currently_deployed_versions) { [currently_deployed_version_1, currently_deployed_version_2] } + + it "schedules a triggered webhook for each currently deployed version" do + expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "1"), options, 0) + expect(Service).to receive(:schedule_webhook).with(webhook, pact, verification, PactBroker::Webhooks::WebhookEvent::CONTRACT_CONTENT_CHANGED, expected_event_context.merge(currently_deployed_provider_version_number: "2"), options, 5) + subject + end + + context "when the same version is deployed to multiple environments" do + let(:currently_deployed_version_2) { instance_double("PactBroker::Deployments::DeployedVersion", version_number: "1") } + + it "only triggers one webhook" do + expect(Service).to receive(:schedule_webhook).with(anything, anything, anything, anything, expected_event_context.merge(currently_deployed_provider_version_number: "1"), anything, 0) + subject + end + end + end end context "when no webhooks are found" do let(:webhooks) { [] } it "does nothing" do - expect(Service).to_not receive(:run_later) + expect(Service).to_not receive(:run_webhooks_later) subject end