Help us learn about your current experience with the documentation. Take the survey.

Gems 开发指南

GitLab 使用 Gems 作为工具,在单体代码库中提高代码的可重用性和模块化程度。

当某段代码的功能高度独立,并且我们希望在其他应用程序中使用它,或者认为它会对更广泛的社区有益时,我们会将其从代码库中提取为库。

将代码提取为 gem 还可以确保该 gem 不包含对我们应用程序代码的任何隐藏依赖。

当实现可以被视为独立、与 GitLab 的业务逻辑解耦且可以单独开发的功能时,应该始终使用 Gems。

在 Rails 代码库中提取新 gem 的最佳位置是 lib/ 文件夹。

我们的 lib/ 文件夹混合了通用/通用的代码、GitLab 特定的代码以及与代码库其余部分紧密集成的代码。

为了决定是否将代码库的一部分提取为 Gem,请自问以下问题:

  1. 这段代码是否是通用或通用的,可以作为独立的小项目实现?
  2. 我是否期望它在单体之外被内部使用?
  3. 这对更广泛的社区是否有用,我们应该考虑将其作为独立组件发布?

如果以上任何问题的答案是 Yes,您应该认真考虑创建一个新的 Gem。

您始终可以先在同一仓库中创建一个新的 Gem,然后在需要被更广泛的社区使用时,评估是否将其迁移到独立的仓库。

为防止恶意行为者抢占提取的 Gems 名称,请按照说明保留 gem 名称

使用 Gems 的优势

使用 Gems 可以为代码维护提供多种好处:

  • 代码可重用性 - Gems 是独立的库,服务于单一目的。使用 Gems 时,通用功能可以隔离在一个简单的包中,该包有良好的文档、经过测试,并可以在不同的应用程序中重用。

  • 模块化 - Gems 通过将特定功能封装在自包含的库中来帮助创建隔离。这有助于更好地组织代码,更好地定义给定模块的所有者,使维护或更新特定的 gem 变得更容易。

  • 小巧 - Gems 由于实现的是一组隔离的功能,因此设计上很小。小型项目更容易理解、扩展和维护。

  • 测试 - 由于 Gems 很小,使用它们可以更快地运行所有测试,或者对 gem 进行非常彻底的测试。由于 gem 是打包的,不会经常更改,这也允许我们更少地运行这些测试,从而改进 CI 测试时间。

Gem 命名

Gems 可以分为三种不同的情况:

  • unique_gem: 如果 gem 不包含任何特定于 GitLab 的内容,请不要在 gem 名称中包含 gitlab
  • existing_gem-gitlab: 当您分叉并修改/扩展一个公开可用的 gem 时,根据 Rubygems 的约定添加 -gitlab 后缀
  • gitlab-unique_gem: 为仅在 GitLab 项目上下文中有用的 gem 添加 gitlab- 前缀。

现有 gem 的示例:

  • y-rb: yrs 的 Ruby 绑定。Yrs “wires” 是 Yjs 框架的 Rust 端口。
  • activerecord-gitlab: 为 activerecord 公共 gem 添加 GitLab 特定的补丁。
  • gitlab-rspecgitlab-utils: GitLab 特定的类集合,用于在特定上下文中提供帮助或重用代码。

在同一仓库中

从现有代码库提取 Gems 时,将它们放在 GitLab monorepo 的 gems/

这给了我们 gem 的优势(模块化代码,在开发中更快地运行测试),并避免了复杂性(跨仓库协调更改、新权限、多个项目等)。

存储在同一仓库中的 gem 应该在 Gemfile 中使用 path: 语法引用。

为防止恶意行为者抢占提取的 Gems 名称,请按照说明保留 gem 名称

创建和使用新 Gem

您可以查看添加新 gem 的示例:!121676

  1. 按照 Gem 命名 约定为 gem 选择一个好名称。

  2. 使用 bundle gem gems/<name-of-gem> --no-exe --no-coc --no-ext --no-mitgems/<name-of-gem> 中创建新的 gem。

  3. 使用 rm -rf gems/<name-of-gem>/.git 删除 gems/<name-of-gem> 中的 .git 文件夹。

  4. 编辑 gems/<name-of-gem>/README.md 以提供 gem 的简单描述。

  5. 编辑 gems/<name-of-gem>/<name-of-gem>.gemspec 并填写 gem 的详细信息,如下例所示:

    # 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
  6. 使用以下内容更新 gems/<name-of-gem>/.rubocop.yml

    inherit_from:
      - ../config/rubocop.yml
  7. 为新添加的 Gem 配置 CI:

    • 添加 gems/<name-of-gem>/.gitlab-ci.yml

      include:
        - local: gems/gem.gitlab-ci.yml
          inputs:
            gem_name: "<name-of-gem>"
    • .gitlab/ci/gitlab-gems.gitlab-ci.yml 添加:

      include:
        - local: .gitlab/ci/templates/gem.gitlab-ci.yml
          inputs:
            gem_name: "<name-of-gem>"
  8. Gemfile 中引用 Gem:

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

为 Gem 指定依赖项

虽然 gem 有自己的 Gemfile,但在实际应用程序中,使用的是单体 GitLab 的顶层 Gemfile,而不是 gem 目录中单独的 Gemfile

这意味着我们应该意识到 gem 的 Gemfile 不应该使用可能与顶层 Gemfile 冲突的任何依赖项版本,并且如果可能,我们应该尝试使用相同的依赖项。

Rack 就是一个例子。如果单体使用的是 Rack 2,并且我们正在升级到 Rack 3,那么我们开发的所有 gem 也应该针对 Rack 2 进行测试,如果在 CI 中使用单独的 Gemfile,也可以选择性地测试 Rack 3。请参见此处的示例

这不仅限于 Rack,而是适用于任何依赖项。

Gem 提取的示例

gitlab-utils 是一个 Gem,包含一组由 GitLab 开发人员使用的通用内建函数实现的类,例如 strong_memoizeGitlab::Utils.to_boolean

gitlab-database-schema-migrations 是一个潜在的 Gem,包含我们对 Rails 框架的扩展,改进了数据库迁移在仓库中的存储方式。这建立在 Rails 之上,并非特定于 GitLab 应用程序,可以普遍用于其他项目,或者可能被上游采用。

gitlab-database-load-balancing 与前一个类似,是一个潜在的 Gem,用于实现 GitLab 特定的 Rails 数据库处理负载均衡。由于这相当复杂且高度特定的代码,将其维护在一个隔离且经过良好测试的 Gem 中,有助于从大型单体代码库中移除这种复杂性。

gitlab-flipper 是另一个潜在的 Gem,实现了我们对代码库中支持功能标志的所有自定义扩展。随着时间的推移,单体代码库确实随着功能标志使用检查的增长而增长,添加了一致性检查和各种辅助功能来跟踪功能标志的所有者。这实际上不是 GitLab 业务逻辑的一部分,可以用来更好地跟踪我们对 Flipper 的实现,并可能更容易地切换到使用 GitLab Feature Flags

activerecord-gitlab 是一个添加了 GitLab 特定 Active Record 补丁的 gem。将此类内容单独管理以隔离复杂性是非常理想的。

其他潜在用例

gitlab-ci-config 是一个潜在的 Gem,包含我们用于解析 .gitlab-ci.yml 的所有 CI 代码。由于缺乏适当的抽象,这段代码今天与 GitLab 应用程序有轻微的耦合。然而,将其移动到专用的 Gem 中,可以让我们构建各种适配器来处理与 GitLab 应用程序的集成。例如,接口将定义一个用于解析 includes: 的适配器。一旦我们有了 gitlab-ci-config Gem,它就可以在 GitLab 内部和 GitLab Rails 以及 GitLab CLI 之外使用。

在外部仓库中

通常,我们希望在这样做之前仔细考虑,因为存在严重的缺点。

存储在外部仓库中的 gem 必须在 Gemfile 中使用 version 语法引用。它们必须始终发布到 RubyGems。

示例

在 GitLab 中,我们使用许多外部 gem:

潜在缺点

  • Gems - 即使是由 GitLab 维护的 - 也不一定会经过与主 Rails 应用程序相同的代码审查流程。这对于应用程序安全尤其关键。
  • 需要从头设置 CI/CD,包括支持一致代码审查标准的工具,如 Danger。
  • 将代码提取到单独的项目中意味着我们需要至少两个合并请求来更改功能:一个在 gem 中进行功能更改,另一个在 Rails 应用中升级版本。
  • gitlab-rails 的集成需要第二个 MR,这意味着集成问题可能会被发现得很晚。
  • gitlab-rails 相比,审查者和维护者的数量较少,可能需要更长时间进行代码审查,“bus factor” 的影响增加。
  • 新 gem 版本的发布流程不一致。目前由库维护者自行决定如何操作。
  • 促进了知识孤岛,因为代码的可见性和曝光度低于 gitlab-rails
  • 我们有明确的流程来提升 GitLab 审查者成为维护者。对于提取的库来说并非如此,这降低了代码审查的门槛,增加了发布更改的风险。
  • 我们自己对 gem 的使用需求可能与更广泛的社区需求不一致。一般来说,如果我们没有使用自己 gem 的最新版本,这可能是一个警告信号。

潜在优势

  • 更快的反馈循环,因为 CI/CD 在较小的仓库上运行。
  • 能够向更广泛的社区展示项目,并受益于外部贡献。
  • 仓库所有者很可能是审查更改的最佳受众,这减少了在 gitlab-rails 中寻找合适审查者的必要性。

创建和发布 Ruby gem

新 Gem 的项目应该始终在 gitlab-org/ruby/gems 命名空间 中创建:

  1. 确定 gem 的合适名称。如果是 GitLab 拥有的 gem,请在 gem 名称前加上 gitlab- 前缀。例如,gitlab-sidekiq-fetcher
  2. 根据需要本地创建 gem 或进行分叉。
  3. 将 gem 的空版本 0.0.1 发布到 rubygems.org 以确保 gem 名称已被保留。
  1. 通过运行以下命令将 gitlab_rubygems 用户添加为新 gem 的所有者:

    gem owner <gem-name> --add gitlab_rubygems
  2. 可选。添加部分或以下用户作为共同所有者:

  1. 可选。添加任何其他相关开发人员作为共同所有者。
  2. 访问 https://rubygems.org/gems/<gem-name> 并验证 gem 是否已成功发布,并且 gitlab_rubygems 也是所有者。
  3. gitlab-org/ruby/gems(或其子组)中创建项目:
    1. 遵循新项目说明

    2. 遵循设置 CI/CD 配置 的说明。

    3. 使用 gem-release CI 组件 通过向 .gitlab-ci.yml 添加以下内容来发布和发布新的 gem 版本:

      include:
        - component: $CI_SERVER_FQDN/gitlab-org/components/gem-release/gem-release@~latest

      此作业将处理构建和发布 gem(它使用从 gitlab-org/ruby/gems 组继承的 gitlab_rubygems Rubygems.org API 令牌来发布 gem 包),以及创建标签、发布和使用生成变更日志数据 API 端点填充其发布说明。

      有关何时以及如何生成变更日志条目文件的说明,请参阅专用的变更日志条目页面。与 GitLab 项目保持一致,Gem 项目也可以在 .gitlab/changelog_config.yml 中定义变更日志 YAML 配置文件,内容与 gitlab-styles gem 中的相同。

    4. 为简化发布过程,您还可以创建一个 .gitlab/merge_request_templates/Release.md MR 模板,内容与 gitlab-styles gem 中的相同(确保将 gitlab-styles 替换为实际的 gem 名称)。

    5. 遵循发布项目的说明。

注意:在某些情况下,我们可能希望将 gem 移动到自己的命名空间。一些例子可能是它自然会有多个项目(例如,有作为独立库的插件的东西),或者我们期望 GitLab 之外的用户也成为这个项目的维护者,以及 GitLab 团队成员。后一种情况(来自 GitLab 外部的维护者)也可能适用于当前在 GitLab 工作但希望在离开 GitLab 后继续维护该 gem 的人。

vendor/gems/

vendor/ 的目的是将外部依赖项拉入 GitLab monorepo,这些依赖项确实有外部仓库,但为了简单起见,我们希望将它们存储在 monorepo 中:

  • vendor/gems/ 仅当我们通过脚本或手动从外部仓库拉取时才必须使用。
  • vendor/gems/ 不得用于存储内部开发的 gem。
  • vendor/gems/ 可以接受修复以使其能够与 GitLab monorepo 一起构建。
  • gems/ 必须用于存储作为 GitLab monorepo 一部分的所有内部开发的 gem。
  • RubyGems 必须用于所有不在 GitLab monorepo gems/ 中的外部存储依赖项。

处理 vendor/gems/ 中的现有 gem

  • 对于没有外部仓库且当前存储在 vendor/gems/ 中的内部开发的 Gem:

    • 对于被其他仓库使用的 Gems:

      • 我们将将其迁移到自己的仓库。
      • 我们将开始或继续通过 RubyGems 发布它们。
      • 这些 Gems 将在 Gemfile 中通过版本引用,并从 RubyGems 获取。
    • 对于仅被 monorepo 使用的 Gems:

      • 我们将停止向 RubyGems 发布新版本。
      • 我们不会从 RubyGems 拉取已发布的版本,因为可能有应用程序依赖于这些版本。
      • 我们将这些 gem 移动到 gems/
      • 这些 Gems 将在 Gemfile 中通过 path: 引用。
  • 对于在 monorepo 中 vendored 的外部 vendor/gems/

    • 如果它们需要一些无法或尚未上游化的修复,我们将在仓库中维护它们。
    • 预期 vendored 的 gem 可能由第三方发布。
    • 我们不会将这些 gem 发布到 RubyGems。
    • 这些 Gems 将在 Gemfile 中通过 path: 引用,因为我们不能依赖 RubyGems。

关于 rubygems.org 的注意事项

保留 gem 名称

作为预防措施,我们可以在发布包含新 gem 的任何公共代码之前保留 gem 名称,以避免名称抢占者在 RubyGems 中接管该名称。

要保留 gem 名称,请按照创建和发布 Ruby gem的步骤进行,但进行以下更改:

  • 使用 0.0.0 作为版本。
  • 包含一个文件 lib/NAME.rb,内容为 raise "Reserved for GitLab"
  • 执行 buildpublish,并检查 https://rubygems.org/gems/ 确认成功。

账户创建

如果您考虑在 RubyGems.org 上创建账户以用于在 GitLab 的工作,请确保使用您的企业邮箱账户。

维护者和账户更改

所有更改,例如账户邮箱或密码的修改、gem 所有者以及 gem 删除,都应该通过问题或 Slack(团队的 Slack 频道 #rubygems#ruby#development)事先直接负责的团队进行沟通。