diff --git a/.github/workflows/release_gem.yml b/.github/workflows/release_gem.yml
index 2ee570fec..85d037f7a 100644
--- a/.github/workflows/release_gem.yml
+++ b/.github/workflows/release_gem.yml
@@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify ${{ matrix.repository }} of gem release
- uses: peter-evans/repository-dispatch@v2
+ uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GHTOKENNOTIFYPBRELEASED }}
repository: ${{ matrix.repository }}
diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml
index c916c25a8..5b0a7fe60 100644
--- a/.github/workflows/trigger_pact_docs_update.yml
+++ b/.github/workflows/trigger_pact_docs_update.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger docs.pact.io update workflow
- uses: peter-evans/repository-dispatch@v2
+ uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }}
repository: pact-foundation/docs.pact.io
diff --git a/.ruby-version b/.ruby-version
index a3ec5a4bd..351227fca 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.2
+3.2.4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b1e07079..bd3ee1c49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,109 @@
+
+### v2.111.0 (2024-07-26)
+
+#### Features
+
+* add new label api (#703) ([ff3f84e2](/../../commit/ff3f84e2))
+* search pacticipants by display_name ([c5945801](/../../commit/c5945801))
+
+#### Bug Fixes
+
+* **docs**
+ * Update OAS with correct ref to Notice schema ([6729b7f8](/../../commit/6729b7f8))
+
+
+### v2.110.0 (2024-04-02)
+
+#### Features
+
+* reduce contention when updating the contract_data_updated_at field for integrations (#671) ([ff72d03c](/../../commit/ff72d03c))
+* support consumer version selector for all branches (#667) ([34334ca8](/../../commit/34334ca8))
+
+* **clean**
+ * use postgres advisory locks to ensure only one process can run a clean at a time (#672) ([637c25fa](/../../commit/637c25fa))
+
+#### Bug Fixes
+
+* use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch ([14148a34](/../../commit/14148a34))
+* optimise WIP pacts by using branch/tag heads (#668) ([871209e1](/../../commit/871209e1))
+* improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch ([f9705583](/../../commit/f9705583))
+
+
+### v2.109.1 (2024-02-21)
+
+#### Bug Fixes
+
+* improve performance for 'pacts for verification' queries ([299a6abe](/../../commit/299a6abe))
+* correct spelling in message when pact is modified ([ae62ae7a](/../../commit/ae62ae7a))
+
+
+### v2.109.0 (2024-02-01)
+
+#### Features
+
+* use SemanticLogger for Padrino (#662) ([5d9d7002](/../../commit/5d9d7002))
+* improve performance of publication for very large pacts by calculating the content SHA only once per request ([a947e409](/../../commit/a947e409))
+
+#### Bug Fixes
+
+* pass in environment to environment policy when getting an individual environment ([5c386a43](/../../commit/5c386a43))
+* Dockerfile to reduce vulnerabilities (#650) ([9aaa3484](/../../commit/9aaa3484))
+
+
+### v2.108.0 (2024-01-05)
+
+#### Features
+
+* bulk delete branches (#652) ([14ac33c8](/../../commit/14ac33c8))
+* add latest version for branch endpoint (#644) ([c216bec8](/../../commit/c216bec8))
+* add no-cache header ([9a637327](/../../commit/9a637327))
+* suppport `page` + `size` as pagination params (#642) ([c71089fe](/../../commit/c71089fe))
+* do not include pb:record-deployment or pb:record-release relations for versions embedded in resources ([2f43590c](/../../commit/2f43590c))
+* remove status from individual error in problem+error response ([a4b3ec58](/../../commit/a4b3ec58))
+* add version_id indexes to deployed_versions and released_versions ([00fc7d10](/../../commit/00fc7d10))
+* add endpoint to list branches for a pacticipant (#638) ([ff7e3a53](/../../commit/ff7e3a53))
+* stop running tests for ruby 2.7 ([034aba3b](/../../commit/034aba3b))
+* update sinatra and rack-protection to ~> 3.0 ([92ebbdd3](/../../commit/92ebbdd3))
+* add branch endpoint supporting GET and DELETE (#635) ([1bb6088d](/../../commit/1bb6088d))
+* optimise matrix by applying specified limit to pact publications before joining to verifications ([c61c324e](/../../commit/c61c324e))
+* optimise matrix query when selectors with pacticipant names only are used ([b98f5d1a](/../../commit/b98f5d1a))
+* include environment name in pact metadata ([e120c4e7](/../../commit/e120c4e7))
+* improve wording of 'no version exits' messaging in can-i-deploy response ([9529c679](/../../commit/9529c679))
+* improve performance of matrix when multiple selectors are specified (#631) ([58a28604](/../../commit/58a28604))
+* add pagination parameter validation for paginated endpoints. (#626) ([abb0a1c6](/../../commit/abb0a1c6))
+* add endpoint to list pacticipant versions by branch ([9b4e3f61](/../../commit/9b4e3f61))
+* add endpoint to return latest pact for consumer, provider and consumer branch ([f77086ef](/../../commit/f77086ef))
+* update required ruby version from 2.2 to 2.7 ([f1b1e906](/../../commit/f1b1e906))
+* add pagination and filtering for integrations endpoint ([68d7cf30](/../../commit/68d7cf30))
+* add contract_data_updated_at to integrations table to speed up dashboard query (#617) ([e43c10f2](/../../commit/e43c10f2))
+* support setting feature toggles via individual environment variables (#609) ([be7d9d52](/../../commit/be7d9d52))
+
+* **metrics**
+ * hardcode matrix count to -1 as calculating it causes performance issues and it has no meaning ([62e121b8](/../../commit/62e121b8))
+
+* **matrix**
+ * optimise identification of the 'latest tag' ([824c516a](/../../commit/824c516a))
+
+#### Bug Fixes
+
+* **metrics**
+ * correct the query for pactRevisionsPerConsumerVersion ([f76b9935](/../../commit/f76b9935))
+
+* fix performance issues due to contention in the integrations table when publishing a large number of contracts (> 20) per request, in parallel (#654) ([321a2291](/../../commit/321a2291))
+* raise 404 on paths with missing path segments (#648) ([930b45cd](/../../commit/930b45cd))
+* do not error when no environment is found by name ([d1501618](/../../commit/d1501618))
+* ensure pact associations are eager loaded when finding a single pact ([c98abda6](/../../commit/c98abda6))
+* gracefully handle validating an array when a hash is expected ([b26ddb46](/../../commit/b26ddb46))
+* fix error occuring when can-i-deploy badge is requested and no version is found ([db7dee3a](/../../commit/db7dee3a))
+* fix bug in error handling for 'can-i-deploy branch to environment' badge ([c23beb6b](/../../commit/c23beb6b))
+* improve performance of network diagram (#614) ([ffd3ec4b](/../../commit/ffd3ec4b))
+* fix error raised when attempting to log warning when webhook_redact_sensitive_data is set to false ([9b66270e](/../../commit/9b66270e))
+* gracefully handle execution of webhooks that are deleted between execution attempts (#613) ([1127b41f](/../../commit/1127b41f))
+* add extra validation to ensure parsed content is a hash when publishing pacts ([913e0a52](/../../commit/913e0a52))
+
+* **matrix**
+ * return only most recent row missing verification when latestby=cp ([b7550e53](/../../commit/b7550e53))
+
### v2.107.1 (2023-05-02)
diff --git a/Dockerfile b/Dockerfile
index d30b4ad1f..34402ad3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:3.2.1-alpine3.17
+FROM ruby:3.2.4-alpine3.18
WORKDIR /home
diff --git a/Gemfile b/Gemfile
index 5b0a2a94c..467bcc449 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,8 +2,6 @@ source "https://rubygems.org"
gemspec
-# While https://github.com/brandonhilkert/sucker_punch/pull/253 is outstanding
-gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2"
gem "rake", "~>12.3.3"
gem "sqlite3", "~>1.3"
diff --git a/README.md b/README.md
index b6d61974a..baecc231d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
[![Gem Version](https://badge.fury.io/rb/pact_broker.svg)](http://badge.fury.io/rb/pact_broker)
![Build status](https://github.com/pact-foundation/pact_broker/workflows/Test/badge.svg)
[![Join the chat at https://pact-foundation.slack.com/](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack)](https://slack.pact.io)
- [![security](https://hakiri.io/github/pact-foundation/pact_broker/master.svg)](https://hakiri.io/github/pact-foundation/pact_broker/master)
The Pact Broker is an application for sharing of consumer driven contracts and verification results. It is optimised for use with "pacts" (contracts created by the [Pact][pact-docs] framework), but can be used for any type of contract that can be serialized to JSON.
@@ -151,7 +150,7 @@ You can use the [Pact Broker Docker image][docker] or [Terraform on AWS][terrafo
* Are you sure you don't just want to use the [Pact Broker Docker image][docker]? No Docker at your company yet? Ah well, keep reading.
* Create a PostgreSQL database.
- * To ensure you're on a supported version of the database that you choose, check the [travis.yml][travisyml] file to see which versions we're currently running our tests against.
+ * To ensure you're on a supported version of the database that you choose, check the [.github/workflows/test.yml](.github/workflows/test.yml) file to see which versions we're currently running our tests against.
* MySQL was supported for the native Ruby application until around 2021, but the official `pactfoundation/pact-broker` Docker image does not support it. New features will not be optimised for MySQL, and some new features may not even be supported on it (eg. the database clean feature).
* You'll find a sample database creation script in the [example/config.ru](https://github.com/pact-foundation/pact_broker/blob/master/example/config.ru).
* Install ruby 2.7 and the latest version of bundler (if you've come this far, I'm assuming you know how to do both of these. Did I mention there was a [Docker][docker] image?)
diff --git a/docker-compose-dev-postgres.yml b/docker-compose-dev-postgres.yml
index b992d2d30..e48828360 100644
--- a/docker-compose-dev-postgres.yml
+++ b/docker-compose-dev-postgres.yml
@@ -34,7 +34,7 @@ services:
- ./Rakefile:/home/Rakefile
shell:
- image: ruby:2.5.3-alpine
+ image: ruby:3.2.4-alpine
depends_on:
- pact-broker
entrypoint: /bin/sh
diff --git a/docs/developer/design_pattern_for_eager_loading_collections.md b/docs/developer/design_pattern_for_eager_loading_collections.md
new file mode 100644
index 000000000..ebc759490
--- /dev/null
+++ b/docs/developer/design_pattern_for_eager_loading_collections.md
@@ -0,0 +1,23 @@
+# Design pattern for eager loading collections
+
+For collection resources (eg. `/versions` ), associations included in the items (eg. branch versions) must be eager loaded for performance reasons.
+
+The responsiblities of each class used to render a collection resource are as follows:
+
+* collection decorator (eg. `VersionsDecorator`) - delegate each item in the collection to be rendered by the decorator for the individual item, render pagination links
+* item decorator (eg. `VersionDecorator`) - render the JSON for each item
+* resource (eg. `PactBroker::Api::Resources::Versions`) - coordinate between, and delegate to, the service and the decorator
+* service (eg. `PactBroker::Versions::Service`) - just delegate to repository, as there is no business logic required
+* repository (eg. `PactBroker::Versions::Repository`) - load the domain objects from the database
+
+If the associations for a model are not eager loaded, then each individual association will be lazy loaded when the decorator for the item calls the association method to render it. This results in at least ` * ` calls to the database, and potentially more if any of the associations have their own associations that are required to render the item. This can cause significant performance issues.
+
+To efficiently render a collection resource, associations must be eager loaded when the collection items are loaded from the database in the repository. Since the repository method for loading the collection may be used in multiple places, and the eager loaded associations required for each of those places may be different (some may not require any associations to be eager loaded), we do not want to hard code the repository to load a fixed set of associations. The list of associations to eager load is therefore passed in to the repository finder method as an argument `eager_load_associations`.
+
+The decorator is the class that knows what associations are going to be called on the model to render the JSON, so following the design guideline of "put things together that change together", the best place for the declaration of "what associations should be eager loaded for this decorator" is in the decorator itself. The `PactBroker::Api::Decorators::BaseDecorator` has a default implementation of this method called `eager_load_associations` which attempts to automatically identify the required associations, but this can be overridden when necessary.
+
+We can therefore add the following responsiblities to our previous list:
+
+* item decorator - return a list of all the associations (including nested associations) that should be eager loaded in order to render its item
+* repository - eager load the associations that have been passed into it
+* resource - pass in the eager load associations to the repository from the decorator
diff --git a/MATRIX.md b/docs/developer/matrix.md
similarity index 100%
rename from MATRIX.md
rename to docs/developer/matrix.md
diff --git a/docs/developer/rack.md b/docs/developer/rack.md
new file mode 100644
index 000000000..ff3d5e613
--- /dev/null
+++ b/docs/developer/rack.md
@@ -0,0 +1,11 @@
+# Rack
+
+https://medium.com/quick-code/rack-middleware-vs-rack-application-vs-rack-the-gem-vs-rack-the-architecture-912cd583ed24
+https://github.com/rack/rack/blob/main/SPEC.rdoc
+https://www.rubyguides.com/2018/09/rack-middleware/
+
+
+* Responds to `call`
+* Accepts a hash of parameters
+* Returns an array where the first item is the http status, the second is a hash of headers, and the third is an object that responds to `each` (or `call`) that provides the body (99% of the time it's an array of length 1 with a string)
+
diff --git a/example/Gemfile b/example/Gemfile
index 4d05a180a..3c9aa1199 100644
--- a/example/Gemfile
+++ b/example/Gemfile
@@ -4,4 +4,3 @@ gem "pact_broker" # Would pin this to a known minor version when using properly
gem "puma", "~> 5.3" # Can be replaced with your choice of application server
gem "sqlite3", "~>1.3" # Sqlite is just for testing. Replace this with "pg" for production.
# gem "pg" # Production database gem
-gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2"
diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb
index 3954a1ce4..a9e5f9ef8 100644
--- a/lib/pact_broker/api.rb
+++ b/lib/pact_broker/api.rb
@@ -5,7 +5,6 @@
require "pact_broker/api/contracts"
require "pact_broker/application_context"
require "pact_broker/feature_toggle"
-require "pact_broker/initializers/subscriptions"
module Webmachine
class Request
@@ -86,6 +85,9 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_
add ["pacticipants", :pacticipant_name], Api::Resources::Pacticipant, {resource_name: "pacticipant"}
add ["pacticipants", :pacticipant_name, "labels", :label_name], Api::Resources::Label, {resource_name: "pacticipant_label"}
+ # Labels
+ add ["labels"], Api::Resources::Labels, {resource_name: "labels"}
+
# Versions
add ["pacticipants", :pacticipant_name, "versions"], Api::Resources::Versions, {resource_name: "pacticipant_versions"}
add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions"], Api::Resources::BranchVersions, {resource_name: "pacticipant_branch_versions"}
@@ -93,10 +95,12 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_
add ["pacticipants", :pacticipant_name, "latest-version", :tag], Api::Resources::LatestVersion, {resource_name: "latest_tagged_pacticipant_version"}
add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to], Api::Resources::CanIDeployPacticipantVersionByTagToTag, { resource_name: "can_i_deploy_latest_tagged_version_to_tag" }
add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to, "badge"], Api::Resources::CanIDeployPacticipantVersionByTagToTagBadge, { resource_name: "can_i_deploy_latest_tagged_version_to_tag_badge" }
+ add ["pacticipants", :pacticipant_name, "main-branch", "can-i-merge", "badge"], Api::Resources::CanIMergeBadge, { resource_name: "can_i_merge_badge" }
add ["pacticipants", :pacticipant_name, "latest-version"], Api::Resources::LatestVersion, {resource_name: "latest_pacticipant_version"}
add ["pacticipants", :pacticipant_name, "versions", :pacticipant_version_number, "tags", :tag_name], Api::Resources::Tag, {resource_name: "pacticipant_version_tag"}
add ["pacticipants", :pacticipant_name, "branches"], Api::Resources::PacticipantBranches, {resource_name: "pacticipant_branches"}
add ["pacticipants", :pacticipant_name, "branches", :branch_name], Api::Resources::Branch, { resource_name: "branch" }
+ add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version"], Api::Resources::LatestVersion, { resource_name: "latest_pacticipant_version_for_branch" }
add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions", :version_number], Api::Resources::BranchVersion, { resource_name: "branch_version" }
add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironment, { resource_name: "can_i_deploy_latest_branch_version_to_environment" }
add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name, "badge"], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironmentBadge, { resource_name: "can_i_deploy_latest_branch_version_to_environment_badge" }
diff --git a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb
index dda334b59..6ce831fc4 100644
--- a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb
+++ b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb
@@ -13,7 +13,7 @@ class ConsumerVersionSelectorContract < BaseContract
json do
optional(:mainBranch).filled(included_in?: [true])
optional(:tag).filled(:str?)
- optional(:branch).filled(:str?)
+ optional(:branch).filled { str? | eql?(true) }
optional(:matchingBranch).filled(included_in?: [true])
optional(:latest).filled(included_in?: [true, false])
optional(:fallbackTag).filled(:str?)
diff --git a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb
index bf2761b3d..7f3e69a48 100644
--- a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb
+++ b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb
@@ -5,8 +5,13 @@ module Api
module Contracts
class PaginationQueryParamsSchema < BaseContract
params do
+ # legacy format
optional(:pageNumber).maybe(:integer).value(gteq?: 1)
optional(:pageSize).maybe(:integer).value(gteq?: 1)
+
+ # desired format
+ optional(:page).maybe(:integer).value(gteq?: 1)
+ optional(:size).maybe(:integer).value(gteq?: 1)
end
end
end
diff --git a/lib/pact_broker/api/decorators/base_decorator.rb b/lib/pact_broker/api/decorators/base_decorator.rb
index ec5a801c4..b14524cc8 100644
--- a/lib/pact_broker/api/decorators/base_decorator.rb
+++ b/lib/pact_broker/api/decorators/base_decorator.rb
@@ -1,7 +1,6 @@
require "roar/decorator"
require "roar/json/hal"
require "pact_broker/api/pact_broker_urls"
-require "pact_broker/api/decorators/decorator_context"
require "pact_broker/api/decorators/format_date_time"
require "pact_broker/string_refinements"
require "pact_broker/hash_refinements"
@@ -35,7 +34,16 @@ def self.property(name, options={}, &block)
end
end
- # Returns the names of the model associations to eager load for use with this decorator
+ # Returns the names of the model associations to eager load for use with this decorator.
+ # The default implementation attempts to do an "auto detect" of the associations.
+ # For single item decorators, it attempts to identify the attributes that are themselves models.
+ # For collection decorators, it delegates to the eager_load_associations
+ # method of the single item decorator used to decorate the collection.
+ #
+ # The "auto detect" logic can only go so far. It cannot identify when a child object needs its own
+ # child object(s) to render the attribute.
+ # This method should be overridden when the "auto detect" logic cannot identify the correct associations
+ # to load. eg VersionDecorator
# @return [Array]
def self.eager_load_associations
if is_collection_resource?
diff --git a/lib/pact_broker/api/decorators/branch_decorator.rb b/lib/pact_broker/api/decorators/branch_decorator.rb
index 915a12975..9f7fee210 100644
--- a/lib/pact_broker/api/decorators/branch_decorator.rb
+++ b/lib/pact_broker/api/decorators/branch_decorator.rb
@@ -18,7 +18,7 @@ class BranchDecorator < BaseDecorator
link "pb:latest-version" do | user_options |
{
title: "Latest version for branch",
- href: branch_versions_url(represented, user_options.fetch(:base_url)) + "?pageSize=1"
+ href: latest_version_for_branch_url(represented, user_options.fetch(:base_url))
}
end
diff --git a/lib/pact_broker/api/decorators/decorator_context.rb b/lib/pact_broker/api/decorators/decorator_context.rb
deleted file mode 100644
index 2bf4eded7..000000000
--- a/lib/pact_broker/api/decorators/decorator_context.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module PactBroker
- module Api
- module Decorators
- class DecoratorContext < Hash
- attr_reader :base_url, :resource_url, :resource_title, :env, :query_string, :request_url
-
- def initialize base_url, resource_url, env, options = {}
- @base_url = self[:base_url] = base_url
- @resource_url = self[:resource_url] = resource_url
- @resource_title = self[:resource_title] = options[:resource_title]
- @env = self[:env] = env
- @query_string = self[:query_string] = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil)
- @request_url = self[:request_url] = query_string ? resource_url + "?" + query_string : resource_url
- merge!(options)
- end
-
- def to_s
- "DecoratorContext #{super}"
- end
- end
- end
- end
-end
diff --git a/lib/pact_broker/api/decorators/decorator_context_creator.rb b/lib/pact_broker/api/decorators/decorator_context_creator.rb
index 6d9441a3c..c712941d3 100644
--- a/lib/pact_broker/api/decorators/decorator_context_creator.rb
+++ b/lib/pact_broker/api/decorators/decorator_context_creator.rb
@@ -1,11 +1,56 @@
-require "pact_broker/api/decorators/decorator_context"
+# Builds the Hash that is passed into the Decorator as the `user_options`. It contains the request details, rack env, the (optional) title
+# and anything else that is required by the decorator to render the resource (eg. the pacticipant that the versions belong to)
module PactBroker
module Api
module Decorators
class DecoratorContextCreator
+
+ # @param [PactBroker::BaseResource] the Pact Broker webmachine resource
+ # @param [Hash] options any extra options that need to be passed through to the decorator.
+ # @return [Hash] decorator_context
+
+ # decorator_context [String] :base_url
+ # The location where the Pact Broker is hosted.
+ # eg. http://some.host:9292/pact_broker
+ # Always present
+
+ # decorator_context [String] :resource_url
+ # The resource URL without any query string.
+ # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions
+ # Always present
+
+ # decorator_context [String] :query_string
+ # The query string.
+ # "page=1&size=50"
+ # May be empty
+
+ # decorator_context [String] :request_url
+ # The full request URL.
+ # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions?page=1&size=50
+ # Always present
+
+ # decorator_context [Hash] :env
+ # The rack env.
+ # Always present
+
+ # decorator_context [Hash] :resource_title
+ # eg. "Pacticipant versions for Foo"
+ # Optional
+ # Used when a single decorator is being used for multiple resources and the title needs to be
+ # set from the resource.
+
def self.call(resource, options)
- Decorators::DecoratorContext.new(resource.base_url, resource.resource_url, resource.request.env, options)
+ env = resource.request.env
+ decorator_context = {}
+ decorator_context[:base_url] = resource.base_url
+ decorator_context[:resource_url] = resource.resource_url
+ decorator_context[:query_string] = query_string = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil)
+ decorator_context[:request_url] = query_string ? resource.resource_url + "?" + query_string : resource.resource_url
+ decorator_context[:env] = env
+ decorator_context[:resource_title] = options[:resource_title]
+ decorator_context.merge!(options)
+ decorator_context
end
end
end
diff --git a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb
index 61b0e1d18..647f7a504 100644
--- a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb
+++ b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb
@@ -1,11 +1,11 @@
require "pact_broker/api/decorators/base_decorator"
-require "pact_broker/api/decorators/deployed_version_decorator"
+require "pact_broker/api/decorators/embedded_deployed_version_decorator"
module PactBroker
module Api
module Decorators
class DeployedVersionsDecorator < BaseDecorator
- collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::DeployedVersionDecorator
+ collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedDeployedVersionDecorator
link :self do | context |
href = append_query_if_present(context[:resource_url], context[:query_string])
diff --git a/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb
new file mode 100644
index 000000000..26be2573e
--- /dev/null
+++ b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb
@@ -0,0 +1,30 @@
+require "pact_broker/api/decorators/base_decorator"
+require "pact_broker/api/decorators/embedded_pacticipant_decorator"
+require "pact_broker/api/decorators/embedded_version_decorator"
+require "pact_broker/api/decorators/environment_decorator"
+
+module PactBroker
+ module Api
+ module Decorators
+ class EmbeddedDeployedVersionDecorator < BaseDecorator
+ property :uuid
+ property :currently_deployed, camelize: true
+ property :target, camelize: true # deprecated
+ property :applicationInstance, getter: lambda { |_| target }
+ property :undeployedAt, getter: lambda { |_| undeployed_at ? FormatDateTime.call(undeployed_at) : nil }, writeable: false
+
+ property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) }
+ property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) }
+ property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) }
+
+ include Timestamps
+
+ link :self do | user_options |
+ {
+ href: deployed_version_url(represented, user_options.fetch(:base_url))
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb
new file mode 100644
index 000000000..d98cd97fc
--- /dev/null
+++ b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb
@@ -0,0 +1,27 @@
+require "pact_broker/api/decorators/base_decorator"
+require "pact_broker/api/decorators/embedded_pacticipant_decorator"
+require "pact_broker/api/decorators/embedded_version_decorator"
+require "pact_broker/api/decorators/environment_decorator"
+
+module PactBroker
+ module Api
+ module Decorators
+ class EmbeddedReleasedVersionDecorator < BaseDecorator
+ property :uuid
+ property :currently_supported, camelize: true
+ include Timestamps
+ property :supportEndedAt, getter: lambda { |_| support_ended_at ? FormatDateTime.call(support_ended_at) : nil }, writeable: false
+
+ property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) }
+ property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) }
+ property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) }
+
+ link :self do | user_options |
+ {
+ href: released_version_url(represented, user_options.fetch(:base_url))
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/decorators/extended_pact_decorator.rb b/lib/pact_broker/api/decorators/extended_pact_decorator.rb
index aa5568c7b..23daac697 100644
--- a/lib/pact_broker/api/decorators/extended_pact_decorator.rb
+++ b/lib/pact_broker/api/decorators/extended_pact_decorator.rb
@@ -1,5 +1,10 @@
require "pact_broker/api/decorators/pact_decorator"
+# Pactflow notes:
+# This decorator was added for Pactflow, but we needed to change it so much that there
+# is a separate class in pact_broker_fork/lib/pactflow/api/decorators/extended_pact_decorator.rb now
+# and this one isn't used.
+
module PactBroker
module Api
module Decorators
@@ -47,4 +52,4 @@ def head_tags
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/pact_broker/api/decorators/label_decorator.rb b/lib/pact_broker/api/decorators/label_decorator.rb
index 009df5d1c..098c0bcc1 100644
--- a/lib/pact_broker/api/decorators/label_decorator.rb
+++ b/lib/pact_broker/api/decorators/label_decorator.rb
@@ -11,20 +11,26 @@ class LabelDecorator < BaseDecorator
include Timestamps
- link :self do | options |
- {
- title: "Label",
- name: represented.name,
- href: label_url(represented, options[:base_url])
- }
- end
+ # This method is overridden to conditionally render the links based on the user_options
+ def to_hash(options)
+ hash = super
+
+ unless options.dig(:user_options, :hide_label_decorator_links)
+ hash[:_links] = {
+ self: {
+ title: "Label",
+ name: represented.name,
+ href: label_url(represented, options.dig(:user_options, :base_url))
+ },
+ pacticipant: {
+ title: "Pacticipant",
+ name: represented.pacticipant.name,
+ href: pacticipant_url(options.dig(:user_options, :base_url), represented.pacticipant)
+ }
+ }
+ end
- link :pacticipant do | options |
- {
- title: "Pacticipant",
- name: represented.pacticipant.name,
- href: pacticipant_url(options.fetch(:base_url), represented.pacticipant)
- }
+ hash
end
end
end
diff --git a/lib/pact_broker/api/decorators/labels_decorator.rb b/lib/pact_broker/api/decorators/labels_decorator.rb
new file mode 100644
index 000000000..b668f1c92
--- /dev/null
+++ b/lib/pact_broker/api/decorators/labels_decorator.rb
@@ -0,0 +1,23 @@
+require_relative "base_decorator"
+require_relative "pagination_links"
+require_relative "label_decorator"
+require "pact_broker/domain/label"
+
+module PactBroker
+ module Api
+ module Decorators
+ class LabelsDecorator < BaseDecorator
+ collection :entries, :as => :labels, :class => PactBroker::Domain::Label, :extend => PactBroker::Api::Decorators::LabelDecorator, embedded: true
+
+ include PaginationLinks
+
+ link :self do | options |
+ {
+ title: "Labels",
+ href: options.fetch(:resource_url)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/decorators/notices_decorator.rb b/lib/pact_broker/api/decorators/notices_decorator.rb
new file mode 100644
index 000000000..fe46659de
--- /dev/null
+++ b/lib/pact_broker/api/decorators/notices_decorator.rb
@@ -0,0 +1,11 @@
+require_relative "base_decorator"
+
+module PactBroker
+ module Api
+ module Decorators
+ class NoticesDecorator < BaseDecorator
+ property :entries, as: :notices, getter: ->(represented:, **) { represented.collect(&:to_h) }
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/decorators/pacticipant_decorator.rb b/lib/pact_broker/api/decorators/pacticipant_decorator.rb
index c13814743..b2c97eb0b 100644
--- a/lib/pact_broker/api/decorators/pacticipant_decorator.rb
+++ b/lib/pact_broker/api/decorators/pacticipant_decorator.rb
@@ -20,7 +20,7 @@ class PacticipantDecorator < BaseDecorator
property :main_branch
property :latest_version, as: :latestVersion, :class => PactBroker::Domain::Version, extend: PactBroker::Api::Decorators::EmbeddedVersionDecorator, embedded: true, writeable: false
- collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true
+ collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true, writeable: false
include Timestamps
diff --git a/lib/pact_broker/api/decorators/pagination_links.rb b/lib/pact_broker/api/decorators/pagination_links.rb
index eba47eeae..39b00091a 100644
--- a/lib/pact_broker/api/decorators/pagination_links.rb
+++ b/lib/pact_broker/api/decorators/pagination_links.rb
@@ -24,7 +24,7 @@ module PaginationLinks
represented.respond_to?(:page_count) &&
represented.current_page < represented.page_count
{
- href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page + 1}",
+ href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page + 1}",
title: "Next page"
}
@@ -34,7 +34,7 @@ module PaginationLinks
link :previous do | context |
if represented.respond_to?(:first_page?) && !represented.first_page?
{
- href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page - 1}",
+ href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page - 1}",
title: "Previous page"
}
end
diff --git a/lib/pact_broker/api/decorators/publish_contract_decorator.rb b/lib/pact_broker/api/decorators/publish_contract_decorator.rb
index 48e3c40f7..ec336372f 100644
--- a/lib/pact_broker/api/decorators/publish_contract_decorator.rb
+++ b/lib/pact_broker/api/decorators/publish_contract_decorator.rb
@@ -11,8 +11,13 @@ class PublishContractDecorator < BaseDecorator
property :provider_name
property :specification
property :content_type
- property :decoded_content
+ property :decoded_content, setter: -> (fragment:, represented:, user_options:, **) {
+ represented.decoded_content = fragment
+ # Set the pact version sha when we set the content
+ represented.pact_version_sha = user_options.fetch(:sha_generator).call(fragment)
+ }
property :on_conflict, default: "overwrite"
+
end
end
end
diff --git a/lib/pact_broker/api/decorators/released_versions_decorator.rb b/lib/pact_broker/api/decorators/released_versions_decorator.rb
index d21e94ab5..083a9598a 100644
--- a/lib/pact_broker/api/decorators/released_versions_decorator.rb
+++ b/lib/pact_broker/api/decorators/released_versions_decorator.rb
@@ -1,11 +1,11 @@
require "pact_broker/api/decorators/base_decorator"
-require "pact_broker/api/decorators/released_version_decorator"
+require "pact_broker/api/decorators/embedded_released_version_decorator"
module PactBroker
module Api
module Decorators
class ReleasedVersionsDecorator < BaseDecorator
- collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::ReleasedVersionDecorator
+ collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedReleasedVersionDecorator
link :self do | context |
href = append_query_if_present(context[:resource_url], context[:query_string])
diff --git a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb
index a2f92f220..f43f76077 100644
--- a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb
+++ b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb
@@ -3,7 +3,7 @@
module PactBroker
module Api
module Decorators
- class RuntimeErrorProblemJSONDecorator
+ class RuntimeErrorProblemJsonDecorator
# @param message [String]
def initialize(message)
diff --git a/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb
new file mode 100644
index 000000000..c416ca779
--- /dev/null
+++ b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb
@@ -0,0 +1,38 @@
+require_relative "base_decorator"
+
+module PactBroker
+ module Api
+ module Decorators
+ class TriggeredWebhookLogsDecorator < BaseDecorator
+ class WebhookExecutionDecorator < BaseDecorator
+ property :success
+ property :logs
+ property :created_at, as: :createdAt
+ end
+
+
+ nested :triggeredWebhook, embedded: true do
+ property :uuid
+ end
+
+ collection :webhook_executions, as: :executions, :class => PactBroker::Webhooks::Execution, :extend => WebhookExecutionDecorator
+
+ link :self do | options |
+ {
+ title: "Triggered webhook logs",
+ href: options[:resource_url]
+ }
+ end
+
+ link :'pb:webhook' do | context |
+ if represented.webhook
+ {
+ href: webhook_url(represented.webhook.uuid, context[:base_url]),
+ title: "Webhook"
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb
index b033a606b..7b94dbc59 100644
--- a/lib/pact_broker/api/decorators/version_decorator.rb
+++ b/lib/pact_broker/api/decorators/version_decorator.rb
@@ -1,5 +1,6 @@
require_relative "base_decorator"
require_relative "embedded_tag_decorator"
+require_relative "embedded_branch_version_decorator"
module PactBroker
module Api
@@ -16,11 +17,26 @@ class VersionDecorator < BaseDecorator
include Timestamps
+ # Returns the list of associations that must be eager loaded to efficiently render a version
+ # when this decorator is used in a collection (eg. VersionsDecorator)
+ # The associations that need to be eager loaded for the VersionDecorator
+ # are hand coded
+ # @return
+ def self.eager_load_associations
+ [
+ :pacticipant,
+ :pact_publications,
+ { branch_versions: [:version, :branch_head, { branch: :pacticipant }] },
+ { tags: :head_tag }
+ ]
+ end
+
link :self do | options |
{
title: "Version",
name: represented.number,
- href: version_url(options.fetch(:base_url), represented)
+ # This decorator is used for multiple Version resources, so dynamically fetch the current resource URL
+ href: options.fetch(:resource_url)
}
end
@@ -58,7 +74,7 @@ class VersionDecorator < BaseDecorator
end
links :'pb:record-deployment' do | context |
- context.fetch(:environments, []).collect do | environment |
+ context[:environments]&.collect do | environment |
{
title: "Record deployment to #{environment.display_name}",
name: environment.name,
@@ -68,7 +84,7 @@ class VersionDecorator < BaseDecorator
end
links :'pb:record-release' do | context |
- context.fetch(:environments, []).collect do | environment |
+ context[:environments]&.collect do | environment |
{
title: "Record release to #{environment.display_name}",
name: environment.name,
diff --git a/lib/pact_broker/api/decorators/versions_decorator.rb b/lib/pact_broker/api/decorators/versions_decorator.rb
index 5637f7b50..3cb016e61 100644
--- a/lib/pact_broker/api/decorators/versions_decorator.rb
+++ b/lib/pact_broker/api/decorators/versions_decorator.rb
@@ -6,13 +6,23 @@ module PactBroker
module Api
module Decorators
class VersionsDecorator < BaseDecorator
+ class VersionInCollectionDecorator < PactBroker::Api::Decorators::VersionDecorator
+ # VersionDecorator has a dynamic self URL, depending which path the Version resource is mounted at.
+ # Hardcode the URL of the embedded Versions in this collection to use the canonical URL with the version number.
+ link :self do | user_options |
+ {
+ title: "Version",
+ name: represented.number,
+ href: version_url(user_options.fetch(:base_url), represented)
+ }
+ end
+ end
- collection :entries, as: :versions, embedded: true, :extend => PactBroker::Api::Decorators::VersionDecorator
+ collection :entries, as: :versions, embedded: true, :extend => VersionInCollectionDecorator
link :self do | user_options |
- href = append_query_if_present(user_options[:resource_url], user_options[:query_string])
{
- href: href,
+ href: user_options.fetch(:request_url),
title: user_options[:resource_title] || "All application versions of #{user_options[:pacticipant_name]}"
}
end
diff --git a/lib/pact_broker/api/decorators/webhook_decorator.rb b/lib/pact_broker/api/decorators/webhook_decorator.rb
index aad276dac..348b70931 100644
--- a/lib/pact_broker/api/decorators/webhook_decorator.rb
+++ b/lib/pact_broker/api/decorators/webhook_decorator.rb
@@ -16,7 +16,7 @@ class WebhookEventDecorator < BaseDecorator
property :name
end
- property :uuid
+ property :uuid, writeable: false
property :description, getter: lambda { |represented:, **| represented.display_description }
diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb
index 3ce937d9d..c6bd1d72b 100644
--- a/lib/pact_broker/api/pact_broker_urls.rb
+++ b/lib/pact_broker/api/pact_broker_urls.rb
@@ -243,6 +243,10 @@ def branch_version_url(branch_version, base_url = "")
"#{branch_versions_url(branch_version.branch, base_url)}/#{url_encode(branch_version.version_number)}"
end
+ def latest_version_for_branch_url(branch, base_url = "")
+ "#{branch_url(branch, base_url)}/latest-version"
+ end
+
def templated_tag_url_for_pacticipant pacticipant_name, base_url = ""
pacticipant_url_from_params({ pacticipant_name: pacticipant_name }, base_url) + "/versions/{version}/tags/{tag}"
end
@@ -283,7 +287,7 @@ def webhooks_url base_url
"#{base_url}/webhooks"
end
- def webhook_url uuid, base_url
+ def webhook_url uuid, base_url = ""
"#{base_url}/webhooks/#{uuid}"
end
diff --git a/lib/pact_broker/api/paths.rb b/lib/pact_broker/api/paths.rb
index f1ab12e38..81ceb8443 100644
--- a/lib/pact_broker/api/paths.rb
+++ b/lib/pact_broker/api/paths.rb
@@ -3,11 +3,12 @@ module Api
module Paths
PACT_BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze
MATRIX_BADGE_PATH = %r{^/matrix/provider/[^/]+/latest/[^/]+/consumer/[^/]+/latest/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
+ CAN_I_MERGE_BADGE_PATH = %r{^/pacticipants/[^/]+/main-branch/can-i-merge/badge(?:\.[A-Za-z]+)?$}.freeze
CAN_I_DEPLOY_TAG_BADGE_PATH = %r{^/pacticipants/[^/]+/latest-version/[^/]+/can-i-deploy/to/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH = %r{^/pacticipants/[^/]+/branches/[^/]+/latest-version/can-i-deploy/to-environment/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze
VERIFICATION_RESULTS = %r{^/pacts/provider/[^/]+/consumer/[^/]+/pact-version/[^/]+/verification-results/[^/]+}
- BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH]
+ BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH, CAN_I_MERGE_BADGE_PATH]
extend self
diff --git a/lib/pact_broker/api/resources/after_reply.rb b/lib/pact_broker/api/resources/after_reply.rb
new file mode 100644
index 000000000..cbf7da715
--- /dev/null
+++ b/lib/pact_broker/api/resources/after_reply.rb
@@ -0,0 +1,15 @@
+require "pact_broker/async/after_reply"
+
+module PactBroker
+ module Api
+ module Resources
+ module AfterReply
+
+ # @param [Callable] block the block to execute after the response has been sent to the user.
+ def after_reply(&block)
+ PactBroker::Async::AfterReply.new(request.env).execute(&block)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/resources/all_webhooks.rb b/lib/pact_broker/api/resources/all_webhooks.rb
index 95b492bee..d8d4f544e 100644
--- a/lib/pact_broker/api/resources/all_webhooks.rb
+++ b/lib/pact_broker/api/resources/all_webhooks.rb
@@ -38,7 +38,7 @@ def to_json
end
def from_json
- saved_webhook = webhook_service.create(next_uuid, webhook, consumer, provider)
+ saved_webhook = webhook_service.create(next_uuid, webhook, webhook_consumer, webhook_provider)
response.body = decorator_class(:webhook_decorator).new(saved_webhook).to_json(**decorator_options)
end
@@ -60,11 +60,11 @@ def schema
api_contract_class(:webhook_contract)
end
- def consumer
+ def webhook_consumer
webhook.consumer&.name ? pacticipant_service.find_pacticipant_by_name(webhook.consumer.name) : nil
end
- def provider
+ def webhook_provider
webhook.provider&.name ? pacticipant_service.find_pacticipant_by_name(webhook.provider.name) : nil
end
diff --git a/lib/pact_broker/api/resources/base_resource.rb b/lib/pact_broker/api/resources/base_resource.rb
index 1b4f5da4c..c91468496 100644
--- a/lib/pact_broker/api/resources/base_resource.rb
+++ b/lib/pact_broker/api/resources/base_resource.rb
@@ -207,7 +207,7 @@ def invalid_json?
def find_pacticipant name, role
pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant |
if pacticipant.nil?
- set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not_found", status: 404)
+ set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not-found", status: 404)
end
end
end
diff --git a/lib/pact_broker/api/resources/branch_versions.rb b/lib/pact_broker/api/resources/branch_versions.rb
index 6bb1b10ea..0a25d9d7a 100644
--- a/lib/pact_broker/api/resources/branch_versions.rb
+++ b/lib/pact_broker/api/resources/branch_versions.rb
@@ -31,7 +31,7 @@ def to_json
end
def versions
- @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options)
+ @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
end
def policy_name
diff --git a/lib/pact_broker/api/resources/can_i_merge_badge.rb b/lib/pact_broker/api/resources/can_i_merge_badge.rb
new file mode 100644
index 000000000..964b74957
--- /dev/null
+++ b/lib/pact_broker/api/resources/can_i_merge_badge.rb
@@ -0,0 +1,36 @@
+require "pact_broker/api/resources/base_resource"
+require "pact_broker/api/resources/badge_methods"
+
+module PactBroker
+ module Api
+ module Resources
+ class CanIMergeBadge < BaseResource
+ include BadgeMethods # This module contains all necessary webmachine methods for badge implementation
+
+ def badge_url
+ if pacticipant.nil? # pacticipant method is defined in BaseResource
+ # if the pacticipant is nil, we return an error badge url
+ badge_service.error_badge_url("pacticipant", "not found")
+ elsif version.nil?
+ # when there is no main branch version, we return an error badge url
+ badge_service.error_badge_url("main branch version", "not found")
+ else
+ # we call badge_service to build the badge url
+ badge_service.can_i_merge_badge_url(deployable: results)
+ end
+ end
+
+ private
+
+ def results
+ # can_i_merge returns true or false if the main branch version is compatible with all the integrations
+ @results ||= matrix_service.can_i_merge(pacticipant: pacticipant, latest_main_branch_version: version)
+ end
+
+ def version
+ @version ||= version_service.find_latest_version_from_main_branch(pacticipant)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb
index 1d56f9f7b..b2de08e25 100644
--- a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb
+++ b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb
@@ -21,7 +21,7 @@ def resource_exists?
end
def to_json
- decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title))
+ decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version]))
end
def policy_name
diff --git a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb
index 95dd313b6..d34e6c317 100644
--- a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb
+++ b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb
@@ -21,7 +21,7 @@ def resource_exists?
end
def to_json
- decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title))
+ decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version]))
end
def policy_name
diff --git a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb
index 9e79be72a..00f8c3e0c 100644
--- a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb
+++ b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb
@@ -78,7 +78,7 @@ def application_instance
end
def title
- "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number}"
+ "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number} in environment #{environment.display_name}"
end
end
end
diff --git a/lib/pact_broker/api/resources/environment.rb b/lib/pact_broker/api/resources/environment.rb
index 80b1d7707..5f92cd843 100644
--- a/lib/pact_broker/api/resources/environment.rb
+++ b/lib/pact_broker/api/resources/environment.rb
@@ -42,6 +42,10 @@ def policy_name
:'deployments::environment'
end
+ def policy_record
+ environment
+ end
+
def to_json
decorator_class(:environment_decorator).new(environment).to_json(**decorator_options)
end
diff --git a/lib/pact_broker/api/resources/error_response_generator.rb b/lib/pact_broker/api/resources/error_response_generator.rb
index d372c5d86..c4f757b40 100644
--- a/lib/pact_broker/api/resources/error_response_generator.rb
+++ b/lib/pact_broker/api/resources/error_response_generator.rb
@@ -3,8 +3,9 @@
require "pact_broker/errors"
require "pact_broker/messages"
-# Generates the response headers and body for use when there is an unexpected
-# error when executing a Webmachine resource request.
+# Generates the response headers and body for use when there is a runtime
+# error in the business logic (services and repositories) when executing a Webmachine resource request.
+# Obfuscates any exception messages that might expose vulnerablities in production.
# Uses the Accept header to determine whether to return application/problem+json
# or application/hal+json, for backwards compatibility.
# In the next major version of the Pact Broker, all error responses
@@ -58,7 +59,7 @@ def self.display_message(error, message, obfuscated_message)
end
private_class_method def self.problem_json_response_body(message, env)
- PactBroker::Api::Decorators::RuntimeErrorProblemJSONDecorator.new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] })
+ error_decorator_class(env).new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] })
end
private_class_method def self.obfuscated_error_message(error_reference)
@@ -76,6 +77,10 @@ def self.display_message(error, message, obfuscated_message)
private_class_method def self.problem_json?(env)
env["HTTP_ACCEPT"]&.include?("application/problem+json")
end
+
+ private_class_method def self.error_decorator_class(env)
+ env["pactbroker.application_context"].decorator_configuration.class_for(:runtime_error_problem_json_decorator)
+ end
end
end
end
diff --git a/lib/pact_broker/api/resources/event_methods.rb b/lib/pact_broker/api/resources/event_methods.rb
new file mode 100644
index 000000000..0dcbe3a0b
--- /dev/null
+++ b/lib/pact_broker/api/resources/event_methods.rb
@@ -0,0 +1,15 @@
+require "pact_broker/events/subscriber"
+
+module PactBroker
+ module Api
+ module Resources
+ module EventMethods
+ def subscribe(listener)
+ PactBroker::Events.subscribe(listener) do
+ yield
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb
index 0a2653f8e..83877f91e 100644
--- a/lib/pact_broker/api/resources/index.rb
+++ b/lib/pact_broker/api/resources/index.rb
@@ -116,6 +116,12 @@ def links
title: "Get, create or delete a tag for a pacticipant version",
templated: true
},
+ "pb:pacticipant-branch" =>
+ {
+ href: base_url + "/pacticipants/{pacticipant}/branches/{branch}",
+ title: "Get or delete a pacticipant branch",
+ templated: true
+ },
"pb:pacticipant-branch-version" =>
{
href: base_url + "/pacticipants/{pacticipant}/branches/{branch}/versions/{version}",
diff --git a/lib/pact_broker/api/resources/labels.rb b/lib/pact_broker/api/resources/labels.rb
new file mode 100644
index 000000000..d6620969a
--- /dev/null
+++ b/lib/pact_broker/api/resources/labels.rb
@@ -0,0 +1,37 @@
+require "pact_broker/api/resources/base_resource"
+require "pact_broker/api/decorators/labels_decorator"
+require "pact_broker/api/resources/pagination_methods"
+
+module PactBroker
+ module Api
+ module Resources
+ class Labels < BaseResource
+ include PaginationMethods
+
+ def content_types_provided
+ [["application/hal+json", :to_json]]
+ end
+
+ def allowed_methods
+ ["GET", "OPTIONS"]
+ end
+
+ def policy_name
+ :'labels::labels'
+ end
+
+ def to_json
+ decorator_class(:labels_decorator).new(labels).to_json(
+ **decorator_options(
+ hide_label_decorator_links: true,
+ )
+ )
+ end
+
+ def labels
+ label_service.get_all_unique_labels(pagination_options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/api/resources/latest_version.rb b/lib/pact_broker/api/resources/latest_version.rb
index bac4ab7a2..430410fd8 100644
--- a/lib/pact_broker/api/resources/latest_version.rb
+++ b/lib/pact_broker/api/resources/latest_version.rb
@@ -17,6 +17,8 @@ def allowed_methods
def version
if identifier_from_path[:tag]
@version ||= version_service.find_by_pacticipant_name_and_latest_tag(identifier_from_path[:pacticipant_name], identifier_from_path[:tag])
+ elsif identifier_from_path[:branch_name]
+ @version ||= version_service.find_latest_by_pacticipant_name_and_branch_name(identifier_from_path[:pacticipant_name], identifier_from_path[:branch_name])
else
@version ||= version_service.find_latest_by_pacticpant_name(identifier_from_path)
end
diff --git a/lib/pact_broker/api/resources/pact.rb b/lib/pact_broker/api/resources/pact.rb
index 441ed1496..a27c9a4d6 100644
--- a/lib/pact_broker/api/resources/pact.rb
+++ b/lib/pact_broker/api/resources/pact.rb
@@ -9,14 +9,17 @@
require "pact_broker/webhooks/execution_configuration"
require "pact_broker/api/resources/webhook_execution_methods"
require "pact_broker/api/resources/pact_resource_methods"
+require "pact_broker/api/resources/event_methods"
+require "pact_broker/integrations/event_listener"
module PactBroker
module Api
module Resources
class Pact < BaseResource
+ include EventMethods
include PacticipantResourceMethods
- include WebhookExecutionMethods
include PactResourceMethods
+ include WebhookExecutionMethods
include PactBroker::Messages
def content_types_provided
@@ -65,11 +68,13 @@ def resource_exists?
def from_json
response_code = pact ? 200 : 201
- handle_webhook_events do
- if request.patch? && resource_exists?
- @pact = pact_service.merge_pact(pact_params)
- else
- @pact = pact_service.create_or_update_pact(pact_params)
+ subscribe(PactBroker::Integrations::EventListener.new) do
+ handle_webhook_events do
+ if request.patch? && resource_exists?
+ @pact = pact_service.merge_pact(pact_params.merge(pact_version_sha: pact_version_sha))
+ else
+ @pact = pact_service.create_or_update_pact(pact_params.merge(pact_version_sha: pact_version_sha))
+ end
end
end
response.body = to_json
@@ -109,7 +114,7 @@ def pact
end
def disallowed_modification?
- if request.really_put? && pact_service.disallowed_modification?(pact, pact_params.json_content)
+ if request.really_put? && pact_service.disallowed_modification?(pact, pact_version_sha)
message_params = { consumer_name: pact_params.consumer_name, consumer_version_number: pact_params.consumer_version_number, provider_name: pact_params.provider_name }
set_json_error_message(message("errors.validation.pact_content_modification_not_allowed", message_params))
true
@@ -121,6 +126,10 @@ def disallowed_modification?
def schema
api_contract_class(:put_pact_params_contract)
end
+
+ def pact_version_sha
+ @pact_version_sha ||= pact_service.generate_sha(pact_params.json_content)
+ end
end
end
end
diff --git a/lib/pact_broker/api/resources/pacticipant_branches.rb b/lib/pact_broker/api/resources/pacticipant_branches.rb
index f97da58a2..e30181b45 100644
--- a/lib/pact_broker/api/resources/pacticipant_branches.rb
+++ b/lib/pact_broker/api/resources/pacticipant_branches.rb
@@ -1,6 +1,8 @@
require "pact_broker/api/resources/base_resource"
require "pact_broker/api/resources/pagination_methods"
require "pact_broker/api/resources/filter_methods"
+require "pact_broker/api/resources/after_reply"
+require "rack/utils"
module PactBroker
module Api
@@ -8,13 +10,14 @@ module Resources
class PacticipantBranches < BaseResource
include PaginationMethods
include FilterMethods
+ include AfterReply
def content_types_provided
[["application/hal+json", :to_json]]
end
def allowed_methods
- ["GET", "OPTIONS"]
+ ["GET", "DELETE", "OPTIONS"]
end
def resource_exists?
@@ -29,6 +32,17 @@ def policy_name
:'versions::branches'
end
+ # Allows bulk deletion of pacticipant branches, keeping the specified branches and the main branch.
+ # Deletes the branches asyncronously, after the response has been sent, for performance reasons.
+ def delete_resource
+ after_reply do
+ branch_service.delete_branches_for_pacticipant(pacticipant, exclude: exclude)
+ end
+ notices = branch_service.branch_deletion_notices(pacticipant, exclude: exclude)
+ response.body = decorator_class(:notices_decorator).new(notices).to_json(**decorator_options)
+ 202
+ end
+
private
def branches
@@ -40,6 +54,10 @@ def branches
)
end
+ def exclude
+ Rack::Utils.parse_nested_query(request.uri.query)["exclude"] || []
+ end
+
def eager_load_associations
decorator_class(:pacticipant_branches_decorator).eager_load_associations
end
diff --git a/lib/pact_broker/api/resources/pagination_methods.rb b/lib/pact_broker/api/resources/pagination_methods.rb
index 069f71853..a8c2f4a9f 100644
--- a/lib/pact_broker/api/resources/pagination_methods.rb
+++ b/lib/pact_broker/api/resources/pagination_methods.rb
@@ -2,8 +2,14 @@ module PactBroker
module Api
module Resources
module PaginationMethods
+ # rubocop: disable Metrics/CyclomaticComplexity
def pagination_options
- if request.query["pageNumber"] || request.query["pageSize"]
+ if request.query["page"] || request.query["size"]
+ {
+ page_number: request.query["page"]&.to_i || 1,
+ page_size: request.query["size"]&.to_i || 100
+ }
+ elsif request.query["pageNumber"] || request.query["pageSize"]
{
page_number: request.query["pageNumber"]&.to_i || 1,
page_size: request.query["pageSize"]&.to_i || 100
diff --git a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb
index 852e143d4..822bd4310 100644
--- a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb
+++ b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb
@@ -15,6 +15,10 @@ def content_types_provided
[["application/hal+json", :to_json]]
end
+ # TODO drop support for GET in next major version.
+ # GET was only used by the very first Ruby Pact clients that supported the 'pacts for verification'
+ # feature, until it became clear that the parameters for the request were going to get nested and complex,
+ # at which point the POST was added.
def allowed_methods
["GET", "POST", "OPTIONS"]
end
@@ -32,6 +36,7 @@ def process_post
end
end
+ # For this endoint, the POST is a "read" action (used for Pactflow)
def read_methods
super + %w{POST}
end
diff --git a/lib/pact_broker/api/resources/publish_contracts.rb b/lib/pact_broker/api/resources/publish_contracts.rb
index 45b921735..c7e598c6e 100644
--- a/lib/pact_broker/api/resources/publish_contracts.rb
+++ b/lib/pact_broker/api/resources/publish_contracts.rb
@@ -53,7 +53,7 @@ def policy_record_context
private
def parsed_contracts
- @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params)
+ @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params, { user_options: { sha_generator: PactBroker.configuration.sha_generator } } )
end
def params
diff --git a/lib/pact_broker/api/resources/triggered_webhook_logs.rb b/lib/pact_broker/api/resources/triggered_webhook_logs.rb
index 5925e8db1..097788bff 100644
--- a/lib/pact_broker/api/resources/triggered_webhook_logs.rb
+++ b/lib/pact_broker/api/resources/triggered_webhook_logs.rb
@@ -7,7 +7,7 @@ module Resources
class TriggeredWebhookLogs < BaseResource
def content_types_provided
- [["text/plain", :to_text]]
+ [["text/plain", :to_text], ["application/hal+json", :to_json]]
end
def allowed_methods
@@ -27,6 +27,10 @@ def to_text
end
end
+ def to_json
+ decorator_class(:triggered_webhook_logs_decorator).new(triggered_webhook).to_json(**decorator_options)
+ end
+
def policy_name
:'webhooks::triggered_webhook'
end
diff --git a/lib/pact_broker/api/resources/verifications.rb b/lib/pact_broker/api/resources/verifications.rb
index ac743214f..a6f8ff27d 100644
--- a/lib/pact_broker/api/resources/verifications.rb
+++ b/lib/pact_broker/api/resources/verifications.rb
@@ -5,6 +5,8 @@
require "pact_broker/api/decorators/verification_decorator"
require "pact_broker/api/resources/webhook_execution_methods"
require "pact_broker/api/resources/metadata_resource_methods"
+require "pact_broker/api/resources/event_methods"
+require "pact_broker/integrations/event_listener"
module PactBroker
module Api
@@ -12,6 +14,7 @@ module Resources
class Verifications < BaseResource
include WebhookExecutionMethods
include MetadataResourceMethods
+ include EventMethods
def content_types_accepted
[["application/json", :from_json]]
@@ -42,10 +45,12 @@ def create_path
end
def from_json
- handle_webhook_events(build_url: verification_params["buildUrl"]) do
- verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors])
- verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context)
- response.body = decorator_for(verification).to_json(**decorator_options)
+ subscribe(PactBroker::Integrations::EventListener.new) do
+ handle_webhook_events(build_url: verification_params["buildUrl"]) do
+ verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors])
+ verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context)
+ response.body = decorator_for(verification).to_json(**decorator_options)
+ end
end
true
end
diff --git a/lib/pact_broker/api/resources/versions.rb b/lib/pact_broker/api/resources/versions.rb
index e03b2d76d..4e91ca18a 100644
--- a/lib/pact_broker/api/resources/versions.rb
+++ b/lib/pact_broker/api/resources/versions.rb
@@ -31,7 +31,7 @@ def to_json
end
def versions
- @versions ||= version_service.find_all_pacticipant_versions_in_reverse_order(pacticipant_name, pagination_options)
+ @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, {}, pagination_options, decorator_class(:versions_decorator).eager_load_associations)
end
def policy_name
diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb
index af4c0c932..2f6be0006 100644
--- a/lib/pact_broker/app.rb
+++ b/lib/pact_broker/app.rb
@@ -1,8 +1,3 @@
-# Must be defined before loading Padrino
-PADRINO_LOGGER ||= {
- ENV.fetch("RACK_ENV", "production").to_sym => { log_level: :error, stream: :stdout, format_datetime: "%Y-%m-%dT%H:%M:%S.000%:z" }
-}
-
require "pact_broker/configuration"
require "pact_broker/db"
require "pact_broker/initializers/database_connection"
@@ -21,6 +16,7 @@
require "rack/pact_broker/configurable_make_it_later"
require "rack/pact_broker/no_auth"
require "rack/pact_broker/reset_thread_data"
+require "rack/pact_broker/add_cache_header"
require "rack/pact_broker/add_vary_header"
require "rack/pact_broker/use_when"
require "rack/pact_broker/application_context"
@@ -31,6 +27,7 @@
require "pact_broker/api/authorization/resource_access_policy"
require "pact_broker/api/middleware/http_debug_logs"
require "pact_broker/application_context"
+require "pact_broker/db/advisory_lock"
module PactBroker
@@ -105,22 +102,19 @@ def post_configure
def prepare_database
logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}"
+ lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock)
if configuration.auto_migrate_db
- migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
- if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
- logger.info "Skipping database migrations as the latest migration has already been applied"
- else
- logger.info "Migrating database schema"
- PactBroker::DB.run_migrations configuration.database_connection, migration_options
- logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
+ lock.with_lock do
+ ensure_all_database_migrations_are_applied
end
else
logger.info "Skipping database schema migrations as database auto migrate is disabled"
end
if configuration.auto_migrate_db_data
- logger.info "Migrating data"
- PactBroker::DB.run_data_migrations configuration.database_connection
+ lock.with_lock do
+ run_data_migrations
+ end
else
logger.info "Skipping data migrations"
end
@@ -129,6 +123,23 @@ def prepare_database
PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks
end
+ def ensure_all_database_migrations_are_applied
+ migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files }
+
+ if PactBroker::DB.is_current?(configuration.database_connection, migration_options)
+ logger.info "Skipping database migrations as the latest migration has already been applied"
+ else
+ logger.info "Migrating database schema"
+ PactBroker::DB.run_migrations(configuration.database_connection, migration_options)
+ logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}"
+ end
+ end
+
+ def run_data_migrations
+ logger.info "Migrating data"
+ PactBroker::DB.run_data_migrations(configuration.database_connection)
+ end
+
def load_configuration_from_database
configuration.load_from_database!
end
@@ -180,16 +191,17 @@ def configure_middleware
@app_builder.use PactBroker::Api::Middleware::HttpDebugLogs if configuration.http_debug_logging_enabled
configure_basic_auth
configure_rack_protection
+ @app_builder.use Rack::PactBroker::ApplicationContext, application_context
@app_builder.use Rack::PactBroker::InvalidUriProtection
@app_builder.use Rack::PactBroker::ResetThreadData
@app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader
@app_builder.use Rack::PactBroker::AddVaryHeader
@app_builder.use Rack::Static, :urls => ["/stylesheets", "/css", "/fonts", "/js", "/javascripts", "/images"], :root => PactBroker.project_root.join("public")
@app_builder.use Rack::Static, :urls => ["/favicon.ico"], :root => PactBroker.project_root.join("public/images"), header_rules: [[:all, {"Content-Type" => "image/x-icon"}]]
+ @app_builder.use Rack::PactBroker::AddCacheHeader
@app_builder.use Rack::PactBroker::ConvertFileExtensionToAcceptHeader
# Rack::PactBroker::SetBaseUrl needs to be before the Rack::PactBroker::HalBrowserRedirect
@app_builder.use Rack::PactBroker::SetBaseUrl, configuration.base_urls
- @app_builder.use Rack::PactBroker::ApplicationContext, application_context
if configuration.use_hal_browser
logger.info "Mounting HAL browser"
diff --git a/lib/pact_broker/async/after_reply.rb b/lib/pact_broker/async/after_reply.rb
new file mode 100644
index 000000000..f25676fef
--- /dev/null
+++ b/lib/pact_broker/async/after_reply.rb
@@ -0,0 +1,30 @@
+# Saves a block for execution after the HTTP response has been sent to the user.
+# When the block is executed, it connects to the database before executing the code.
+# This is good for doing things that might take a while and don't have to be done before
+# the response is sent, and don't need retries (in which case, it might be better to use a SuckerPunch Job).
+#
+# This leverages a feature of Puma which I'm not sure is meant to be public or not.
+# There are serveral mentions of it on the internet, so I assume it's ok to use it.
+# Puma itself uses the rack.after_reply for http request logging.
+#
+# https://github.com/search?q=repo%3Apuma%2Fpuma%20rack.after_reply&type=code
+
+module PactBroker
+ module Async
+ class AfterReply
+ def initialize(rack_env)
+ @rack_env = rack_env
+ @database_connector = rack_env.fetch("pactbroker.database_connector")
+ end
+
+ def execute(&block)
+ dc = @database_connector
+ @rack_env["rack.after_reply"] << lambda {
+ dc.call do
+ block.call
+ end
+ }
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/badges/service.rb b/lib/pact_broker/badges/service.rb
index 5a79a5e22..a96b53220 100644
--- a/lib/pact_broker/badges/service.rb
+++ b/lib/pact_broker/badges/service.rb
@@ -40,6 +40,24 @@ def can_i_deploy_badge_url(tag, environment_tag, label, deployable)
build_shield_io_uri(title, status, color)
end
+ def can_i_merge_badge_url(deployable: nil)
+ title = "can-i-merge"
+
+ # rubocop:disable Layout/EndAlignment
+ color, status = case deployable
+ when nil
+ [ "lightgrey", "unknown" ]
+ when true
+ [ "brightgreen", "success" ]
+ else
+ [ "red", "failed" ]
+ end
+ # rubocop:enable Layout/EndAlignment
+
+ # left text is "can-i-merge", right text is the version number
+ build_shield_io_uri(title, status, color)
+ end
+
def error_badge_url(left_text, right_text)
build_shield_io_uri(left_text, right_text, "lightgrey")
end
diff --git a/lib/pact_broker/contracts/contract_to_publish.rb b/lib/pact_broker/contracts/contract_to_publish.rb
index 96a7ad5a5..a710925cf 100644
--- a/lib/pact_broker/contracts/contract_to_publish.rb
+++ b/lib/pact_broker/contracts/contract_to_publish.rb
@@ -1,11 +1,10 @@
module PactBroker
module Contracts
- ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict) do
- # rubocop: disable Metrics/ParameterLists
- def self.from_hash(consumer_name: nil, provider_name: nil, decoded_content: nil, content_type: nil, specification: nil, on_conflict: nil)
- new(consumer_name, provider_name, decoded_content, content_type, specification, on_conflict)
+ ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict, :pact_version_sha, keyword_init: true) do
+
+ def self.from_hash(hash)
+ new(**hash)
end
- # rubocop: enable Metrics/ParameterLists
def pact?
specification == "pact"
diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb
index e13bff71c..698430a65 100644
--- a/lib/pact_broker/contracts/service.rb
+++ b/lib/pact_broker/contracts/service.rb
@@ -35,6 +35,7 @@ def publish(parsed_contracts, base_url: )
version, version_notices = create_version(parsed_contracts)
tags = create_tags(parsed_contracts, version)
pacts, pact_notices = create_pacts(parsed_contracts, base_url)
+ create_or_update_integrations(pacts)
notices = version_notices + pact_notices
ContractsPublicationResults.from_hash(
pacticipant: version.pacticipant,
@@ -56,7 +57,7 @@ def add_pact_conflict_notices(notices, parsed_contracts)
parsed_contracts.contracts.collect do | contract_to_publish |
pact_params = create_pact_params(parsed_contracts, contract_to_publish)
existing_pact = pact_service.find_pact(pact_params)
- if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.decoded_content)
+ if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.pact_version_sha)
add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_pact.json_content, contract_to_publish.decoded_content)
end
end
@@ -66,7 +67,7 @@ def add_pact_conflict_notices(notices, parsed_contracts)
def add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_json_content, new_json_content)
message_params = {
- consumer_name: contract_to_publish.provider_name,
+ consumer_name: contract_to_publish.consumer_name,
consumer_version_number: parsed_contracts.pacticipant_version_number,
provider_name: contract_to_publish.provider_name
}
@@ -133,7 +134,7 @@ def create_pacts(parsed_contracts, base_url)
pact_params = create_pact_params(parsed_contracts, contract_to_publish)
existing_pact = pact_service.find_pact(pact_params)
listener = TriggeredWebhooksCreatedListener.new
- created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params, listener)
+ created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params.merge(pact_version_sha: contract_to_publish.pact_version_sha), listener)
notices.concat(notices_for_pact(parsed_contracts, contract_to_publish, existing_pact, created_pact, listener, base_url))
created_pact
end
@@ -303,6 +304,14 @@ def url_for_triggered_webhook(triggered_webhook, base_url)
PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url)
end
+ # Creating/updating the integrations all at once at the end of the transaction instead
+ # of one by one, as each pact is created, reduces the amount of time that
+ # a lock is held on the integrations table, therefore reducing contention
+ # and potential for deadlocks when there are many pacts being published at once.
+ def create_or_update_integrations(pacts)
+ integration_service.handle_bulk_contract_data_published(pacts)
+ end
+
private :url_for_triggered_webhook
end
end
diff --git a/lib/pact_broker/db/advisory_lock.rb b/lib/pact_broker/db/advisory_lock.rb
new file mode 100644
index 000000000..d59e1a257
--- /dev/null
+++ b/lib/pact_broker/db/advisory_lock.rb
@@ -0,0 +1,58 @@
+require "pact_broker/logging"
+
+# Uses a Postgres advisory lock to ensure that a given block of code can only have ONE
+# thread in excution at a time against a given database.
+# When the database is not Postgres, the block will yield without any locks, allowing
+# this class to be used safely with other database types, but without the locking
+# functionality.
+#
+# This is a wrapper around the actual implementation code in the Sequel extension from https://github.com/yuryroot/sequel-pg_advisory_lock
+# which was copied into this codebase and modified for usage in this codebase.
+#
+# See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS for docs on lock types
+#
+
+module PactBroker
+ module DB
+ class AdvisoryLock
+ include PactBroker::Logging
+
+ def initialize(database_connection, name, type = :pg_try_advisory_lock)
+ @database_connection = database_connection
+ @name = name
+ @type = type
+ @lock_obtained = false
+ register_advisory_lock if postgres?
+ end
+
+ def with_lock
+ if postgres?
+ @database_connection.with_advisory_lock(@name) do
+ logger.debug("Lock #{@name} obtained")
+ @lock_obtained = true
+ yield
+ end
+ else
+ logger.warn("Executing block without lock as this is not a Postgres database")
+ @lock_obtained = true
+ yield
+ end
+ end
+
+ def lock_obtained?
+ @lock_obtained
+ end
+
+ private
+
+ def postgres?
+ @database_connection.adapter_scheme.to_s =~ /postgres/
+ end
+
+ def register_advisory_lock
+ @database_connection.extension :pg_advisory_lock
+ @database_connection.register_advisory_lock(@name, @type)
+ end
+ end
+ end
+end
diff --git a/lib/pact_broker/doc/views/index/pacticipant-branch.markdown b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown
new file mode 100644
index 000000000..481d29518
--- /dev/null
+++ b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown
@@ -0,0 +1,25 @@
+# Pacticipant branch
+
+Allowed methods: `GET`, `DELETE`
+
+Path: `/pacticipants/{pacticipant}/branches/{branch}`
+
+Get or delete a pacticipant branch.
+
+## Create
+
+Branches cannot be created via the resource URL. They are created automatically when publishing contracts.
+
+## Get
+
+### Example
+
+ curl http://broker/pacticipants/Bar/branches/main -H "Accept: application/hal+json"
+
+## Delete
+
+Deletes a pacticipant branch. Does NOT delete the associated pacticipant versions.
+
+Send a `DELETE` request to the branch resource.
+
+ curl -XDELETE http://broker/pacticipants/Bar/branches/main
diff --git a/lib/pact_broker/domain/version.rb b/lib/pact_broker/domain/version.rb
index 7e053710a..7dc3733c2 100644
--- a/lib/pact_broker/domain/version.rb
+++ b/lib/pact_broker/domain/version.rb
@@ -4,9 +4,10 @@
module PactBroker
module Domain
- VERSION_COLUMNS = [:id, :number, :repository_ref, :pacticipant_id, :order, :created_at, :updated_at, :build_url]
+ class Version < Sequel::Model
+ VERSION_COLUMNS = Sequel::Model.db.schema(:versions).collect(&:first) - [:branch] # do not include the branch column, as we now have a branches table
+ set_dataset(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
- class Version < Sequel::Model(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) }))
set_primary_key :id
plugin :timestamps, update_on_create: true
diff --git a/lib/pact_broker/initializers/subscriptions.rb b/lib/pact_broker/initializers/subscriptions.rb
deleted file mode 100644
index 7ac2bc32d..000000000
--- a/lib/pact_broker/initializers/subscriptions.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require "pact_broker/events/subscriber"
-require "pact_broker/integrations/event_listener"
-
-PactBroker::Events.subscribe(PactBroker::Integrations::EventListener.new)
diff --git a/lib/pact_broker/integrations/integration.rb b/lib/pact_broker/integrations/integration.rb
index 78c6750ab..9f5fa4ea1 100644
--- a/lib/pact_broker/integrations/integration.rb
+++ b/lib/pact_broker/integrations/integration.rb
@@ -7,7 +7,17 @@
module PactBroker
module Integrations
- class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :contract_data_updated_at))
+ # The columns are explicitly specified for the Integration object so that the consumer_name and provider_name columns aren't included
+ # in the model.
+ # Those columns exist in the integrations table because the integrations table used to be an integrations view based on the
+ # pact_publications table, and those columns existed in the view.
+ # When the view was migrated to be a table (in db/migrations/20211102_create_table_temp_integrations.rb and the following migrations)
+ # the columns had to be maintained for backwards compatiblity.
+ # They are not used by the current code, however.
+ class Integration < Sequel::Model
+ INTEGRATION_COLUMNS = Sequel::Model.db.schema(:integrations).collect(&:first) - [:consumer_name, :provider_name]
+ set_dataset(Sequel::Model.db[:integrations].select(*INTEGRATION_COLUMNS))
+
set_primary_key :id
plugin :insert_ignore, identifying_columns: [:consumer_id, :provider_id]
associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id)
diff --git a/lib/pact_broker/integrations/repository.rb b/lib/pact_broker/integrations/repository.rb
index e790a32da..9890171a6 100644
--- a/lib/pact_broker/integrations/repository.rb
+++ b/lib/pact_broker/integrations/repository.rb
@@ -21,12 +21,27 @@ def create_for_pact(consumer_id, provider_id)
Integration.new(
consumer_id: consumer_id,
provider_id: provider_id,
- created_at: Sequel.datetime_class.now
+ created_at: Sequel.datetime_class.now,
+ contract_data_updated_at: Sequel.datetime_class.now
).insert_ignore
end
nil
end
+ # Ensure an Integration exists for each consumer/provider pair.
+ # Using SELECT ... INSERT IGNORE rather than just INSERT IGNORE so that we do not
+ # need to lock the table at all when the integrations already exist, which will
+ # be the most common use case. New integrations get created incredibly rarely.
+ # The INSERT IGNORE is used rather than just INSERT to handle race conditions
+ # when requests come in parallel.
+ # @param [Array