doc/development/gems.md
GitLab uses Gems as a tool to improve code reusability and modularity in a monolithic codebase.
We extract libraries from our codebase when their functionality is highly isolated and we want to use them in other applications ourselves or we think it would benefit the wider community.
Extracting code to a gem also ensures that the gem does not contain any hidden dependencies on our application code.
Gems should always be used when implementing functionality that can be considered isolated, that are decoupled from the business logic of GitLab and can be developed separately.
The best place in a Rails codebase with opportunities to extract new gems is the lib/ folder.
Our lib/ folder is a mix of code that is generic/universal, GitLab-specific, and tightly integrated with the rest of the codebase.
In order to decide whether to extract part of the codebase as a Gem, ask yourself the following questions:
If the answer is Yes for any of the questions above, you should strongly consider creating a new Gem.
You can always start by creating a new Gem in the same repository and later evaluate whether to migrate it to a separate repository, when it is intended to be used by a wider community.
[!warning] To prevent malicious actors from name-squatting the extracted Gems, follow the instructions to reserve a gem name.
Using Gems can provide several benefits for code maintenance:
Gems can fall under three different case:
unique_gem: Don't include gitlab in the gem name if the gem doesn't include anything specific to GitLabexisting_gem-gitlab: When you fork and modify/extend a publicly available gem, add the -gitlab suffix, according to RubyGems conventiongitlab-unique_gem: Include a gitlab- prefix to gems that are only useful in the context of GitLab projects.Examples of existing gems:
y-rb: Ruby bindings for yrs. Yrs "wires" is a Rust port of the Yjs framework.activerecord-gitlab: Adds GitLab-specific patches to the activerecord public gem.gitlab-rspec and gitlab-utils: GitLab-specific set of classes to help in a particular context, or re-use code.When extracting Gems from existing codebase, put them in gems/ of the GitLab monorepo
That gives us the advantages of gems (modular code, quicker to run tests in development). and prevents complexity (coordinating changes across repositories, new permissions, multiple projects, etc.).
Gems stored in the same repository should be referenced in Gemfile with the path: syntax.
[!warning] To prevent malicious actors from name-squatting the extracted Gems, follow the instructions to reserve a gem name.
You can see example adding a new gem: !121676.
Pick a good name for the gem, by following the Gem naming convention.
Create the new gem in gems/<name-of-gem> with bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mit.
Remove the .git folder in gems/<name-of-gem> with rm -rf gems/<name-of-gem>/.git.
Remove the auto-generated RBS sig/ directory with rm -r gems/<name-of-gem>/sig/.
Edit gems/<name-of-gem>/README.md to provide a simple description of the Gem.
Edit gems/<name-of-gem>/<name-of-gem>.gemspec and fill the details about the Gem as in the following example:
# frozen_string_literal: true
require_relative "lib/name/of/gem/version"
Gem::Specification.new do |spec|
spec.name = "<name-of-gem>"
spec.version = Name::Of::Gem::Version::VERSION
spec.authors = ["group::tenant-scale"]
spec.email = ["[email protected]"]
spec.summary = "Gem summary"
spec.description = "A more descriptive text about what the gem is doing."
spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/<name-of-gem>"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0"
spec.metadata["rubygems_mfa_required"] = "true"
spec.files = Dir['lib/**/*.rb']
spec.require_paths = ["lib"]
end
Update gems/<name-of-gem>/.rubocop.yml with:
inherit_from:
- ../config/rubocop.yml
Configure CI for a newly added Gem:
Add gems/<name-of-gem>/.gitlab-ci.yml:
include:
- local: gems/gem.gitlab-ci.yml
inputs:
gem_name: "<name-of-gem>"
To .gitlab/ci/gitlab-gems.gitlab-ci.yml add:
include:
- local: .gitlab/ci/templates/gem.gitlab-ci.yml
inputs:
gem_name: "<name-of-gem>"
Reference Gem in Gemfile with:
gem '<name-of-gem>', path: 'gems/<name-of-gem>'
While the gem has its own Gemfile, in the
actual application the top-level Gemfile for the monolith GitLab is
used instead of the individual Gemfile sitting in the directory of the gem.
This means we should be aware that the Gemfile for the gem should not use
any versions of dependencies which might be conflicting with the top-level
Gemfile, and we should try to use the same dependencies if possible.
An example of this is Rack. If the monolith
is using Rack 2 and we're in the process of
upgrading to Rack 3,
all gems we develop should also be tested against Rack 2, optionally also with
Rack 3 if a separate Gemfile is used in CI. See an
example here.
This does not limit to just Rack, but any dependencies.
The gitlab-utils is a Gem containing as of set of class that implement common intrinsic 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
maintaining its 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.
The activerecord-gitlab is a gem adding GitLab specific Active Record patches.
It is very well desired for such to be managed separately to isolate complexity.
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.
In general, we want to think carefully before doing this as there are severe disadvantages.
Gems stored in the external repository MUST be referenced in Gemfile with version syntax.
They MUST be always published to RubyGems.
At GitLab we use a number of external gems:
gitlab-rails requiring a second MR means integration problems
may be discovered late.gitlab-rails,
it may take longer to get code reviewed and the impact of "bus factor" increases.gitlab-rails.gitlab-rails.The project for a new Gem should always be created in gitlab-org/ruby/gems namespace:
gitlab-. For example, gitlab-sidekiq-fetcher.0.0.1 version of the gem to rubygems.org to ensure the gem name is reserved.Add the gitlab_rubygems user as owner of the new gem by running:
gem owner <gem-name> --add gitlab_rubygems
Optional. Add some or all of the following users as co-owners:
https://rubygems.org/gems/<gem-name> and verify that the gem was published
successfully and gitlab_rubygems is also an owner.gitlab-org/ruby/gems group (or in a subgroup of it):
Follow the instructions for new projects.
Follow the instructions for setting up a CI/CD configuration.
Use the gem-release CI component
to release and publish new gem versions by adding the following to their .gitlab-ci.yml:
include:
- component: $CI_SERVER_FQDN/gitlab-org/components/gem-release/gem-release@~latest
This job will handle building and publishing the gem (it uses a gitlab_rubygems RubyGems
API token inherited from the gitlab-org/ruby/gems group, in order to publish the gem
package), as well as creating the tag, release and populating its release notes by
using the
Generate changelog data
API endpoint.
For instructions for when and how to generate a changelog entry file, see the
dedicated Changelog entries
page.
To be consistent with the GitLab project,
Gem projects could also define a changelog YAML configuration file at
.gitlab/changelog_config.yml with the same content
as in the gitlab-styles gem.
To ease the release process, you could also create a .gitlab/merge_request_templates/Release.md MR template with the same content
as in the gitlab-styles gem
(make sure to replace gitlab-styles with the actual gem name).
Follow the instructions for publishing a project.
Notes: 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.
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:
vendor/gems/ MUST ONLY be used if we are pulling from external repository either via script, or manually.vendor/gems/ MUST NOT be used for storing in-house gems.vendor/gems/ MAY accept fixes to make them buildable with GitLab monorepogems/ MUST be used for storing all in-house gems that are part of GitLab monorepo.gems/ in GitLab monorepo.vendor/gemsFor in-house Gems that do not have external repository and are currently stored in vendor/gems/:
For Gems that are used by other repositories:
Gemfile and fetched from RubyGems.For Gems that are only used by monorepo:
gems/.path: in Gemfile.For vendor/gems/ that are external and vendored in monorepo:
path: in Gemfile, since we cannot depend on RubyGems.We may reserve gem names as a precaution before publishing any public code that contains a new gem, to avoid name-squatters taking over the name in RubyGems.
To reserve a gem name, follow the steps to Create and publish a Ruby gem, with the following changes:
0.0.0 as the version.lib/NAME.rb with the content raise "Reserved for GitLab".build and publish, and check https://rubygems.org/gems/ to confirm it succeeded.If you are creating an account on RubyGems.org for the purposes of your work at GitLab, you should:
@gitlab.com).All changes such as modifications to account emails or passwords, gem owners, and gem deletion ought to be communicated previously to the directly responsible teams, through issues or Slack (the team's Slack channel, #rubygems, #ruby, #development).