기업용 기능 구현 가이드

  • 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 SaaS 기능의 이름
introduced_by_url 아니요 SaaS 기능을 도입한 합병 요청의 URL
milestone 아니요 SaaS 기능이 생성된 마일스톤
group 아니요 기능 플래그 소유 그룹 (https://handbook.gitlab.com/handbook/product/categories/#devops-stages)

새로운 SaaS 기능 파일 정의 생성

GitLab 코드베이스에는 새로운 SaaS 기능 정의를 만들기 위한 전용 도구인 bin/saas-feature.rb이 제공됩니다. 이 도구는 새로운 SaaS 기능에 대해 다양한 질문을 하고, 나중에 ee/config/saas_features에 YAML 정의를 생성합니다.

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

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

>> (MR 폐기되었을 경우 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

SaaS-only 기능을 CE의 기능으로 사용하지 마십시오

Gitlab::Saas.feature_available?는 CE에 나타나서는 안 됩니다. CE를 EE 백엔드 코드로 확장하는 가이드를 참조하세요.

테스트에서의 SaaS-only 기능

코드베이스에 SaaS 전용 기능을 도입하면 테스트해야 할 추가 코드 경로가 생성됩니다. SaaS 전용 기능에 영향을 받는 모든 코드에 대해 자동화된 테스트를 포함하는 것이 권장됩니다. 이러한 테스트는 기능이 제대로 작동하는지를 확인하기 위해 활성화비활성화 상태일 때 모두 포함해야 합니다.

테스트에서 SaaS-only 기능을 활성화하려면 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. 왼쪽 사이드바에서 아래쪽에서 관리 영역을 선택합니다.
    2. 왼쪽 사이드바에서 설정 > 일반을 선택합니다.
    3. 계정 및 제한을 확장합니다.
    4. 라이선스가 있는 EE 기능의 사용 허용 확인란을 선택합니다.
    5. 변경 사항 저장을 선택합니다.
  3. 테스트할 그룹이 실제로 EE 계획을 사용하는지 확인하세요:
    1. 왼쪽 사이드바에서 아래쪽에서 관리 영역을 선택합니다.
    2. 왼쪽 사이드바에서 개요 > 그룹을 선택합니다.
    3. 수정하려는 그룹을 식별하고 편집을 선택합니다.
    4. 권한 및 그룹 기능으로 스크롤합니다. 계획에서 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. 이 기능은 전역적으로(시스템 전체 수준에서) 사용 가능한가요?
    • 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 features to work with unlicensed EE instance의 구현 후에는 GitLab Enterprise Edition이 라이선스가 활성화되지 않은 경우에는 GitLab Community Edition처럼 작동합니다.

CE 사양은 가능한 한 원래대로 유지하고 추가 사양은 EE를 위해 추가해야 합니다. 라이선스 기능은 EE::LicenseHelpers의 spec 도우미 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 jobs and cross project downstream pipeline 파이프라인 문서를 참조하십시오.

백엔드에서 EE 코드 분리

EE 전용 기능

개발 중인 기능이 CE에 형태로는 없는 경우 해당 코드를 EE 네임스페이스 아래에 둘 필요가 없습니다. 예를 들어, EE 모델은 다음과 같이 사용될 수 있습니다: ee/app/models/awesome.rb, 클래스 이름은 Awesome입니다. 모델에만 해당되는 것은 아닙니다. 다음은 다른 예시 목록입니다:

  • 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/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)

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

기존 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를 사용하십시오. 이를 통해 클래스의 메서드를 모듈의 메서드로 재정의할 수 있으며, super를 사용하여 클래스의 구현에 여전히 액세스할 수 있습니다.

이에는 몇 가지 주의해야 할 사항이 있습니다.

  • 항상 extend ::Gitlab::Utils::Override를 사용하고 override를 활용하여 override 메서드를 보호해야 합니다. 이렇게 하면 CE에서 메서드가 이름이 바뀌어도 EE 재정의가 묵시적으로 잊혀지지 않습니다.
  • ‘overrider’가 CE 구현을 중간에 바꿀 때는 CE 메서드를 리팩터링하고 더 작은 메서드로 분할해야 합니다. 또는 CE에 비어 있는 “훅” 메서드를 만들거나, EE별 구현이 있는 “훅” 메서드를 만들어야 합니다.
  • 원래 구현이 가드 절(예: return unless condition)을 포함하는 경우, 재정의된 메서드(즉, 재정의된 메서드에서 super를 호출하는 경우)가 언제 일찍 중지되길 원하는 지 알 수 없기 때문에 메서드를 간단히 재정의할 수 없습니다. 이 경우, 단순히 재정의하는 것이 아니라 원본 메서드를 업데이트하여, 우리가 확장하고자 하는 다른 메서드를 호출하도록 만드는 것이 더 좋습니다. 템플릿 메서드 패턴과 같은 것입니다. 예를 들어, 기본이 다음과 같은 경우:

      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
          # 상위 메서드를 호출하고 확장하는 방법을 따르세요
        end
      end
    

prepend를 할 때, 특정 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/config/routes/ee_only.rbrender_if_exists와 유사하게 EE에 추가할 수 있습니다.

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::Model을 확장해야 합니다.

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

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

app/views의 코드

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

완화책

EE 특정 뷰 코드 블록은 부분(partial)로 이동해야 합니다. 이렇게 하면 들여쓰기를 추가할 때 발생하는 충돌을 피할 수 있습니다.

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

render_if_exists 사용

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

이의 장점:

  • 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 부분을 찾습니다. 특정 부분만 렌더링하며, 동일한 부분 경로(예: projects/settings/archive)가 CE에서는 CE 부분(즉, app/views/projects/settings/_archive.html.haml)을, EE에서는 EE 부분(즉, ee/app/views/projects/settings/_archive.html.haml)을 나타낼 수 있습니다. 이렇게하여 CE와 EE 간에 서로 다른 내용을 표시할 수 있습니다.

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

이 경우, render_ce를 사용하여 EE 부분을 무시할 수 있습니다. 예를 들어 ee/app/views/projects/settings/_archive.html.haml에서:

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

위 예제에서는 render 'projects/settings/archive' 대신 render_ce를 사용해야 합니다. 그렇지 않으면 동일한 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: '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 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에 그 인터페이스를 오버라이드하려면, CE에서 먼저 “인터페이스”를 정의해야 합니다. Grape 내부가 복잡하여 쉽게 이를 수행할 수 없기 때문에 일반적인 객체 지향 관행을 따릅니다.

예를들어, EE에 몇 가지 추가적인 선택적 파라미터가 있다고 가정해봅시다. 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')

해당 CE API params를 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-특정 동작이 필요합니다. 보통 CE 메서드를 재정의하기 위해 EE 메서드를 사용할 수 있지만, API 경로는 메서드가 아니기 때문에 재정의할 수 없습니다. 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를 배치할 필요 없이 데이터가 사용되기 전에 해당 모듈이나 클래스를 확장할 수 있습니다.

예를 들어, 특정 API 경로에 추가 인수를 전달해야 할 때 다음과 같이 처리할 수 있습니다.

# 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.scope, 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가 있을 수 있습니다. 해당하는 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
// (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?의 기능 보호는 컨트롤러에서 처리되며 백엔드 가이드에 설명되어 있습니다.

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

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

라이센스 기능을 활성화하는 데 관한 EE 전용 백엔드 기능 테스트 아래의 노트를 확인하세요.

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에 기능이 나타나는지 확인하세요.

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

UI에서 기존 기능을 향상시키는 EE 라이선스 기능은 컴포넌트로 새로운 요소나 상호작용을 추가합니다.

템플릿 차이를 분리하려면 CE 컴포넌트 내에서 EE 컴포넌트를 비동기적으로 가져와야 합니다.

이렇게 하면 EE에서 올바른 컴포넌트를 로드하고, CE에서는 아무 것도 렌더링하지 않는 빈 컴포넌트를 로드합니다. 해당 코드는 CE 리포지토리에 있어야 합니다. EE 리포지토리 외에도 추가되어야 합니다.

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() {
      // gon.licensed_features에서 가져온 것이며 `my_feature_name`의 낙타 표기법 버전입니다.
      return this.glFeatures.myFeatureName;
    }
  },
};
</script>

<template>
  <div v-if="shouldRenderComponent">
    <!-- EE 라이선스 기능 UI -->
  </div>
</template>

참고: 가능하다면 반드시 필요할 때에만 믹스인을 사용하세요. 대안 패턴을 찾아보세요.

권장 대안 접근 방식 (이름 지정/범위 지정 슬롯)
  • 우리는 mixin으로 한 것과 동일한 결과를 얻기 위해 슬롯 및/또는 범위 지정 슬롯을 사용할 수 있습니다. EE 구성 요소만 필요한 경우 CE 구성 요소를 만들 필요가 없습니다.
  1. 먼저, EE 템플릿 및 기능이 CE 기본 구성 요소 위에 장식되어야 하는 경우에 CE 구성 요소가 슬롯을 렌더링할 수 있는 CE 구성 요소가 있습니다.
// ./ce/my_component.vue

<script>
export default {
  props: {
    tooltipDefaultText: {
      type: String,
    },
  },
  computed: {
    tooltipText() {
      return this.tooltipDefaultText || "5개의 이슈입니다";
    }
  },
}
</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 자식 구성 요소
    • 우리가 구성 요소를 불러오기 위해 비동기 로딩을 사용하는 경우에는 여전히 구성 요소의 이름을 사용해야하며, 이 예제를 확인하세요.
  • 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 */

// CE와 동일한 유틸리티 내보내기
export * from '~/feature/utils';

// `myFunction`만 오버라이드
export const myFunction = () => {
  const result = ceMyFunction();
  // EE 기능 로직을 추가합니다.
  return result;
};

/* eslint-enable import/export */

EE/CE 별칭을 사용하여 모듈 테스트하기

Frontend 테스트를 작성할 때 테스트 중인 모듈이 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('친구를 렌더링합니다', () => {
    // 이는 CE에서 `ee/component...`로 한 경우에 실패하며, EE에서 `~/component...`로 한 경우에도 실패합니다.
    expect(findFriend().exists()).toBe(true);
  });
});

assets/stylesheets의 SCSS 코드

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

일부 경우에는 이것이 완전히 가능하지 않거나 전용 SCSS 파일을 생성하는 것이 지나치게 과하다면, 예를 들어, 어떤 컴포넌트의 텍스트 스타일이 EE에 따라 다르다면 이러한 경우에는 스타일을 일반적으로 CE와 EE 모두에 대한 공통 스타일시트에 유지하고, 이러한 규칙을 CE 규칙에서 격리하는 것이 현명합니다(동시에 동일한 내용을 설명하는 주석을 추가함으로써). 이렇게하면 CE에서 EE로의 병합 중 충돌을 피할 수 있습니다.

// 나쁨
.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를 사용하여 해당 에셋을 다시 생성함으로써 해결할 수 있습니다.