Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gems.md « development « doc - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 54d6e6dc30d7efbfe514631fbd2475fe3e25ba02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
---
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.

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/](https://gitlab.com/gitlab-org/gitlab/-/tree/master/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:

1. Is this code generic or universal that can be done as a separate and small project?
1. Do I expect it to be used internally outside of the Monolith?
1. Is this useful for the wider community that we should consider releasing as a separate component?

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](#in-the-same-repo) 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](#reserve-a-gem-name).

## Advantages of using Gems

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.

## Gem naming

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 GitLab
- `existing_gem-gitlab`: When you fork and modify/extend a publicly available gem, add the `-gitlab` suffix, according to [Rubygems' convention](https://guides.rubygems.org/name-your-gem/)
- `gitlab-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.

## In the same repo

**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](#reserve-a-gem-name).

### Create and use a new Gem

You can see example adding a new gem: [!121676](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121676).

1. Pick a good name for the gem, by following the [Gem naming](#gem-naming) convention.
1. Create the new gem in `gems/<name-of-gem>` with `bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mit`.
1. Remove the `.git` folder in `gems/<name-of-gem>` with `rm -rf gems/<name-of-gem>/.git`.
1. Edit `gems/<name-of-gem>/README.md` to provide a simple description of the Gem.
1. Edit `gems/<name-of-gem>/<name-of-gem>.gemspec` and fill the details about the Gem as in the following example:

   ```ruby
   # 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 = ["engineering@gitlab.com"]

     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
   ```

1. Update `gems/<name-of-gem>/.rubocop.yml` with:

   ```yaml
   inherit_from:
     - ../config/rubocop.yml
   ```

1. Configure CI for a newly added Gem:

   - Add `gems/<name-of-gem>/.gitlab-ci.yml`:

     ```yaml
     include:
       - local: gems/gem.gitlab-ci.yml
         inputs:
           gem_name: "<name-of-gem>"
     ```

   - To `.gitlab/ci/gitlab-gems.gitlab-ci.yml` add:

     ```yaml
     include:
       - local: .gitlab/ci/templates/gem.gitlab-ci.yml
         inputs:
           gem_name: "<name-of-gem>"
     ```

1. Reference Gem in `Gemfile` with:

   ```ruby
   gem '<name-of-gem>', path: 'gems/<name-of-gem>'
   ```

### Examples of Gem extractions

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](../operations/feature_flags.md).

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.

### 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).

## In the external repo

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.

### 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. Locally create the gem or fork as necessary.
1. [Publish an empty `0.0.1` version of the gem to rubygems.org](https://guides.rubygems.org/publishing/#publishing-to-rubygemsorg) to ensure the gem name is reserved.
1. Add the [`gitlab_rubygems`](https://rubygems.org/profiles/gitlab_rubygems) user as owner of the new gem by running:

   ```shell
   gem owner <gem-name> --add gitlab_rubygems
   ```

1. Optional. Add some or all of the following users as co-owners:
   - [Marin Jankovski](https://rubygems.org/profiles/marinjankovski)
   - [Rémy Coutable](https://rubygems.org/profiles/rymai)
   - [Stan Hu](https://rubygems.org/profiles/stanhu)
1. Optional. Add any other relevant developers as co-owners.
1. Visit `https://rubygems.org/gems/<gem-name>` and verify that the gem was published
   successfully and `gitlab_rubygems` is also an owner.
1. Create a project in the [`gitlab-org/ruby/gems` group](https://gitlab.com/gitlab-org/ruby/gems/) (or in a subgroup of it):
    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. Use the [gem-release CI component](https://gitlab.com/gitlab-org/quality/pipeline-common/-/tree/master/gem-release)
       to release and publish new gem versions by adding the following to their `.gitlab-ci.yml`:

       ```yaml
       include:
         - component: gitlab.com/gitlab-org/quality/pipeline-common/gem-release@<REPLACE WITH LATEST TAG FROM https://gitlab.com/gitlab-org/quality/pipeline-common/-/releases>
       ```

       This job will handle building and publishing the gem (it uses a `gitlab_rubygems` Rubygems.org
       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/repositories.md#generate-changelog-data)
       API endpoint.

       For instructions for when and how to generate a changelog entry file, see the
       dedicated [Changelog entries](changelog.md)
       page.
       [To be consistent with the GitLab project](changelog.md),
       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](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/blob/master/.gitlab/changelog_config.yml).
    1. 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](https://gitlab.com/gitlab-org/ruby/gems/gitlab-styles/-/raw/master/.gitlab/merge_request_templates/Release.md)
      (make sure to replace `gitlab-styles` with the actual gem name).
    1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/gitlab-repositories/#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.

## 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 dependent 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.

## Reserve a gem name

We reserve a gem name 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](#create-and-publish-a-ruby-gem), with the following changes:

- Use `0.0.0` as the version.
- Include a single file `lib/NAME.rb` with the content `raise "Reserved for GitLab"`.
- Perform the `build` and `publish`, and check <https://rubygems.org/gems/> to confirm it succeeded.