Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webhooks): support ${pactbroker.currentlyDeployedProviderVersionNumber} in webhook templates #402

Merged
merged 1 commit into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/pact_broker/deployments/deployed_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions lib/pact_broker/deployments/deployed_version_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/pact_broker/domain/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 20 additions & 1 deletion lib/pact_broker/test/http_test_data_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -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}"
Expand Down
11 changes: 9 additions & 2 deletions lib/pact_broker/webhooks/pact_and_verification_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
36 changes: 24 additions & 12 deletions lib/pact_broker/webhooks/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/pact_broker/webhooks/webhook_request_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
47 changes: 47 additions & 0 deletions script/reproduce-issue-expand-currently-deployed.rb
Original file line number Diff line number Diff line change
@@ -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
39 changes: 35 additions & 4 deletions spec/lib/pact_broker/webhooks/service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }
Expand Down Expand Up @@ -206,21 +210,48 @@ 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

it "merges the event name in the options" do
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

Expand Down