Make PushOptions a cross-domain framework
Problem
We are using push options as Hash object in many parts of the application and this has turned into a Primitive Obsession. Push options is more of a framework for passing data to GitLab backend via git push
. Today we have various types of data (namespaces) of push options such as ci
, merge_request
, integrations
, etc.
While groupsource code would own the framework (of passing data), each data namespace should be owned by various domains. The proposal below can be one way we could solve this in a modularized way.
Context
This issue is extracted from a code review where we introduced a CI-specific push options class rather than having specific parser logic defined in Git::BaseHooksService
.
The following discussion from !186297 (merged) should be addressed:
-
@fabiopitino started a discussion: non-blocking: I just realized that we do have
Gitlab::PushOptions
object that that is used inPostReceiveService
https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L61 but later it's pushed to thePostReceive
Sidekiq worker as JSON and from there on we use it as Hash.It would be great to have a SSoT for dealing with push options but it would require an even bigger refactoring because:
-
Gitlab::PushOptions
expects options in Array form (as it comes from Gitaly). - The initializer of
Gitlab::PushOptions
is primarily a parser. The rest of the class is a basic access object. - We could have
Gitlab::PushOptions
to be a SSoT for serialized and deserialized options - Each domain could implement their own Push Options namespace (e.g.
ci.*
) and be responsible for it. - The groupsource code would be responsible for the general push options framework while each team owns their own namespace
class Gitlab::PushOptions def self.fabricate(push_options) # handle: array (gitaly), hash/json, PushOptions object, nil end NAMESPACES = { ci: ::Ci::PushOptions, # each domain owns their own push options merge_request: ::MergeRequests::PushOptions, # ... } def initialize(data) @data = data @opts = {} NAMESPACES.each do |namespace, handler| @opts[namespace] = handler.new(data.fetch(namespace.to_sym, {})) end @opts.each(&:validate!) end def ci @opts.fetch(:ci) end def merge_request @opts.fetch(:merge_request) end def as_json(*_args) @data.as_json end end class Gitlab::PushOptions::Namespace def initialize(data) @data = data end def validate! # use locally defined ALLOWED_OPTIONS and MULTI_VALUE_OPTIONS end end class Ci::PushOptions < Gitlab::PushOptions::Namespace ALLOWED_OPTIONS = [:skip, :variable, :input] MULTI_VALUE_OPTIONS = [:input, :variable] def skip? data[:skip].present? end def variables # ... end def inputs # ... end end #### # Usage: push_options = Gitlab::PushOptions.fabricate(data) push_options.ci.skip? # => false push_options.ci.inputs # => { foo: 'bar' } push_options.merge_request.labels # => ['foo', 'bar'] push_options.secret_push_protection.skip_all? # => true push_options.as_json # => { ... }
-