기업용 에디션 기능 구현 가이드라인

  • ee/ 디렉터리에 코드 배치: 기업용 에디션(EE) 코드를 ee/ 최상위 디렉터리에 모두 배치합니다. 나머지 코드는 가능한 커뮤니티 에디션(CE) 파일과 가까워야 합니다.
  • 테스트 작성: 모든 EE 기능은 회귀를 방지하기 위해 좋은 테스트 커버리지가 있어야 합니다. 모든 ee/ 코드는 ee/에서 해당하는 테스트를 가져야 합니다.
  • 문서 작성: doc/ 디렉터리에 문서를 추가합니다. 해당 기능을 설명하고 적용되는 경우 스크린샷을 포함하세요. 어느 에디션에 해당하는지 표시하세요.
  • www-gitlab-com 프로젝트에 MR 제출: 새로운 기능을 EE 기능 디렉터리에 추가합니다.

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 yes SaaS 기능의 이름
introduced_by_url no SaaS 기능을 도입한 Merge Request의 URL
milestone no SaaS 기능이 생성된 마일스톤
group no 피처 플래그를 소유하는 그룹

새로운 SaaS 기능 파일 정의하기

GitLab 코드베이스는 새로운 SaaS 기능 정의를 생성하는 전용 도구인 bin/saas-feature.rb를 제공합니다. 이 도구는 새로운 SaaS 기능에 대해 여러 질문을 하고, 그런 다음 ee/config/saas_features에 YAML 정의를 생성합니다.

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

❯ bin/saas-feature my_saas_feature
'group::acquisition' 그룹을 선택하셨습니다.

>> SaaS 기능을 도입한 Merge Request의 URL(건너뛰려면 빈칸을 누르고, Danger에서 MR에 대한 제안을 직접 제공하도록 합니다):
?> https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38602
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에 나타나서는 안 됩니다. EE 백엔드 코드로 CE 기능 확장을 참조하세요.

테스트에서의 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 인스턴스에 환경 변수를 전달하는 여러 가지 방법이 있습니다. 예를 들어, 위 스니펫을 사용하여 GDK 루트에 env.runit 파일을 생성할 수 있습니다.

  2. 라이선스된 EE 기능 사용 허용을 활성화하여 라이선스된 EE 기능을 프로젝트에 사용할 수 있도록 합니다.

    1. 왼쪽 사이드바에서 맨 아래쪽에 있는 관리 영역(Admin Area)을 선택합니다.
    2. 왼쪽 사이드바에서 설정 > 일반(General)을 선택합니다.
    3. 계정 및 제한(Account and limit)을 확장합니다.
    4. 라이선스된 EE 기능 사용 허용 확인란을 선택합니다.
    5. 변경 사항 저장을 선택합니다.
  3. 특정 그룹에서 EE 기능을 테스트하려면 해당 그룹이 EE 요금제를 실제로 사용하고 있는지 확인하세요:

    1. 왼쪽 사이드바에서 맨 아래쪽에 있는 관리 영역(Admin Area)을 선택합니다.
    2. 왼쪽 사이드바에서 개요 > 그룹(Overview > Groups)을 선택합니다.
    3. 수정하려는 그룹을 식별하고 편집(Edit)을 선택합니다.
    4. 권한 및 그룹 기능(Permissions and group features)로 스크롤하십시오. 요금제(Plan)에서 Ultimate을 선택합니다.
    5. 변경 사항 저장을 선택합니다.

새로운 EE 기능 구현

GitLab Premium 또는 GitLab Ultimate 라이선스 기능을 개발 중이라면 다음 단계를 사용하여 새로운 기능을 추가하거나 확장하세요.

GitLab 라이선스 기능은 ee/app/models/gitlab_subscriptions/features.rb에 추가됩니다. 이 파일을 어떻게 수정해야 하는지 결정하려면 먼저 제품 관리자와 라이선싱에 대해 논의하십시오.

다음 질문을 활용하세요:

  1. 이것은 새로운 기능인가요, 아니면 기존의 라이선스 기능을 확장하는 것인가요?
    • 기존에 해당 기능이 있는 경우에는 features.rb을 수정할 필요는 없지만, 기존의 기능 식별자를 보호해야 합니다.
    • 이것이 새로운 기능이라면 features.rb 파일에 추가할 my_feature_name과 같은 식별자를 결정하십시오.
  2. 이것은 GitLab Premium 또는 GitLab Ultimate 기능인가요?
    • 사용할 계획에 따라 기능 식별자를 PREMIUM_FEATURES 또는 ULTIMATE_FEATURES에 추가하십시오.
  3. 이 기능은 전역적으로 사용 가능한가요 (GitLab 인스턴스 수준에서 시스템 전체로)?
    • Geo데이터베이스 로드 밸런싱과 같은 기능들은 인스턴스 전체에서 사용되며 개별 사용자 네임스페이스로 제한할 수 없습니다. 이러한 기능들은 인스턴스 라이선스에 정의됩니다. 이러한 기능들을 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 인스턴스 모의 실행

GitLab CE 기능을 라이선스가 없는 EE 인스턴스와 함께 사용하도록 구현한 후, GitLab Enterprise Edition은 라이선스가 활성화되지 않은 경우 GitLab Community Edition처럼 작동합니다.

CE 사양은 가능한 한 건드리지 말아야 하며, EE용으로 추가 사양을 추가해야 합니다. 라이선스 기능은 EE::LicenseHelpersstub_licensed_features를 사용하여 스텁화할 수 있습니다.

ee/ 디렉터리를 삭제하거나 FOSS_ONLY 환경 변수true로 평가되는 값으로 설정하여 GitLab가 CE로 작동하도록 강제할 수 있습니다. 테스트 실행에도 동일한 방법을 사용할 수 있습니다 (예: 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 파이프라인 실행

기본적으로 개발용 Merge Request 파이프라인은 EE 컨텍스트에서만 실행됩니다. FOSS 및 EE 간에 차이가 있는 기능을 개발 중이라면 FOSS 컨텍스트에서도 파이프라인을 실행하고 싶을 수 있습니다.

양쪽 컨텍스트에서 파이프라인을 실행하려면 Merge Request에 ~"pipeline:run-as-if-foss" 라벨을 추가하십시오.

자세한 정보는 As-if-FOSS jobs and cross project downstream pipeline 파이프라인 문서를 참조하십시오.

백엔드에서 EE 코드 분리

EE 전용 기능

개발 중인 기능이 CE에 어떤 형태로든 존재하지 않는 경우, 해당 코드를 EE 네임스페이스에 넣을 필요가 없습니다. 예를 들어, EE 모델은 다음과 같이 배치될 수 있습니다: ee/app/models/awesome.rbAwesome 클래스 이름을 사용합니다. 이는 모델에만 적용되는 것이 아닙니다. 다음은 다른 예시들의 디렉터리입니다:

  • 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 경로에 있는 모든 경로에 대해 config/application.rb에 동일한 ee/ 접두어 경로를 추가하기 때문에 작동합니다. 이는 뷰에도 적용됩니다.

EE 전용 백엔드 기능 테스트

CE에 존재하지 않는 EE 클래스를 테스트하려면 보통처럼 ee/spec 디렉터리에 특정 ee/ 하위 디렉터리 없이 특정 ee/ 하위 디렉터리 없이 spec 파일을 생성합니다. 예를 들어, 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로의 Merge 과정에서 충돌이 발생할 가능성이 줄어들기 때문입니다. 예를 들어, 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/ 하위 디렉터리를 포함하여 ee/spec 디렉터리에 특정 ee/ 하위 디렉터리 없이 특정 ee/ 하위 디렉터리 없이 spec 파일을 생성합니다. 예를 들어, 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 feature added through extension'
end

CE 메소드 오버라이딩

CE 코드베이스에 존재하는 메소드를 오버라이드하려면 prepend를 사용합니다. 이를 통해 해당 클래스의 메소드를 모듈의 메소드로 오버라이드하면서도 super를 사용하여 클래스의 구현에 계속 액세스할 수 있습니다.

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

  • 항상 extend ::Gitlab::Utils::Override를 사용하고 override를 사용하여 overrider 메소드를 가드해야 합니다. 이렇게 함으로써 CE에서 메소드가 이름이 바뀌더라도 EE 오버라이드가 무시되지 않도록 보장합니다.
  • overrider가 CE 구현의 중간에 줄을 추가할 경우, CE 메소드를 리팩터링하고 여러 작은 메소드로 나눌 필요가 있습니다. 또는 CE에서 빈 “훅” 메소드를 생성하거나, 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로 감싸야 합니다.

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

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.rb에서 draw :admin을 추가하면, 애플리케이션이 config/routes/admin.rb에 있는 파일을 로드하려고 시도하고 동시에 ee/config/routes/admin.rb에 있는 파일을 로드하려고 합니다.

EE에서는 최소한 한 파일을 로드해야 합니다. 최대 두 파일까지 로드할 수 있습니다. 파일을 찾을 수 없다면 오류가 발생합니다. CE의 경우, EE 경로가 있는지 모르기 때문에 아무것도 찾을 수 없더라도 오류가 발생하지 않습니다.

따라서 특정 CE route 파일을 확장하려면 ee/config/routes에 동일한 파일을 추가하면 되고, EE 전용 route를 추가하려면 CE 및 EE 모두에 draw :ee_only를 추가한 다음 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

# use case에 가장 적합한 방법으로 작성된 심볼 배열을 항상 반환합니다.
# 알파벳 순으로 정렬되어야 합니다.
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::Model을 확장해야 합니다.

예를 들어, EE에 특정 Tanuki 모델이 있다면 ee/app/models/ee/tanuki.rb에 배치해야 합니다.

ActiveRecord enumsFOSS에서만 정의되어야 합니다.

app/views의 코드

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

대처법

EE 전용 코드 블록은 부분에 이동해야 합니다. 이렇게 하면 들여쓰기를 추가할 때 발생하는 충돌을 피할 수 있습니다.

EE 전용 뷰는 ee/app/views/에 배치해야 하며 적절한 경우 추가 하위 디렉터리를 사용해야 합니다.

render_if_exists 사용

일반적인 render 대신 render_if_exists를 사용해야 합니다. 이는 특정 부분을 찾을 수 없는 경우 아무것도 렌더링하지 않습니다. CE 및 EE 간의 코드를 동일하게 유지하기 위해 이렇게 합니다.

이의 장점은 다음과 같습니다.

  • CE 코드를 읽는 동안 어디에서 EE 뷰를 확장하고 있는지에 대해 매우 명확한 힌트를 제공합니다.

이의 단점은 다음과 같습니다.

  • 부분 이름에 오타가 있으면 조용히 무시됩니다.
주의 사항

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

- # 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 및 EE 사이에 다른 내용을 표시할 수 있습니다.

그러나 때로는 CE 부분을 EE 부분에서 재사용하고 싶을 수도 있습니다. 이를 해결하기 위해 다른 이름의 부분을 추가할 수 있지만 이는 번거로울 수 있습니다.

이 경우, 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 오버라이드를 다음과 같이 만듭니다.

module EE
  module Mutations
    module Tanukis
      module Create
        extend ActiveSupport::Concern
        
        prepended do
          argument :name,
                   GraphQL::Types::String,
                   required: false,
                   description: 'Tanuki name'
        end
      end
    end
  end
end

lib/의 코드

EE 전용 로직은 최상위 EE 모듈 네임스페이스에 배치합니다. 클래스는 일반적으로 EE 모듈 아래에 네임스페이스를 지정합니다.

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

lib/api/의 코드

EE 특징을 prepend_mod_with의 단일 라인으로 확장하는 것은 매우 까다로울 수 있으며, 각기 다른 Grape 기능마다 다른 전략이 필요할 수 있습니다. 다양한 전략을 쉽게 적용하기 위해, EE 모듈에서 extend ActiveSupport::Concern을 사용합니다.

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

EE API 라우트

EE API 라우트에 대해서는 prepended 블록에 배치합니다.

module EE
  module API
    module MergeRequests
      extend ActiveSupport::Concern
      
      prepended do
        params do
          requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
        end
        resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
          # ...
        end
      end
    end
  end
end

일부 상수에 대해 전체 한정자를 사용해야 하는 차이로 인해 주의해야 합니다.

EE 파라미터

우리는 params를 정의하고 CE에서 오버라이드되도록 “인터페이스”를 먼저 정의해야 합니다. 그렇지 않으면 Grape는 내부적으로 복잡하므로, 모든 위치에 이를 해 줄 필요가 없지만, 일반적인 객체 지향적인 사례를 따르기 때문에 그렇게 해야 합니다.

예를 들어, 몇 가지 추가적인 선택적 매개변수가 있다고 가정해 봅시다. 우리는 매개변수를 Grape::API::Instance 클래스에서 helper 모듈로 이동하여 클래스에서 사용되기 전에 주입할 수 있도록 합니다.

이렇게 주어진 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 route_setting을 EE 모듈에서 확장하는 것은 매우 어렵습니다. 특정 루트에 대한 메타 데이터를 저장하고 있습니다. 이에 따라, CE에서는 사용되지 않기 때문에 CE에 route_setting을 남겨둘 수 있습니다.

route_setting을 더 많이 사용하게 되고 EE에서 확장해야 하는지 여부를 결정할 수 있습니다. 현재는 그것을 많이 사용하고 있지 않습니다.

유틸리징 클래스 메소드를 사용하여 EE-특정 데이터 설정

가끔씩 특정 API 루트에 대해 다른 인자를 사용해야 할 때가 있습니다. 그리고 Grape는 서로 다른 블록에서 다른 컨텍스트를 갖기 때문에 쉽게 EE 모듈로 확장할 수 없습니다. 이를 극복하기 위해 데이터를 별도의 모듈 또는 클래스에 있는 클래스 메소드로 이동해야 합니다. 이렇게 하면 데이터가 사용되기 전에 해당 모듈이나 클래스를 확장할 수 있으며, CE 코드 중간에 prepend_mod_with를 배치할 필요가 없습니다.

예를 들어, 특정 위치에서 API가 EE 전용 인자를 최소 인자로 고려할 수 있도록 at_least_one_of에 추가적인 인자를 전달해야 할 수 있습니다. 이를 다음과 같이 접근할 수 있습니다:

# 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에 코드 추가

기존에 정의된 팩토리를 확장하기 위해 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가 있을 수 있습니다. 그에 상응하는 가져오기 문은 다음과 같아야 합니다:

// 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
// (EE에서만 작동)
import bundle from 'ee/protected_branches/protected_branches_bundle.js';

// CE에서: app/assets/javascripts/protected_branches/protected_branches_bundle.js
// 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?를 기능 가드에 추가하는 것은 일반적으로 컨트롤러에서 수행됩니다. (백엔드 가이드에 설명되어 있음).

Testing EE-only frontend features

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

Testing EE-only backend features 아래의 노트를 확인하여 라이선스가 필요한 기능을 활성화하는지 확인하세요.

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

기존 뷰를 확장하는 프론트엔드 기능을 가드하기 위해 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에서 기능이 나타나는지 확인하세요.

EE Vue 컴포넌트로 Vue 애플리케이션 확장

UI를 향상시키는 EE 라이선스 기능은 컴포넌트로서 새로운 요소나 상호작용을 추가하므로 Vue 템플릿 차이를 분리하려면 상위 Vue 템플릿과 함께 EE 컴포넌트를 가져와야합니다. 이를 위해 비동기적으로 가져와야합니다.

이를 통해 CE에는 아무것도 렌더링하지 않는 빈 컴포넌트를 로드하지만, GitLab은 EE에서 올바른 컴포넌트를 로드합니다. 이 코드는 CE 리포지터리에 있어야합니다.

CE 컴포넌트는 EE 기능의 진입점 역할을합니다. EE 컴포넌트를 추가하려면 ee/ 디렉터리에서 찾아야하며 import('ee_component/...')을 사용하여 추가해야합니다:

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

export default {
  mixins: [glFeatureFlagMixin()],
  components: {
    // CE에서 EE 컴포넌트 가져오기
    MyEeComponent: () => import('ee_component/components/my_ee_component.vue'),
  },
};
</script>

<template>
  <div>
    <!-- ... -->
    <my-ee-component/>
    <!-- ... -->
  </div>
</template>

glFeatures를 확인하여 Vue 컴포넌트가 가드되었는지 확인하세요. 라이선스가있는 경우에만 컴포넌트가 렌더링됩니다.

<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() {
      // `my_feature_name`의 캐멀 케이스 버전으로서 `gon.licensed_features`에서 가져옵니다.
      return this.glFeatures.myFeatureName;
    }
  },
};
</script>

<template>
  <div v-if="shouldRenderComponent">
    <!-- EE 라이선스 기능 UI -->
  </div>
</template>
note
필요한 경우에만 mixin을 사용하지 마십시오. 대안 패턴을 찾으십시오.
권장되는 대체 접근법 (네임드/스코프드 슬롯)
  • 우리는 CE 컴포넌트를 만들어 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-t구체적-구성요소">
</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="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 자식 컴포넌트
    • 우리는 어떤 컴포넌트를 로드할지 확인하기 위해 비동기로 설정된 컴포넌트 이름을 사용하기 때문에 이 예제를 확인하십시오.
  • EE 추가 HTML
    • EE에 추가 HTML이 있는 템플릿의 경우 새로운 컴포넌트로 이동하고 ee_else_ce 동적 가져오기를 사용해야합니다.

기타 JS 코드 확장

JS 파일을 확장하려면 다음 단계를 완료하십시오.

  1. ee/ 폴더 내에 EE 전용 코드가 있어야하는 경우 ee_else_ce 헬퍼를 사용합니다.
    1. EE 전용 코드가있는 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 */

// 동일한 utils를 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를 사용했으므로 spec에서도 동일하게 해야합니다.
import Friend from 'ee_else_ce/components/friend.vue;'

describe('ComponentUnderTest', () => {
  const findFriend = () => wrapper.find(Friend);
  
  it('renders friend', () => {
    // 이 코드는 CE에서 `ee/component...`를 사용하면 실패하고
    // EE에서 `~/component...`를 사용하면 실패합니다.
    expect(findFriend().exists()).toBe(true);
  });
});

assets/stylesheets의 SCSS 코드

스타일을 추가하는 컴포넌트가 EE에 한정되어 있는 경우 app/assets/stylesheets 내의 적절한 디렉터리에 별도의 SCSS 파일을 갖는 것이 좋습니다.

일부 경우에는 이것이 완전히 가능하지 않거나 전용 SCSS 파일을 만드는 것이 지나치게 복잡한 경우도 있습니다. 예를 들어 특정 컴포넌트의 텍스트 스타일이 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로 해당 자산을 다시 생성하여 해결할 수 있습니다.