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](https://wiki.c2.com/?PrimitiveObsession). 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 ~"group::source 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 should be addressed: - [ ] @fabiopitino started a [discussion](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186297#note_2438200807): > **non-blocking**: I just realized that we do have `Gitlab::PushOptions` object that that is used in `PostReceiveService` https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L61 but later it's pushed to the `PostReceive` Sidekiq worker [as JSON](https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L33) 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 ~"group::source code" would be responsible for the general push options framework while each team owns their own namespace > > ```ruby > 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 # => { ... } > ```
issue