diff options
Diffstat (limited to 'doc/development/gems.md')
-rw-r--r-- | doc/development/gems.md | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/doc/development/gems.md b/doc/development/gems.md new file mode 100644 index 00000000000..709dfa105bf --- /dev/null +++ b/doc/development/gems.md @@ -0,0 +1,321 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Gems development guidelines + +GitLab uses Gems as a tool to improve code reusability and modularity +in a monolithic codebase. + +Sometimes we create libraries within our codebase that we want to +extract, either because their functionality is highly isolated, +we want to use them in other applications +ourselves, or we think it would benefit the wider community. +Extracting code to a gem also means that we can be sure that the gem +does not contain any hidden dependencies on our application code. + +## When to use Gems + +Gems should be used always when implementing functions that can be considered isolated, +that are decoupled from the business logic of GitLab and can be developed separately. Consider the +following examples where Gem logic could be placed: + +The best example where we can look for opportunities to introduce new gems +is the [lib/](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/) folder. + +The **lib/** folder is a mix of code that is generic/universal, GitLab-specific, and tightly integrated with the rest of the codebase. + +If you cannot find a good place for your code in **lib/** you should strongly +consider creating the new Gem [In the same repo](#in-the-same-repo). + +## In the same repo + +**Our GitLab Gems should be always put in `gems/` of GitLab monorepo.** + +That gives us the advantages of gems (modular code, quicker to run tests in development). +and prevents complexity (coordinating changes across repos, new permissions, multiple projects, etc.). + +Gems stored in the same repo should be referenced in `Gemfile` with the `path:` syntax. +They should not be published to RubyGems. + +### Advantages + +Using Gems can provide several benefits for code maintenance: + +- Code Reusability - Gems are isolated libraries that serve single purpose. When using Gems, a common functions + can be isolated in a simple package, that is well documented, tested, and re-used in different applications. + +- Modularity - Gems help to create isolation by encapsulating specific functionality within self-contained library. + This helps to better organize code, better define who is owner of a given module, makes it easier to maintain + or update specific gems. + +- Small - Gems by design due to implementing isolated set of functions are small. Small projects are much easier + to comprehend, extend and maintain. + +- Testing - Using Gems since they are small makes much faster to run all tests, or be very through with testing of the gem. + Since the gem is packaged, not changed too often, it also allows us to run those tests less frequently improving + CI testing time. + +### To Do + +#### Desired use cases + +The `gitlab-utils` is a Gem containing as of set of class that implement common intrisic functions +used by GitLab developers, like `strong_memoize` or `Gitlab::Utils.to_boolean`. + +The `gitlab-database-schema-migrations` is a potential Gem containing our extensions to Rails +framework improving how database migrations are stored in repository. This builds on top of Rails +and is not specific to GitLab the application, and could be generally used for other projects +or potentially be upstreamed. + +The `gitlab-database-load-balancing` similar to previous is a potential Gem to implement GitLab specific +load balancing to Rails database handling. Since this is rather complex and highly specific code +maintaing it's complexity in a isolated and well tested Gem would help with removing this complexity +from a big monolithic codebase. + +The `gitlab-flipper` is another potential Gem implementing all our custom extensions to support feature +flags in a codebase. Over-time the monolithic codebase did grow with the check for feature flags +usage, adding consistency checks and various helpers to track owners of feature flags added. This is +not really part of GitLab business logic and could be used to better track our implementation +of Flipper and possibly much easier change it to dogfood [GitLab Feature Flags](../operations/feature_flags.md). + +The `gitlab-ci-reports-parsers` is a potential Gem that could implement all various parsers for various formats. +The parsed output would be transformed into objects that could then be used by GitLab the application +to store it in the database. This functionality could be an additional Gem since it is isolated, +rarely changed, and GitLab Rails only consumes the data. + +The same pattern could be applied to all other type of parsers, like security vulnerabilities, or any +other complex structures that need to be transformed into a form that is consumed by GitLab Rails. + +The `gitlab-active_record` is a gem adding GitLab specific Active Record patches. +It is very well desired for such to be managed separately to isolate complexity. + +#### Other potential use cases + +The `gitlab-ci-config` is a potential Gem containing all our CI code used to parse `.gitlab-ci.yml`. +This code is today lightly interlocked with GitLab the application due to lack of proper abstractions. +However, moving this to dedicated Gem could allow us to build various adapters to handle integration +with GitLab the application. The interface would for example define an adapter to resolve `includes:`. +Once we would have a `gitlab-ci-config` Gem it could be used within GitLab and outside of GitLab Rails +and [GitLab CLI](https://gitlab.com/gitlab-org/cli). + +### Create and use a new Gem + +You can see example adding new Gem: [!121676](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121676). + +1. Create a new Ruby Gem in `gems/gitlab-<name-of-gem>` with `bundle gem gems/gitlab-<name-of-gem> --no-exe --no-coc --no-ext --no-mit`. +1. Edit or remove `gitlab-<name-of-gem>/README.md` to provide a simple one paragraph description of the Gem. +1. Edit `gitlab-<name-of-gem>/gitlab-<name-of-gem>.gemspec` and fill the details about the Gem as in the following example: + + ```ruby + Gem::Specification.new do |spec| + spec.name = "gitlab-<name-of-gem>" + spec.version = Gitlab::NameOfGem::VERSION + spec.authors = ["group::tenant-scale"] + spec.email = ["engineering@gitlab.com"] + + spec.summary = "GitLab's RSpec extensions" + spec.description = "A set of useful helpers to configure RSpec with various stubs and CI configs." + spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-<name-of-gem>" + spec.required_ruby_version = ">= 2.6.0" + end + ``` + +1. Update `gems/gitlab-<name-of-gem>/.rubocop` with: + + ```yaml + inherit_from: + - ../../.rubocop.yml + + CodeReuse/ActiveRecord: + Enabled: false + + AllCops: + TargetRubyVersion: 3.0 + + Naming/FileName: + Exclude: + - spec/**/*.rb + ``` + +1. Configure CI for a newly added Gem: + +- Add `gems/gitlab-<name-of-gem>/.gitlab-ci.yml`: + + ```yaml + workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + + rspec: + image: "ruby:${RUBY_VERSION}" + cache: + key: gitlab-<name-of-gem> + paths: + - gitlab-<name-of-gem>/vendor/ruby + before_script: + - cd vendor/gems/bundler-checksum + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle config set with 'development' + - bundle config set --local frozen 'true' # Disallow Gemfile.lock changes on CI + - bundle config # Show bundler configuration + - bundle install -j $(nproc) + script: + - bundle exec rspec + parallel: + matrix: + - RUBY_VERSION: ["2.7", "3.0", "3.1", "3.2"] + ``` + +- To `.gitlab/ci/rules.gitlab-ci.yml` add: + + ```yaml + .gems:rules:gitlab-<name-of-gem>: + rules: + - <<: *if-merge-request + changes: ["gems/gitlab-<name-of-gem>/**/*"] + ``` + +- To `.gitlab/ci/gitlab-gems.gitlab-ci.yml` add: + + ```yaml + gems gitlab-<name-of-gem>: + extends: + - .gems:rules:gitlab-<name-of-gem> + needs: [] + trigger: + include: gems/gitlab-<name-of-gem>/.gitlab-ci.yml + strategy: depend + ``` + +1. Reference Gem in `Gemfile` with: + + ```ruby + gem 'gitlab-<name-of-gem>', path: 'gems/gitlab-<name-of-gem>' + ``` + +## In the external repo + +In general, we want to think carefully before doing this as there are +severe disadvantages. + +Gems stored in the external repo MUST be referenced in `Gemfile` with `version` syntax. +They MUST be always published to RubyGems. + +### Examples + +At GitLab we use a number of external gems: + +- [LabKit Ruby](https://gitlab.com/gitlab-org/labkit-ruby) +- [GitLab Ruby Gems](https://gitlab.com/gitlab-org/ruby/gems) + +### Potential disadvantages + +- Gems - even those maintained by GitLab - do not necessarily go + through the same [code review process](code_review.md) as the main + Rails application. This is particularly critical for Application Security. +- Requires setting up CI/CD from scratch, including tools like Danger that + support consistent code review standards. +- Extracting the code into a separate project means that we need a + minimum of two merge requests to change functionality: one in the gem + to make the functional change, and one in the Rails app to bump the + version. +- Integration with `gitlab-rails` requiring a second MR means integration problems + may be discovered late. +- With a smaller pool of reviewers and maintainers compared to `gitlab-rails`, + it may take longer to get code reviewed and the impact of "bus factor" increases. +- Inconsistent workflows for how a new gem version is released. It is currently at + the discretion of library maintainers to decide how it works. +- Promotes knowledge silos because code has less visibility and exposure than `gitlab-rails`. +- We have a well defined process for promoting GitLab reviewers to maintainers. + This is not true for extracted libraries, increasing the risk of lowering the bar for code reviews, + and increasing the risk of shipping a change. +- Our needs for our own usage of the gem may not align with the wider + community's needs. In general, if we are not using the latest version + of our own gem, that might be a warning sign. + +### Potential advantages + +- Faster feedback loops, since CI/CD runs against smaller repositories. +- Ability to expose the project to the wider community and benefit from external contributions. +- Repository owners are most likely the best audience to review a change, which reduces + the necessity of finding the right reviewers in `gitlab-rails`. + +### Create and publish a Ruby gem + +The project for a new Gem should always be created in [`gitlab-org/ruby/gems` namespace](https://gitlab.com/gitlab-org/ruby/gems/): + +1. Determine a suitable name for the gem. If it's a GitLab-owned gem, prefix + the gem name with `gitlab-`. For example, `gitlab-sidekiq-fetcher`. +1. Create the gem or fork as necessary. +1. Ensure the `gitlab_rubygems` group is an owner of the new gem by running: + + ```shell + gem owner <gem-name> --add gitlab_rubygems + ``` + +1. [Publish the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg) +1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem published + successfully and `gitlab_rubygems` is also an owner. +1. Create a project in [`gitlab-org/ruby/gems` namespace](https://gitlab.com/gitlab-org/ruby/gems/). + + - To create this project: + 1. Follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#creating-a-new-project). + 1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#cicd-configuration). + 1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#publishing-a-project). + - See [issue #325463](https://gitlab.com/gitlab-org/gitlab/-/issues/325463) + for an example. + - In some cases we may want to move a gem to its own namespace. Some + examples might be that it will naturally have more than one project + (say, something that has plugins as separate libraries), or that we + expect users outside GitLab to be maintainers on this project as + well as GitLab team members. + + The latter situation (maintainers from outside GitLab) could also + apply if someone who currently works at GitLab wants to maintain + the gem beyond their time working at GitLab. + +When publishing a gem to RubyGems.org, also note the section on +[gem owners](https://about.gitlab.com/handbook/developer-onboarding/#ruby-gems) +in the handbook. + +## The `vendor/gems/` + +The purpose of `vendor/` is to pull into GitLab monorepo external dependencies, +which do have external repositories, but for the sake of simplicity we want +to store them in monorepo: + +- The `vendor/gems/` MUST ONLY be used if we are pulling from external repository either via script, or manually. +- The `vendor/gems/` MUST NOT be used for storing in-house gems. +- The `vendor/gems/` MAY accept fixes to make them buildable with GitLab monorepo +- The `gems/` MUST be used for storing all in-house gems that are part of GitLab monorepo. +- The **RubyGems** MUST be used for all externally stored dependencies that are not in `gems/` in GitLab monorepo. + +### Handling of an existing gems in `vendor/gems` + +- For in-house Gems that do not have external repository and are currently stored in `vendor/gems/`: + + - For Gems that are used by other repositories: + + - We will migrate it into its own repository. + - We will start or continue publishing them via RubyGems. + - Those Gems will be referenced via version in `Gemfile` and fetched from RubyGems. + + - For Gems that are only used by monorepo: + + - We will stop publishing new versions to RubyGems. + - We will not pull from RubyGems already published versions since there might + be applications depedent on those. + - We will move those gems to `gems/`. + - Those Gems will be referenced via `path:` in `Gemfile`. + +- For `vendor/gems/` that are external and vendored in monorepo: + + - We will maintain them in the repository if they require some fixes that cannot be or are not yet upstreamed. + - It is expected that vendored gems might be published by third-party. + - Those Gems will not be published by us to RubyGems. + - Those Gems will be referenced via `path:` in `Gemfile`, since we cannot depend on RubyGems. |