엔터프라이즈 에디션 기능 구현 지침

  • 코드를 ee/에 배치: 모든 엔터프라이즈 에디션(EE) 코드는 ee/ 최상위 디렉터리에 배치해야 합니다. 나머지 코드는 커뮤니티 에디션(CE) 파일에 최대한 가깝게 유지해야 합니다.

  • 테스트 작성: 모든 코드와 마찬가지로 EE 기능은 회귀를 방지하기 위해 좋은 테스트 커버리지를 가져야 합니다. 모든 ee/ 코드는 ee/에 해당하는 테스트를 가져야 합니다.

  • 문서 작성: doc/ 디렉터리에 문서를 추가하세요. 기능을 설명하고, 필요한 경우 스크린샷을 포함합니다. 이 기능이 적용되는 에디션을 명시합니다.

SaaS 전용 기능

SaaS(예: CustomersDot 통합)에만 적용되는 기능을 개발할 때는 다음 지침을 사용하십시오.

일반적으로 기능은 SaaS와 자체 관리 배포 모두를 위해 제공되어야 합니다. 하지만 특정 기능이 SaaS 전용으로만 제공되어야 하는 경우도 있으며, 이 가이드는 이를 수행하는 방법을 보여줍니다.

Gitlab::Saas.feature_available?를 사용하는 것이 좋습니다. 이렇게 하면 기능이 SaaS 전용인 이유에 대한 맥락이 풍부한 정의가 가능합니다.

Gitlab::Saas.feature_available?를 사용하여 SaaS 전용 기능 구현하기

FEATURES 상수에 추가하기

  1. 새로운 SaaS 전용 기능의 이름짓기에 대한 도움을 받으려면 네임스페이싱 개념 가이드를 참조하십시오.

  2. 새로운 기능을 ee/lib/ee/gitlab/saas.rbFEATURE에 추가합니다.

    FEATURES = %i[purchases_additional_minutes some_domain_new_feature_name].freeze
    
  3. 코드에서 Gitlab::Saas.feature_available?(:some_domain_new_feature_name)를 사용하여 새로운 기능을 사용합니다.

SaaS 전용 기능 정의 및 검증

이 프로세스는 코드베이스에서 일관된 SaaS 기능 사용을 보장하기 위한 것입니다. 모든 SaaS 기능은 다음과 같아야 합니다:

  • 알려져 있어야 합니다. 명시적으로 정의된 SaaS 기능만 사용하십시오.

  • 소유자가 있어야 합니다.

모든 SaaS 기능은 YAML 파일에 자기 문서화되어 있으며, 해당 파일은 다음 위치에 저장됩니다:

각 SaaS 기능은 여러 필드로 구성된 별도의 YAML 파일에 정의됩니다:

필드 필수 여부 설명
name SaaS 기능의 이름.
introduced_by_url 아니오 SaaS 기능을 도입한 머지 요청의 URL.
milestone 아니오 SaaS 기능이 생성된 마일스톤.
group 아니오 기능 플래그를 소유하는 그룹.

새로운 SaaS 기능 파일 정의 만들기

GitLab 코드베이스는 새로운 SaaS 기능 정의를 만들기 위한 전용 도구인 bin/saas-feature.rb를 제공합니다.

이 도구는 새로운 SaaS 기능에 대한 다양한 질문을 한 뒤, ee/config/saas_features에 YAML 정의를 생성합니다.

YAML 정의 파일이 있는 SaaS 기능만 개발 또는 테스트 환경에서 사용할 수 있습니다.

❯ bin/saas-feature my_saas_feature
당신은 'group::acquisition' 그룹을 선택했습니다.

>> SaaS 기능을 소개하는 MR의 URL (건너뛰려면 엔터를 누르고 Danger가 MR에서 직접 제안을 제공하도록 하세요):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
create ee/config/saas_features/my_saas_feature.yml
---
name: my_saas_feature
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
milestone: '16.8'
group: group::acquisition

다른 SaaS 인스턴스(JiHu)에서 SaaS 전용 기능 제외하기

ee/lib/ee/gitlab/saas.rb 모듈을 전면에 추가하고 Gitlab::Saas.feature_available? 메서드를 재정의합니다.

JH_DISABLED_FEATURES = %i[some_domain_new_feature_name].freeze

override :feature_available?
def feature_available?(feature)
  super && JH_DISABLED_FEATURES.exclude?(feature)
end

CE의 기능을 위해 SaaS 전용 기능 사용 금지

Gitlab::Saas.feature_available?는 CE에 나타나지 않아야 합니다.

CE를 EE 가이드로 확장하기를 참조하십시오.

테스트에서의 SaaS 전용 기능

코드베이스에 SaaS 전용 기능을 도입하면 추가적인 코드 경로가 생성되므로 이를 테스트해야 합니다.

SaaS 전용 기능이 활성화비활성화 상태일 때 모두 영향을 받는 모든 코드에 대해 자동화된 테스트를 포함하는 것이 강력히 권장됩니다.

테스트에서 SaaS 전용 기능을 활성화하려면 stub_saas_features 헬퍼를 사용합니다.

예를 들어, 테스트에서 purchases_additional_minutes 기능 플래그를 전역적으로 비활성화하려면:

stub_saas_features(purchases_additional_minutes: false)

::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false

두 경로를 테스트하는 일반적인 패턴은 다음과 같습니다:

it 'purchases/additional_minutes가 사용 가능하지 않음' do
  # purchases_additional_minutes가 기본적으로 활성화되지 않았다고 가정하는 테스트
  ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => false
end

context 'purchases_additional_minutes가 사용 가능한 경우' do
  before do
    stub_saas_features(purchases_additional_minutes: true)
  end

  it 'true를 반환함' do
    ::Gitlab::Saas.feature_available?(:purchases_additional_minutes) # => true
  end
end

SaaS 인스턴스 시뮬레이션

로컬에서 개발 중이고 SaaS(GitLab.com) 버전의 제품을 시뮬레이션해야 하는 경우:

  1. 이 환경 변수를 내보내십시오:

    export GITLAB_SIMULATE_SAAS=1
    

    환경 변수를 로컬 GitLab 인스턴스에 전달하는 방법은 여러 가지가 있습니다.

    예를 들어, 위의 코드 조각을 사용하여 env.runit 파일을 GDK의 루트에 생성할 수 있습니다.

  2. 라이센스가 있는 EE 기능 사용 허용을 활성화하여 프로젝트에 대해 라이센스가 있는 EE 기능을 사용할 수 있도록 하세요.

    1. 왼쪽 사이드바에서 아래쪽에 있는 Admin을 선택합니다.
    2. 왼쪽 사이드바에서 Settings > General을 선택합니다.
    3. Account and limit를 확장합니다.
    4. Allow use of licensed EE features 체크박스를 선택합니다.
    5. Save changes를 선택합니다.
  3. 테스트할 EE 기능을 사용하려는 그룹이 실제로 EE 플랜을 사용하는지 확인합니다:

    1. 왼쪽 사이드바에서 아래쪽에 있는 Admin을 선택합니다.
    2. 왼쪽 사이드바에서 Overview > Groups를 선택합니다.
    3. 수정할 그룹을 식별한 후 Edit을 선택합니다.
    4. Permissions and group features로 스크롤합니다. Plan에서 Ultimate를 선택합니다.
    5. Save changes를 선택합니다.

새로운 EE 기능 구현

GitLab Premium 또는 GitLab Ultimate 라이센스 기능을 개발하고 있다면, 새로운 기능을 추가하거나 확장하기 위해 다음 단계를 따르세요.

GitLab 라이센스 기능은 ee/app/models/gitlab_subscriptions/features.rb에 추가됩니다. 이 파일을 수정하는 방법을 결정하기 위해 먼저 제품 관리자와 기능이 우리 라이센스에 어떻게 적합한지에 대해 논의하세요.

다음 질문을 가이드로 사용하세요:

  1. 새로운 기능인가요, 아니면 기존 라이센스 기능을 확장하고 있나요?
    • 기능이 이미 존재한다면 features.rb를 수정할 필요가 없지만, 기존 기능 식별자를 찾아서 보호해야 합니다.
    • 새로운 기능이라면 features.rb 파일에 추가할 식별자(ex. my_feature_name)를 결정하세요.
  2. 이것이 GitLab Premium 또는 GitLab Ultimate 기능인가요?
    • 기능을 사용할 계획에 따라 기능 식별자를 PREMIUM_FEATURESULTIMATE_FEATURES에 추가하세요.
  3. 이 기능이 전 세계적으로(시스템 전체에) 제공되나요?
    • GeoDatabase Load Balancing와 같은 기능은 전체 인스턴스에서 사용되며 개별 사용자 네임스페이스로 제한할 수 없습니다. 이러한 기능은 인스턴스 라이센스에 정의됩니다. 이러한 기능은 GLOBAL_FEATURES에 추가하세요.

EE 기능 보호하기

라이센스 기능은 라이센스 사용자에게만 제공될 수 있습니다. 사용자가 기능에 접근할 수 있는지 확인하는 체크 또는 보호 기능을 추가해야 합니다.

라이센스 기능을 보호하는 방법:

  1. ee/app/models/gitlab_subscriptions/features.rb에서 기능 식별자를 찾습니다.

  2. 다음 메서드를 사용하세요. 여기서 my_feature_name은 기능 식별자입니다:

    • 프로젝트 컨텍스트에서:

      my_project.licensed_feature_available?(:my_feature_name) # my_project에 대해 사용 가능하면 true
      
    • 그룹 또는 사용자 네임스페이스 컨텍스트에서:

      my_group.licensed_feature_available?(:my_feature_name) # my_group에 대해 사용 가능하면 true
      
    • 글로벌(시스템 전체) 기능의 경우:

      License.feature_available?(:my_feature_name)  # 이 인스턴스에서 사용 가능하면 true
      
  3. 선택 사항. 글로벌 기능이 유료 플랜이 있는 네임스페이스에도 제공되는 경우, 두 기능 식별자를 결합하여 관리자는 물론 그룹 사용자도 허용할 수 있습니다. 예를 들어:

    License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # 관리자와 그룹 구성원이 이 EE 기능을 볼 수 있습니다
    

라이센스가 없을 때 CE 인스턴스 시뮬레이션

라이센스가 활성화되지 않은 EE 인스턴스에서 작업하는 GitLab CE 기능 구현 이후, GitLab Enterprise Edition은 라이센스가 활성화되지 않았을 때 GitLab Community Edition처럼 작동합니다.

CE 사양은 가능한 한 그대로 유지되어야 하며, EE에 대해 추가 사양이 추가되어야 합니다. 라이센스 기능은 EE::LicenseHelpers의 사양 도우미인 stub_licensed_features를 사용하여 스텁 처리할 수 있습니다.

GitLab이 CE처럼 작동하도록 하려면 ee/ 디렉토리를 삭제하거나 FOSS_ONLY 환경 변수true로 평가되는 것으로 설정하면 됩니다. 이는 테스트를 실행할 때도 동일하게 작동합니다(예: FOSS_ONLY=1 yarn jest).

라이센스가 있는 GDK로 CE 인스턴스 시뮬레이션

라이센스를 삭제하지 않고 GDK에서 CE 인스턴스를 시뮬레이션하려면:

  1. GDK의 루트에 env.runit 파일을 생성하고 다음 줄을 추가하세요:

    export FOSS_ONLY=1
    
  2. 그런 다음 GDK를 다시 시작하세요:

    gdk restart rails && gdk restart webpack
    

EE 설치로 되돌리려면 env.runit의 줄을 제거하고 2단계를 반복하세요.

CE로 기능 사양 실행

기능 사양을 CE로 실행할 때는 백엔드와 프론트엔드의 에디션이 일치하는지 확인해야 합니다. 이를 위해:

  1. FOSS_ONLY=1 환경 변수를 설정하세요:

    export FOSS_ONLY=1
    
  2. GDK를 시작하세요:

    gdk start
    
  3. 기능 사양을 실행하세요:

    bin/rspec spec/features/<path_to_your_spec>
    

FOSS 맥락에서 CI 파이프라인 실행

기본적으로 개발을 위한 병합 요청 파이프라인은 EE 맥락에서만 실행됩니다. FOSS와 EE 간에 차이가 있는 기능을 개발하는 경우, FOSS 맥락에서도 파이프라인을 실행하고 싶을 수 있습니다.

두 맥락에서 파이프라인을 실행하려면 병합 요청에 ~"pipeline:run-as-if-foss" 레이블을 추가하세요.

자세한 내용은 As-if-FOSS 작업 및 크로스 프로젝트 다운스트림 파이프라인 문서를 참조하세요.

백엔드에서 EE 코드 분리

EE 전용 기능

개발 중인 기능이 CE에 어떤 형태로든 존재하지 않는 경우, 코드를 EE 네임스페이스에 두지 않아도 됩니다. 예를 들어, EE 모델은 Awesome을 클래스 이름으로 사용하여 ee/app/models/awesome.rb에 저장할 수 있습니다. 이는 모델뿐만 아니라 다른 경우에도 적용됩니다. 다음은 다른 예시 목록입니다:

  • ee/app/controllers/foos_controller.rb
  • ee/app/finders/foos_finder.rb
  • ee/app/helpers/foos_helper.rb
  • ee/app/mailers/foos_mailer.rb
  • ee/app/models/foo.rb
  • ee/app/policies/foo_policy.rb
  • ee/app/serializers/foo_entity.rb
  • ee/app/serializers/foo_serializer.rb
  • ee/app/services/foo/create_service.rb
  • ee/app/validators/foo_attr_validator.rb
  • ee/app/workers/foo_worker.rb
  • ee/app/views/foo.html.haml
  • ee/app/views/foo/_bar.html.haml

이는 CE의 eager-load/auto-load 경로마다 동일한 ee/가 추가된 경로를 config/application.rb에 추가하기 때문에 가능합니다. 이는 뷰에도 적용됩니다.

EE 전용 백엔드 기능 테스트

CE에 존재하지 않는 EE 클래스를 테스트하려면, ee/spec 디렉토리에 일반적으로 사양 파일을 생성하되, 두 번째 ee/ 하위 디렉토리는 포함하지 마세요. 예를 들어, 클래스 ee/app/models/vulnerability.rb에 대한 테스트는 ee/spec/models/vulnerability_spec.rb에 위치해야 합니다.

기본적으로, 라이센스가 있는 기능은 specs/의 사양에서 비활성화되어 있습니다. ee/spec 디렉토리의 사양은 기본적으로 Starter 라이센스가 초기화되어 있습니다.

기능을 효과적으로 테스트하려면 stub_licensed_features 헬퍼를 사용하여 기능을 명시적으로 활성화해야 합니다. 예를 들어:

  stub_licensed_features(my_awesome_feature_name: true)

CE 기능을 EE 백엔드 코드로 확장하기

기존 CE 기능을 기반으로 하는 기능을 위해, EE 네임스페이스에 모듈을 작성하고, 해당 클래스가 위치한 파일의 마지막 줄에서 CE 클래스에 주입하세요. 이렇게 하면 CE에서 EE로 병합할 때 충돌이 발생할 가능성이 줄어듭니다. CE 클래스에 추가되는 것은 모듈을 주입하는 한 줄뿐이기 때문입니다. 예를 들어, User 클래스에 모듈을 추가하려면 다음과 같은 접근 방식을 사용합니다:

class User < ActiveRecord::Base
  # ... 코드가 많이 여기에 있습니다 ...
end

User.prepend_mod

prepend, extend, include와 같은 메소드는 사용하지 마세요. 대신 prepend_mod, extend_mod, 또는 include_mod를 사용하세요. 이 메소드들은 수신 모듈의 이름을 통해 관련 EE 모듈을 찾으려고 합니다. 예를 들어;

module Vulnerabilities
  class Finding
    #...
  end
end

Vulnerabilities::Finding.prepend_mod

위의 코드는 ::EE::Vulnerabilities::Finding이라는 모듈을 추가합니다.

확장 모듈이 이 명명 규칙을 따르지 않을 경우, prepend_mod_with, extend_mod_with, 또는 include_mod_with를 사용하여 모듈 이름을 제공할 수도 있습니다. 이 메소드들은 모듈 자체가 아닌 전체 모듈 이름을 포함하는 _String_을 인수로 취합니다. 예를 들어;

class User
  #...
end

User.prepend_mod_with('UserExtension')

모듈은 EE 네임스페이스를 요구하므로, 파일은 ee/ 하위 디렉토리에 위치해야 합니다. 예를 들어, EE에서 사용자 모델을 확장하고자 할 때, ::EE::User라는 모듈을 ee/app/models/ee/user.rb 내부에 둡니다.

이것은 모델에만 적용되는 것이 아닙니다. 다른 예시들을 살펴보세요:

  • ee/app/controllers/ee/foos_controller.rb
  • ee/app/finders/ee/foos_finder.rb
  • ee/app/helpers/ee/foos_helper.rb
  • ee/app/mailers/ee/foos_mailer.rb
  • ee/app/models/ee/foo.rb
  • ee/app/policies/ee/foo_policy.rb
  • ee/app/serializers/ee/foo_entity.rb
  • ee/app/serializers/ee/foo_serializer.rb
  • ee/app/services/ee/foo/create_service.rb
  • ee/app/validators/ee/foo_attr_validator.rb
  • ee/app/workers/ee/foo_worker.rb

CE 기능을 기반으로 EE 기능 테스트하기

CE 클래스를 EE 기능으로 확장하는 EE 네임스페이스 모듈을 테스트하기 위해, ee/spec 디렉토리 내에 일반적으로 스펙 파일을 생성하고, 두 번째 ee/ 하위 디렉토리를 포함시킵니다. 예를 들어, 확장된 ee/app/models/ee/user.rb의 테스트는 ee/spec/models/ee/user_spec.rb에 위치해야 합니다.

RSpec.describe 호출은 EE 모듈이 사용될 CE 클래스 이름을 사용합니다. 예를 들어, ee/spec/models/ee/user_spec.rb에서 테스트는 다음과 같이 시작합니다:

RSpec.describe User do
  describe 'EE 기능이 확장으로 추가됨'
end

CE 메서드 오버라이드하기

CE 코드베이스에 있는 메서드를 오버라이드 하려면 prepend를 사용합니다. 이는 모듈의 메서드로 클래스의 메서드를 오버라이드하면서 클래스의 구현에 접근할 수 있게 해줍니다.

몇 가지 주의할 점이 있습니다:

  • 항상 extend ::Gitlab::Utils::Override를 사용하고 overrider 메서드에 override를 사용하여 CE에서 메서드가 이름이 바뀌면 EE 오버라이드가 조용히 잊혀지지 않도록 보호해야 합니다.
  • overrider가 CE 구현의 중간에 줄을 추가할 경우, CE 메서드를 리팩토링하고 더 작은 메서드로 나눠야 합니다. 또는 CE에 빈 “후크” 메서드를 생성하고, EE에서 EE 전용 구현을 넣습니다.
  • 원래 구현에 가드 절이 포함되어 있을 경우(예: return unless condition), 메서드를 오버라이드하여 동작을 확장하기 어렵습니다. 이러한 경우에는 단순히 오버라이드를 하지 말고, 원래 메서드를 업데이트하여 우리가 확장하고자 하는 다른 메서드를 호출하도록 만들어야 합니다. 예를 들어 템플릿 메서드 패턴을 사용할 수 있습니다. 예를 들어, 다음과 같은 기본 코드가 주어졌을 때:

      class Base
        def execute
          return unless enabled?
    
          # ...
          # ...
        end
      end
    

    단순히 Base#execute를 오버라이드하는 대신, 업데이트하고 동작을 다른 메서드로 추출해야 합니다:

      class Base
        def execute
          return unless enabled?
    
          do_something
        end
    
        private
    
        def do_something
          # ...
          # ...
        end
      end
    

    그러면 우리는 그 do_something을 오버라이드 하는 것에 대해 걱정할 필요 없이 자유롭게 오버라이드할 수 있습니다:

      module EE::Base
        extend ::Gitlab::Utils::Override
    
        override :do_something
        def do_something
          # 위의 패턴을 따르면서 super를 호출하고 확장하세요
        end
      end
    

모듈을 추가할 때 ee/ 전용 하위 디렉토리에 위치시키고, 이름 충돌을 피하기 위해 클래스나 모듈을 module EE로 감싸야 합니다.

예를 들어, ApplicationController#after_sign_out_path_for의 CE 구현을 오버라이드하려면:

def after_sign_out_path_for(resource)
  current_application_settings.after_sign_out_path.presence || new_user_session_path
end

메서드를 제자리에서 수정하는 대신, 기존 파일에 prepend를 추가해야 합니다:

class ApplicationController < ActionController::Base
  # ...

  def after_sign_out_path_for(resource)
    current_application_settings.after_sign_out_path.presence || new_user_session_path
  end

  # ...
end

ApplicationController.prepend_mod_with('ApplicationController')

그리고 다음과 같이 변경된 구현을 가진 새 파일을 ee/ 하위 디렉토리에 생성해야 합니다:

module EE
  module ApplicationController
    extend ::Gitlab::Utils::Override

    override :after_sign_out_path_for
    def after_sign_out_path_for(resource)
      if Gitlab::Geo.secondary?
        Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
      else
        super
      end
    end
  end
end
CE 클래스 메서드 오버라이드

클래스 메서드에도 동일하게 적용되지만, ActiveSupport::Concern을 사용하고 class_methods 블록 내에 extend ::Gitlab::Utils::Override를 추가해야 합니다. 예를 들어:

module EE
  module Groups
    module GroupMembersController
      extend ActiveSupport::Concern

      class_methods do
        extend ::Gitlab::Utils::Override

        override :admin_not_required_endpoints
        def admin_not_required_endpoints
          super.concat(%i[update override])
        end
      end
    end
  end
end

자기 설명 래퍼 메서드 사용

메서드 구현을 수정하는 것이 불가능하거나 논리적이지 않을 때, 자기 설명적인 메서드로 감싸서 그 메서드를 사용해야 합니다.

예를 들어, GitLab-FOSS에서는 시스템에 의해 생성된 유일한 사용자가 Users::Internal.ghost이지만, EE에는 실제 사용자가 아닌 여러 유형의 봇 사용자들이 있습니다. User#ghost?의 구현을 오버라이드하는 것은 잘못된 것이므로, 대신 app/models/user.rb#internal? 메서드를 추가합니다. 구현은 다음과 같습니다:

def internal?
  ghost?
end

EE에서는 ee/app/models/ee/users.rb의 구현은 다음과 같습니다:

override :internal?
def internal?
  super || bot?
end

config/routes의 코드

config/routes.rbdraw :admin을 추가하면 애플리케이션은 config/routes/admin.rb에 있는 파일을 로드하려고 시도하고, 또한 ee/config/routes/admin.rb에 있는 파일을 로드하려고 시도합니다.

EE에서는 적어도 하나의 파일을 로드해야 하며, 최대 두 개의 파일을 로드합니다. 파일을 찾을 수 없는 경우 오류가 발생합니다. CE에서는 EE 라우트가 존재하는지 알 수 없기 때문에, 아무 것도 찾을 수 없더라도 오류가 발생하지 않습니다.

따라서 특정 CE 라우트 파일을 확장하려면 ee/config/routes에 동일한 파일을 추가하면 됩니다. EE 전용 라우트를 추가하고자 할 경우, CE와 EE 모두에 draw :ee_only를 추가하고, EE에는 ee/config/routes/ee_only.rb를 추가할 수 있습니다. 이는 render_if_exists와 유사합니다.

app/controllers/의 코드

컨트롤러에서 가장 일반적인 충돌 유형은 CE에서 액션 목록을 가진 before_action인데, EE는 그 목록에 일부 액션을 추가합니다.

params.require / params.permit 호출에서도 동일한 문제가 자주 발생합니다.

완화책

CE와 EE 액션/키워드를 분리합니다. 예를 들어 ProjectsControllerparams.require에 대해:

def project_params
  params.require(:project).permit(project_params_attributes)
end

# 항상 사용 사례에 가장 적합한 방법으로 생성된 기호 배열을 반환합니다.
# 알파벳 순으로 정렬되어야 합니다.
def project_params_attributes
  %i[
    description
    name
    path
  ]
end

EE::ProjectsController 모듈에서는:

def project_params_attributes
  super + project_params_attributes_ee
end

def project_params_attributes_ee
  %i[
    approvals_before_merge
    approver_group_ids
    approver_ids
    ...
  ]
end

app/models/의 코드

EE 전용 모델은 ee/app/models/에 정의되어야 합니다.

CE 모델을 오버라이드하려면 ee/app/models/ee/에 파일을 생성하고, prepended 블록에 새로운 코드를 추가합니다.

ActiveRecord enums는 완전히 FOSS에서 정의되어야 합니다.

app/views/의 코드

EE가 CE 뷰에 특정 뷰 코드를 추가하는 것은 매우 빈번한 문제입니다. 예를 들어, 프로젝트 설정 페이지의 승인 코드가 있습니다.

완화 방법

EE 전용 코드 블록은 파셜로 이동해야 합니다. 이는 HAML 코드의 큰 덩어리와의 충돌을 피하는 데 도움이 되며, 들여쓰기를 고려할 때 문제를 해결하기가 그리 재미있지는 않습니다.

EE 전용 뷰는 ee/app/views/에 배치하며, 필요에 따라 추가 하위 디렉토리를 사용할 수 있습니다.

render_if_exists 사용하기

정상 render 대신에, 특정 파셜을 찾을 수 없는 경우 아무것도 렌더링하지 않는 render_if_exists를 사용해야 합니다. 우리는 CE에서 render_if_exists를 사용할 수 있도록 하여 CE와 EE 간의 코드를 동일하게 유지합니다.

이 방법의 장점은 다음과 같습니다:

  • CE 코드를 읽을 때 EE 뷰를 확장하는 위치에 대한 매우 명확한 힌트입니다.

단점은 다음과 같습니다:

  • 파셜 이름에 오타가 있는 경우 조용히 무시됩니다.
주의사항

render_if_exists 뷰 경로 인수는 app/views/ee/app/views에 상대적이어야 합니다. EE 템플릿 경로를 CE 뷰 경로에 상대적으로 해결하는 것은 작동하지 않습니다.

- # app/views/projects/index.html.haml

= render_if_exists 'button' # `ee/app/views/projects/_button`을 렌더링하지 않으며 조용히 실패합니다.
= render_if_exists 'projects/button' # `ee/app/views/projects/_button`을 렌더링합니다.

render_ce 사용하기

renderrender_if_exists는 먼저 EE 파셜을 검색하고 그 다음 CE 파셜을 검색합니다. 특정 파셜만 렌더링하며, 동일한 이름을 가진 모든 파셜을 렌더링하지 않습니다. 우리는 이를 활용하여 CE에서 CE 파셜을 참조하면서 동일한 파셜 경로 (예: projects/settings/archive)를 사용할 수 있습니다 (즉, app/views/projects/settings/_archive.html.haml), 동시에 EE에서 EE 파셜을 사용할 수 있습니다 (즉, ee/app/views/projects/settings/_archive.html.haml). 이렇게 하면 CE와 EE 사이에 서로 다른 것을 표시할 수 있습니다.

하지만 때때로 우리는 EE 파셜에서 기존 CE 파셜을 재사용하고 싶을 수 있습니다. 왜냐하면 기존 CE 파셜에 뭔가를 추가하고 싶을 수 있기 때문입니다. 우리는 다른 이름의 추가 파셜을 추가하여 우회할 수 있지만, 그렇게 만드는 것은 번거롭습니다.

이 경우, 그냥 render_ce를 사용하여 EE 파셜을 무시할 수 있습니다. 한 예로 ee/app/views/projects/settings/_archive.html.haml이 있습니다:

- return if @project.marked_for_deletion?
= render_ce 'projects/settings/archive'

위의 예에서는 render 'projects/settings/archive'를 사용할 수 없습니다. 그 이유는 동일한 EE 파셜을 찾아 무한 재귀를 초래하기 때문입니다. 대신, render_ce를 사용하여 ee/의 파셜을 무시하면 같은 경로 (즉, projects/settings/archive)에 대해 CE 파셜 (즉, app/views/projects/settings/_archive.html.haml)을 렌더링합니다. 이렇게 하면 CE 파셜을 쉽게 래핑할 수 있습니다.

lib/gitlab/background_migration/의 코드

EE 전용 백그라운드 마이그레이션을 생성할 때, GitLab EE에서 CE로 다운그레이드하는 사용자를 계획해야 합니다. 즉, 모든 EE 전용 마이그레이션은 CE 코드에 존재해야 하지만 구현은 없고, 대신 EE 측에서 확장할 필요가 있습니다.

GitLab CE:

# lib/gitlab/background_migration/prune_orphaned_geo_events.rb

module Gitlab
  module BackgroundMigration
    class PruneOrphanedGeoEvents
      def perform(table_name)
      end
    end
  end
end

Gitlab::BackgroundMigration::PruneOrphanedGeoEvents.prepend_mod_with('Gitlab::BackgroundMigration::PruneOrphanedGeoEvents')

GitLab EE:

# ee/lib/ee/gitlab/background_migration/prune_orphaned_geo_events.rb

module EE
  module Gitlab
    module BackgroundMigration
      module PruneOrphanedGeoEvents
        extend ::Gitlab::Utils::Override

        override :perform
        def perform(table_name = EVENT_TABLES.first)
          return if ::Gitlab::Database.read_only?

          deleted_rows = prune_orphaned_rows(table_name)
          table_name   = next_table(table_name) if deleted_rows.zero?

          ::BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, self.class.name.demodulize, table_name) if table_name
        end
      end
    end
  end
end

app/graphql/의 코드

EE 전용 변형, 리졸버 및 유형은 ee/app/graphql/{mutations,resolvers,types}에 추가해야 합니다.

CE 변형, 리졸버 또는 유형을 오버라이드하려면, ee/app/graphql/ee/{mutations,resolvers,types}에 파일을 생성하고 prepended 블록에 새 코드를 추가하십시오.

예를 들어, CE에 Mutations::Tanukis::Create라는 변형이 있고 새 인수를 추가하고 싶다면, EE 오버라이드를 ee/app/graphql/ee/mutations/tanukis/create.rb에 배치합니다:

module EE
  module Mutations
    module Tanukis
      module Create
        extend ActiveSupport::Concern

        prepended do
          argument :name,
                   GraphQL::Types::String,
                   required: false,
                   description: '타누키 이름'
        end
      end
    end
  end
end

lib/의 코드

EE 전용 논리는 최상위 EE 모듈 네임스페이스에 배치합니다. 클래스는 일반적으로 하던 대로 EE 모듈 아래에 네임스페이스를 설정합니다.

예를 들어, CE가 lib/gitlab/ldap/에 LDAP 클래스를 가지고 있다면, EE 전용 LDAP 클래스를 ee/lib/ee/gitlab/ldap에 배치합니다.

lib/api/의 코드

한 줄의 prepend_mod_with로 EE 기능을 확장하는 것은 매우 까다로울 수 있으며,

각기 다른 Grape 기능마다 확장하기 위한 다양한 전략이 필요할 수 있습니다. 이러한 다른 전략을 쉽게 적용하기 위해, EE 모듈에서 extend ActiveSupport::Concern을 사용합니다.

EE 모듈 파일을 EE 백엔드 코드로 CE 기능 확장하기에 따라 배치합니다.

EE API 라우트

EE API 라우트의 경우, prepended 블록에 배치합니다:

module EE
  module API
    module MergeRequests
      extend ActiveSupport::Concern

      prepended do
        params do
          requires :id, types: [String, Integer], desc: '프로젝트의 ID 또는 URL 인코딩 경로'
        end
        resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
          # ...
        end
      end
    end
  end
end

네임스페이스 차이로 인해 일부 상수에 대해 전체 한정자를 사용해야 합니다.

EE 매개변수

params를 정의하고 다른 params 정의에서 use를 사용하여 EE에 정의된 매개변수를 포함할 수 있습니다. 그러나 EE가 이를 오버라이드하려면 CE에서 먼저 “인터페이스”를 정의해야 합니다. prepend_mod_with로 인해 다른 장소에서는 이 작업을 하지 않아도 되지만, Grape는 내부적으로 복잡하고 쉽게 할 수 없기 때문에, 여기에서 인터페이스를 먼저 정의하는 일반 객체 지향 관행을 따릅니다.

예를 들어, EE에 대해 몇 가지 추가 선택적 매개변수가 있다고 가정하면, 매개변수를 Grape::API::Instance 클래스에서 도우미 모듈로 이동하여 클래스에서 사용되기 전에 주입할 수 있습니다.

module API
  class Projects < Grape::API::Instance
    helpers Helpers::ProjectsHelpers
  end
end

이 CE API params가 주어졌을 때:

module API
  module Helpers
    module ProjectsHelpers
      extend ActiveSupport::Concern
      extend Grape::API::Helpers

      params :optional_project_params_ce do
        # CE 전용 매개변수는 여기...
      end

      params :optional_project_params_ee do
      end

      params :optional_project_params do
        use :optional_project_params_ce
        use :optional_project_params_ee
      end
    end
  end
end

API::Helpers::ProjectsHelpers.prepend_mod_with('API::Helpers::ProjectsHelpers')

EE 모듈에서 이를 오버라이드할 수 있습니다:

module EE
  module API
    module Helpers
      module ProjectsHelpers
        extend ActiveSupport::Concern

        prepended do
          params :optional_project_params_ee do
            # EE 전용 매개변수는 여기...
          end
        end
      end
    end
  end
end

EE 도우미

EE 모듈이 CE 도우미를 재정의하기 쉽게 하려면, 먼저 확장하려는 도우미를 정의해야 합니다. 클래스 정의 직후에 이렇게 하는 것이 쉽고 명확합니다:

module API
  module Ci
    class JobArtifacts < Grape::API::Instance
      # EE::API::Ci::JobArtifacts는 다음 도우미를 재정의합니다.
      helpers do
        def authorize_download_artifacts!
          authorize_read_builds!
        end
      end
    end
  end
end

API::Ci::JobArtifacts.prepend_mod_with('API::Ci::JobArtifacts')

그런 다음 일반 객체 지향 관행을 따라 이를 재정의할 수 있습니다:

module EE
  module API
    module Ci
      module JobArtifacts
        extend ActiveSupport::Concern

        prepended do
          helpers do
            def authorize_download_artifacts!
              super
              check_cross_project_pipelines_feature!
            end
          end
        end
      end
    end
  end
end

EE 특정 동작

때때로 일부 API에 EE 특정 동작이 필요합니다. 일반적으로 EE 메소드를 사용하여 CE 메소드를 재정의할 수 있지만, API 경로는 메소드가 아니므로 재정의할 수 없습니다. 이를 간단한 메소드로 추출하거나, CE 경로에 동작을 주입할 수 있는 “후크”를 도입해야 합니다. 다음과 같은 방식입니다:

module API
  class MergeRequests < Grape::API::Instance
    helpers do
      # EE::API::MergeRequests는 다음 도우미를 재정의합니다.
      def update_merge_request_ee(merge_request)
      end
    end

    put ':id/merge_requests/:merge_request_iid/merge' do
      merge_request = find_project_merge_request(params[:merge_request_iid])

      # ...

      update_merge_request_ee(merge_request)

      # ...
    end
  end
end

API::MergeRequests.prepend_mod_with('API::MergeRequests')

update_merge_request_ee는 CE에서 아무것도 하지 않지만, 이후 EE에서 이를 재정의할 수 있습니다:

module EE
  module API
    module MergeRequests
      extend ActiveSupport::Concern

      prepended do
        helpers do
          def update_merge_request_ee(merge_request)
            # ...
          end
        end
      end
    end
  end
end

EE route_setting

EE 모듈에서 이를 확장하기가 매우 어렵고, 이는 특정 경로에 대한 메타 데이터를 저장합니다. 따라서 CE에서 EE route_setting을 남겨두는 것이 좋습니다. 이것은 해가 되지 않으며, CE에서는 이러한 메타 데이터를 사용하지 않기 때문입니다.

이후 route_setting을 더 많이 사용할 때 이 정책을 재검토할 수 있으며, EE에서 정말로 확장해야 하는지 여부를 검토할 수 있습니다. 현재로서는 많이 사용하지 않습니다.

EE 특정 데이터를 설정하기 위한 클래스 메소드 활용

특정 API 경로에 대해 서로 다른 인수를 사용해야 하는 경우가 있습니다. Grape는 다른 블록에서 서로 다른 컨텍스트를 가지므로 EE 모듈로 쉽게 확장할 수 없습니다. 이를 극복하기 위해, 데이터를 별도의 모듈이나 클래스에 있는 클래스 메소드로 이동해야 합니다. 이렇게 하면 CE 코드 중간에 prepend_mod_with를 배치할 필요 없이 데이터를 사용하기 전에 해당 모듈이나 클래스를 확장할 수 있습니다.

예를 들어, 한 곳에서는 at_least_one_of에 추가 인수를 전달해야 하며, 이것이 API가 EE 전용 인수를 최소 인수로 고려할 수 있도록 합니다. 우리는 다음과 같이 접근할 수 있습니다:

# api/merge_requests/parameters.rb
module API
  class MergeRequests < Grape::API::Instance
    module Parameters
      def self.update_params_at_least_one_of
        %i[
          assignee_id
          description
        ]
      end
    end
  end
end

API::MergeRequests::Parameters.prepend_mod_with('API::MergeRequests::Parameters')

# api/merge_requests.rb
module API
  class MergeRequests < Grape::API::Instance
    params do
      at_least_one_of(*Parameters.update_params_at_least_one_of)
    end
  end
end

그런 다음 EE 클래스 메소드에서 해당 인수를 쉽게 확장할 수 있습니다:

module EE
  module API
    module MergeRequests
      module Parameters
        extend ActiveSupport::Concern

        class_methods do
          extend ::Gitlab::Utils::Override

          override :update_params_at_least_one_of
          def update_params_at_least_one_of
            super.push(*%i[
              squash
            ])
          end
        end
      end
    end
  end
end

많은 경로에서 이를 필요로 한다면 번거로울 수 있지만, 지금으로서는 가장 간단한 해결책일 수 있습니다.

이 접근 방식은 모델이 클래스 메소드에 의존하는 유효성을 정의할 때도 사용할 수 있습니다. 예를 들어:

# app/models/identity.rb
class Identity < ActiveRecord::Base
  def self.uniqueness_scope
    [:provider]
  end

  prepend_mod_with('Identity')

  validates :extern_uid,
    allow_blank: true,
    uniqueness: { scope: uniqueness_scope, case_sensitive: false }
end

# ee/app/models/ee/identity.rb
module EE
  module Identity
    extend ActiveSupport::Concern

    class_methods do
      extend ::Gitlab::Utils::Override

      def uniqueness_scope
        [*super, :saml_provider_id]
      end
    end
  end
end

이 접근 방식을 취하기보다는 코드를 다음과 같이 리팩토링할 수 있습니다:

# ee/app/models/ee/identity/uniqueness_scopes.rb
module EE
  module Identity
    module UniquenessScopes
      extend ActiveSupport::Concern

      class_methods do
        extend ::Gitlab::Utils::Override

        def uniqueness_scope
          [*super, :saml_provider_id]
        end
      end
    end
  end
end

# app/models/identity/uniqueness_scopes.rb
class Identity < ActiveRecord::Base
  module UniquenessScopes
    def self.uniqueness_scope
      [:provider]
    end
  end
end

Identity::UniquenessScopes.prepend_mod_with('Identity::UniquenessScopes')

# app/models/identity.rb
class Identity < ActiveRecord::Base
  validates :extern_uid,
    allow_blank: true,
    uniqueness: { scope: Identity::UniquenessScopes.scopes, case_sensitive: false }
end

spec/의 코드

EE 전용 기능을 테스트할 때는 기존 CE 사양에 예제를 추가하지 마세요.

또한 EE가 라이센스 없이 실행될 때 기존 CE 예제가 정상적으로 작동해야 하므로 기존의 CE 예제를 변경하지 마세요.

대신 EE 사양은 ee/spec 폴더에 배치하세요.

spec/factories의 코드

정의된 CE 팩토리를 확장하려면 FactoryBot.modify를 사용하세요.

FactoryBot.modify 블록 내부에서 새로운 팩토리를 정의할 수 없습니다 (중첩된 것이라도). 아래 예제와 같이 별도의 FactoryBot.define 블록에 정의할 수 있습니다:

# ee/spec/factories/notes.rb
FactoryBot.modify do
  factory :note do
    trait :on_epic do
      noteable { create(:epic) }
      project nil
    end
  end
end

FactoryBot.define do
  factory :note_on_epic, parent: :note, traits: [:on_epic]
end

프론트엔드에서 EE 코드 분리

EE 전용 JS 파일을 분리하려면 파일을 ee 폴더로 이동하세요.

예를 들어 다음과 같은 파일이 있을 수 있습니다: app/assets/javascripts/protected_branches/protected_branches_bundle.js와 EE 대응 파일 ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js.

상응하는 import 문은 다음과 같이 보일 것입니다:

// app/assets/javascripts/protected_branches/protected_branches_bundle.js
import bundle from '~/protected_branches/protected_branches_bundle.js';

// ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
// (only works in EE)
import bundle from 'ee/protected_branches/protected_branches_bundle.js';

// in CE: app/assets/javascripts/protected_branches/protected_branches_bundle.js
// in EE: ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js
import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js';

프론트엔드에서 새로운 EE 전용 기능 추가하기

개발 중인 기능이 CE에 존재하지 않는 경우, ee/에 진입점을 추가하세요. 예를 들어:

# 마운트할 HTML 요소 추가
ee/app/views/admin/geo/designs/index.html.haml

# 애플리케이션 초기화
ee/app/assets/javascripts/pages/ee_only_feature/index.js

# 기능 마운트
ee/app/assets/javascripts/ee_only_feature/index.js

기능 보호는 일반적으로 컨트롤러에서 licensed_feature_available?License.feature_available?를 사용하여 발생합니다. 백엔드 가이드를 참조하세요.

EE 전용 프론트엔드 기능 테스트하기

EE 테스트를 ee/spec/frontend/에 추가하고 CE에 사용하는 것과 동일한 디렉터리 구조를 따르세요.

licensed features를 활성화하는 것과 관련하여 EE 전용 백엔드 기능 테스트 아래의 주석을 확인하세요.

EE 프론트엔드 코드로 CE 기능 확장하기

기존 뷰를 확장하는 프론트엔드 기능을 보호하려면 push_licensed_feature를 사용하세요:

# ee/app/controllers/ee/admin/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name) # 전역 기능
end
# ee/app/controllers/ee/group/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # 그룹 페이지
end
# ee/app/controllers/ee/project/my_controller.rb
before_action do
  push_licensed_feature(:my_feature_name, @group) # 그룹 페이지
  push_licensed_feature(:my_feature_name, @project) # 프로젝트 페이지
end

브라우저 콘솔에서 gon.licensed_features에 기능이 나타나는지 확인하세요.

Vue 애플리케이션을 EE Vue 구성 요소로 확장하기

EE 라이센스 기능은 UI의 기존 기능을 향상시키고, Vue 애플리케이션에 구성 요소로 새로운 요소나 상호작용을 추가합니다.

EE 기능을 추가하기 위해 CE 구성 요소 안에 EE 구성 요소를 가져올 수 있습니다.

ee_component 별칭을 사용하여 EE 구성 요소를 가져옵니다. EE에서는 ee_component 가져오기 별칭이 ee/app/assets/javascripts 디렉터리를 가리킵니다. CE에서는 이 별칭이 아무 것도 렌더링하지 않는 빈 구성 요소로 해결됩니다.

다음은 CE 구성 요소에 가져온 EE 구성 요소의 예입니다:

<script>
// app/assets/javascripts/feature/components/form.vue

// EE에서는 `ee/app/assets/javascripts/feature/components/my_ee_component.vue`로 해결됩니다.
// CE에서는 `app/assets/javascripts/vue_shared/components/empty_component.js`로 해결됩니다.
import MyEeComponent from 'ee_component/feature/components/my_ee_component.vue';

export default {
  components: {
    MyEeComponent,
  },
};
</script>

<template>
  <div>
    <!-- ... -->
    <my-ee-component/>
    <!-- ... -->
  </div>
</template>
note
EE 구성 요소는 CE 코드베이스 내에서 렌더링이 특정 체크(예: 기능 플래그 체크)에 의존하는 경우 비동기적으로 가져올 수 있습니다.

Vue 구성 요소가 보호되고 있는지 확인하려면 glFeatures를 확인하세요. 구성 요소는 라이센스가 있을 때만 렌더링됩니다.

<script>
// ee/app/assets/javascripts/feature/components/special_component.vue

import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';

export default {
  mixins: [glFeatureFlagMixin()],
  computed: {
    shouldRenderComponent() {
      // gon.licensed_features에서 camel-case 버전의 `my_feature_name`로 제공됨
      return this.glFeatures.myFeatureName;
    }
  },
};
</script>

<template>
  <div v-if="shouldRenderComponent">
    <!-- EE 라이센스 기능 UI -->
  </div>
</template>
note
믹스를 사용할 필요가 ABSOLUTELY NECESSARY 하지 않는 한 사용하지 마세요. 대체 패턴을 찾아보세요.
권장 대체 접근법 (이름이 지정된/스코프가 지정된 슬롯)
  • 우리는 슬롯 및/또는 스코프가 지정된 슬롯을 사용하여 믹스인으로 한 것과 동일한 것을 달성할 수 있습니다. EE 구성 요소만 필요하다면 CE 구성 요소를 만들 필요가 없습니다.
  1. 먼저, EE 템플릿과 기능이 CE 기본 위에 장식되어야 할 경우 슬롯을 렌더링할 수 있는 CE 구성 요소가 필요합니다.
// ./ce/my_component.vue

<script>
export default {
  props: {
    tooltipDefaultText: {
      type: String,
    },
  },
  computed: {
    tooltipText() {
      return this.tooltipDefaultText || "5 issues please";
    }
  },
}
</script>

<template>
  <span v-gl-tooltip :title="tooltipText" class="ce-text">커뮤니티 에디션 전용 텍스트</span>
  <slot name="ee-specific-component">
</template>
  1. 다음으로, EE 구성 요소를 렌더링하고 EE 구성 요소 내에서 CE 구성 요소를 렌더링하며 슬롯에 추가 콘텐츠를 추가합니다.
// ./ee/my_component.vue

<script>
export default {
  computed: {
    tooltipText() {
      if (this.weight) {
        return "10의 가중치로 5개의 문제";
      }
    }
  },
  methods: {
    submit() {
      // 어떤 작업을 수행합니다.
    }
  },
}
</script>

<template>
  <my-component :tooltipDefaultText="tooltipText">
    <template #ee-specific-component>
      <span class="some-ee-specific">EE 특정 값</span>
      <button @click="submit">클릭하세요</button>
    </template>
  </my-component>
</template>
  1. 마지막으로, 구성 요소가 필요한 곳에서 다음과 같이 가져올 수 있습니다.

import MyComponent from 'ee_else_ce/path/my_component'.vue

  • 이렇게 하면 CE 또는 EE 구현을 위한 올바른 구성 요소가 포함됩니다.

같은 계산된 값에 대해 다른 결과가 필요한 EE 구성 요소의 경우, 예제에서 볼 수 있듯이 CE 래퍼에 props를 전달할 수 있습니다.

  • EE 추가 HTML
    EE에 추가 HTML이 있는 템플릿의 경우 새 구성 요소로 이동하고 ee_else_ce 가져오기 별칭을 사용해야 합니다.

다른 JS 코드 확장하기

JS 파일을 확장하려면, 다음 단계를 완료하세요:

  1. ee_else_ce 헬퍼를 사용하세요. 해당 EE 전용 코드는 ee/ 폴더 안에 있어야 합니다.
    1. EE만 포함된 파일을 생성하고 CE 대응 파일을 확장하세요.
    2. 확장할 수 없는 함수 내부의 코드는 새 파일로 이동하고 ee_else_ce 헬퍼를 사용하세요:
  import eeCode from 'ee_else_ce/ee_code';

  function test() {
    const test = 'a';

    eeCode();

    return test;
  }

일부 경우, 애플리케이션의 다른 로직을 확장해야 할 수도 있습니다. JS 모듈을 확장하려면 파일의 EE 버전을 생성하고 사용자 정의 로직으로 확장하세요:

// app/assets/javascripts/feature/utils.js

export const myFunction = () => {
  // ...
};

// ... 다른 CE 함수들 ...
// ee/app/assets/javascripts/feature/utils.js
import {
  myFunction as ceMyFunction,
} from '~/feature/utils';

/* eslint-disable import/export */

// CE와 동일한 유틸리티를 내보냅니다.
export * from '~/feature/utils';

// `myFunction`만 재정의합니다.
export const myFunction = () => {
  const result = ceMyFunction();
  // EE 기능 로직 추가
  return result;
};

/* eslint-enable import/export */

EE/CE 별칭을 사용한 모듈 테스트

프론트엔드 테스트를 작성할 때, 테스트하는 모듈이 ee_else_ce/...로 다른 모듈을 가져오면, 해당 테스트에서 관련 모듈도 ee_else_ce/...로 가져와야 합니다. 이는 예기치 않은 EE 또는 FOSS 실패를 방지하고, EE가 라이센스가 없는 경우 CE처럼 동작하는지 확인하는 데 도움이 됩니다.

예를 들면:

<script>
// ~/foo/component_under_test.vue

import FriendComponent from 'ee_else_ce/components/friend.vue;'

export default {
  name: 'ComponentUnderTest',
  components: { FriendComponent }.
}
</script>

<template>
  <friend-component />
</template>
// spec/frontend/foo/component_under_test_spec.js

// ...
// 우리는 컴포넌트를 ee_else_ce로 참조했기 때문에, 스펙에서도 동일하게 해야 합니다.
import Friend from 'ee_else_ce/components/friend.vue;'

describe('ComponentUnderTest', () => {
  const findFriend = () => wrapper.find(Friend);

  it('renders friend', () => {
    // 만약 우리가 `ee/component...`로 했다면 CE에서 실패했을 것입니다.
    // 그리고 `~/component...`로 했다면 EE에서 실패했을 것입니다.
    expect(findFriend().exists()).toBe(true);
  });
});

assets/stylesheets의 SCSS 코드

스타일을 추가할 컴포넌트가 EE로 제한되는 경우, 적절한 디렉토리 내에 별도의 SCSS 파일을 두는 것이 좋습니다.

일부 경우, 이는 완전히 가능하지 않거나 전용 SCSS 파일을 만드는 것이 과도할 수 있습니다. 예를 들어, 특정 컴포넌트의 텍스트 스타일이 EE와 다릅니다. 이러한 경우, 스타일은 일반적으로 CE와 EE 모두에 공통된 스타일시트에 유지되며, CE에서 EE로 병합할 때 충돌을 피하기 위해 이런 규칙 세트를 나머지 CE 규칙에서 분리하는 것이 현명합니다 (같은 설명을 추가하는 댓글도 함께).

// 나쁨
.section-body {
  .section-title {
    background: $gl-header-color;
  }

  &.ee-section-body {
    .section-title {
      background: $gl-header-color-cyan;
    }
  }
}
// 좋음
.section-body {
  .section-title {
    background: $gl-header-color;
  }
}

// EE 전용 시작
.section-body.ee-section-body {
  .section-title {
    background: $gl-header-color-cyan;
  }
}
// EE 전용 끝

GitLab-svgs

app/assets/images/icons.json 또는 app/assets/images/icons.svg에 있는 충돌은

yarn run svg로 해당 자산을 재생성하여 해결할 수 있습니다.