Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									36ba0f6636
								
							
						
					
					
						commit
						664a3c2024
					
				| 
						 | 
				
			
			@ -786,7 +786,6 @@ RSpec/FeatureCategory:
 | 
			
		|||
    - 'ee/spec/models/ee/pages_deployment_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/preloaders/group_policy_preloader_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/project_authorization_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/project_statistics_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/protected_ref_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/release_spec.rb'
 | 
			
		||||
    - 'ee/spec/models/ee/resource_label_event_spec.rb'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,7 +123,7 @@
 | 
			
		|||
{"name":"devise","version":"4.9.3","platform":"ruby","checksum":"480638d6c51b97f56da6e28d4f3e2a1b8e606681b316aa594b87c6ab94923488"},
 | 
			
		||||
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
 | 
			
		||||
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
 | 
			
		||||
{"name":"diffy","version":"3.4.2","platform":"ruby","checksum":"36b42ffbe5138ddc56182107c24ad8d6b066ecfd2876829f391e3a4993d89ae1"},
 | 
			
		||||
{"name":"diffy","version":"3.4.3","platform":"ruby","checksum":"4264b9e7db00d1cd426fcd32e36565779163cedc2340a95b0e6f025e71f9aaa7"},
 | 
			
		||||
{"name":"digest-crc","version":"0.6.5","platform":"ruby","checksum":"5ca456f3352dc5ff17eb95deb3dd5a79dc79f8bf751d8005abca5b7b9b252124"},
 | 
			
		||||
{"name":"discordrb-webhooks","version":"3.5.0","platform":"ruby","checksum":"52fba8bce3b08059d4a41a1e73a9a152958e788a9330275450126b44f01c23b1"},
 | 
			
		||||
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -525,7 +525,7 @@ GEM
 | 
			
		|||
      railties (~> 7.0)
 | 
			
		||||
      rotp (~> 6.0)
 | 
			
		||||
    diff-lcs (1.5.0)
 | 
			
		||||
    diffy (3.4.2)
 | 
			
		||||
    diffy (3.4.3)
 | 
			
		||||
    digest-crc (0.6.5)
 | 
			
		||||
      rake (>= 12.0.0, < 14.0.0)
 | 
			
		||||
    discordrb-webhooks (3.5.0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,7 +123,7 @@
 | 
			
		|||
{"name":"devise","version":"4.9.3","platform":"ruby","checksum":"480638d6c51b97f56da6e28d4f3e2a1b8e606681b316aa594b87c6ab94923488"},
 | 
			
		||||
{"name":"devise-two-factor","version":"4.1.1","platform":"ruby","checksum":"c95f5b07533e62217aaed3c386874d94e2d472fb5f2b6598afe8600fc17a8b95"},
 | 
			
		||||
{"name":"diff-lcs","version":"1.5.0","platform":"ruby","checksum":"49b934001c8c6aedb37ba19daec5c634da27b318a7a3c654ae979d6ba1929b67"},
 | 
			
		||||
{"name":"diffy","version":"3.4.2","platform":"ruby","checksum":"36b42ffbe5138ddc56182107c24ad8d6b066ecfd2876829f391e3a4993d89ae1"},
 | 
			
		||||
{"name":"diffy","version":"3.4.3","platform":"ruby","checksum":"4264b9e7db00d1cd426fcd32e36565779163cedc2340a95b0e6f025e71f9aaa7"},
 | 
			
		||||
{"name":"digest-crc","version":"0.6.5","platform":"ruby","checksum":"5ca456f3352dc5ff17eb95deb3dd5a79dc79f8bf751d8005abca5b7b9b252124"},
 | 
			
		||||
{"name":"discordrb-webhooks","version":"3.5.0","platform":"ruby","checksum":"52fba8bce3b08059d4a41a1e73a9a152958e788a9330275450126b44f01c23b1"},
 | 
			
		||||
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -537,7 +537,7 @@ GEM
 | 
			
		|||
      railties (~> 7.0)
 | 
			
		||||
      rotp (~> 6.0)
 | 
			
		||||
    diff-lcs (1.5.0)
 | 
			
		||||
    diffy (3.4.2)
 | 
			
		||||
    diffy (3.4.3)
 | 
			
		||||
    digest-crc (0.6.5)
 | 
			
		||||
      rake (>= 12.0.0, < 14.0.0)
 | 
			
		||||
    discordrb-webhooks (3.5.0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
  "AccessLevelInterface": [
 | 
			
		||||
    "ContainerProtectionAccessLevel",
 | 
			
		||||
    "ContainerProtectionTagRule"
 | 
			
		||||
  ],
 | 
			
		||||
  "AlertManagementIntegration": [
 | 
			
		||||
    "AlertManagementHttpIntegration",
 | 
			
		||||
    "AlertManagementPrometheusIntegration"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -152,10 +152,10 @@ export default {
 | 
			
		|||
    <template v-if="activePanel">
 | 
			
		||||
      <div data-testid="active-panel-template" class="gl-flex gl-items-center gl-py-5">
 | 
			
		||||
        <div class="col-auto">
 | 
			
		||||
          <img aria-hidden :src="activePanel.imageSrc" />
 | 
			
		||||
          <img :alt="''" :src="activePanel.imageSrc" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          <h4>{{ activePanel.title }}</h4>
 | 
			
		||||
          <h1 class="gl-heading-2-fixed gl-my-3">{{ activePanel.title }}</h1>
 | 
			
		||||
 | 
			
		||||
          <p v-if="hasTextDetails">{{ details }}</p>
 | 
			
		||||
          <component :is="details" v-else v-bind="detailProps" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -326,7 +326,7 @@ export default {
 | 
			
		|||
      return this.glFeatures.notificationsTodosButtons;
 | 
			
		||||
    },
 | 
			
		||||
    parentWorkItem() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_HIERARCHY)?.parent;
 | 
			
		||||
    },
 | 
			
		||||
    parentWorkItemId() {
 | 
			
		||||
      return this.parentWorkItem?.id;
 | 
			
		||||
| 
						 | 
				
			
			@ -364,10 +364,10 @@ export default {
 | 
			
		|||
      return this.workItem.workItemType?.iconName;
 | 
			
		||||
    },
 | 
			
		||||
    hasDescriptionWidget() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_DESCRIPTION);
 | 
			
		||||
    },
 | 
			
		||||
    hasDesignWidget() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_DESIGNS) && this.$router;
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_DESIGNS) && this.$router;
 | 
			
		||||
    },
 | 
			
		||||
    showUploadDesign() {
 | 
			
		||||
      return this.hasDesignWidget && this.workspacePermissions.createDesign;
 | 
			
		||||
| 
						 | 
				
			
			@ -376,10 +376,10 @@ export default {
 | 
			
		|||
      return this.hasDesignWidget && this.workspacePermissions.moveDesign;
 | 
			
		||||
    },
 | 
			
		||||
    workItemNotificationsSubscribed() {
 | 
			
		||||
      return Boolean(this.isWidgetPresent(WIDGET_TYPE_NOTIFICATIONS)?.subscribed);
 | 
			
		||||
      return Boolean(this.findWidget(WIDGET_TYPE_NOTIFICATIONS)?.subscribed);
 | 
			
		||||
    },
 | 
			
		||||
    workItemCurrentUserTodos() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_CURRENT_USER_TODOS);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_CURRENT_USER_TODOS);
 | 
			
		||||
    },
 | 
			
		||||
    showWorkItemCurrentUserTodos() {
 | 
			
		||||
      return Boolean(this.$options.isLoggedIn && this.workItemCurrentUserTodos);
 | 
			
		||||
| 
						 | 
				
			
			@ -388,22 +388,22 @@ export default {
 | 
			
		|||
      return this.workItemCurrentUserTodos?.currentUserTodos?.nodes;
 | 
			
		||||
    },
 | 
			
		||||
    workItemAssignees() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_ASSIGNEES);
 | 
			
		||||
    },
 | 
			
		||||
    workItemAwardEmoji() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_AWARD_EMOJI);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_AWARD_EMOJI);
 | 
			
		||||
    },
 | 
			
		||||
    workItemHierarchy() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_HIERARCHY);
 | 
			
		||||
    },
 | 
			
		||||
    workItemNotes() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_NOTES);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_NOTES);
 | 
			
		||||
    },
 | 
			
		||||
    workItemWeight() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_WEIGHT);
 | 
			
		||||
    },
 | 
			
		||||
    workItemDevelopment() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_DEVELOPMENT);
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_DEVELOPMENT);
 | 
			
		||||
    },
 | 
			
		||||
    workItemBodyClass() {
 | 
			
		||||
      return {
 | 
			
		||||
| 
						 | 
				
			
			@ -415,11 +415,11 @@ export default {
 | 
			
		|||
    },
 | 
			
		||||
    workItemLinkedItems() {
 | 
			
		||||
      return this.workItemType === WORK_ITEM_TYPE_VALUE_EPIC
 | 
			
		||||
        ? this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS) && this.hasLinkedItemsEpicsFeature
 | 
			
		||||
        : this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS);
 | 
			
		||||
        ? this.findWidget(WIDGET_TYPE_LINKED_ITEMS) && this.hasLinkedItemsEpicsFeature
 | 
			
		||||
        : this.findWidget(WIDGET_TYPE_LINKED_ITEMS);
 | 
			
		||||
    },
 | 
			
		||||
    showWorkItemTree() {
 | 
			
		||||
      return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY) && this.allowedChildTypes?.length > 0;
 | 
			
		||||
      return this.findWidget(WIDGET_TYPE_HIERARCHY) && this.allowedChildTypes?.length > 0;
 | 
			
		||||
    },
 | 
			
		||||
    titleClassHeader() {
 | 
			
		||||
      return {
 | 
			
		||||
| 
						 | 
				
			
			@ -495,7 +495,7 @@ export default {
 | 
			
		|||
    enableEditMode() {
 | 
			
		||||
      this.editMode = true;
 | 
			
		||||
    },
 | 
			
		||||
    isWidgetPresent(type) {
 | 
			
		||||
    findWidget(type) {
 | 
			
		||||
      return this.widgets?.find((widget) => widget.type === type);
 | 
			
		||||
    },
 | 
			
		||||
    toggleConfidentiality(confidentialStatus) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,50 @@ module Types
 | 
			
		|||
      field :revision, GraphQL::Types::String, null: true, description: 'Revision of the tag.'
 | 
			
		||||
      field :short_revision, GraphQL::Types::String, null: true, description: 'Short revision of the tag.'
 | 
			
		||||
      field :total_size, GraphQL::Types::BigInt, null: true, description: 'Size of the tag.'
 | 
			
		||||
 | 
			
		||||
      field :protection,
 | 
			
		||||
        Types::ContainerRegistry::Protection::AccessLevelType,
 | 
			
		||||
        null: true,
 | 
			
		||||
        experiment: { milestone: '17.9' },
 | 
			
		||||
        description: 'Minimum GitLab access level required to push and delete container image tags. ' \
 | 
			
		||||
          'If multiple protection rules match an image tag, the highest access levels are applied'
 | 
			
		||||
 | 
			
		||||
      def protection
 | 
			
		||||
        return unless Feature.enabled?(:container_registry_protected_tags, project)
 | 
			
		||||
 | 
			
		||||
        highest_matching_rule
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      private
 | 
			
		||||
 | 
			
		||||
      def project
 | 
			
		||||
        object.repository.project
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def highest_matching_rule
 | 
			
		||||
        result = ::ContainerRegistry::Protection::TagRule.new
 | 
			
		||||
 | 
			
		||||
        project.container_registry_protection_tag_rules.each do |rule|
 | 
			
		||||
          next unless Gitlab::UntrustedRegexp.new(rule.tag_name_pattern).match?(object.name)
 | 
			
		||||
 | 
			
		||||
          set_max_access_level(result, rule)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        result
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def set_max_access_level(result, rule)
 | 
			
		||||
        %i[push delete].each do |action|
 | 
			
		||||
          attribute = :"minimum_access_level_for_#{action}"
 | 
			
		||||
 | 
			
		||||
          result[attribute] = [
 | 
			
		||||
            # minimum_access_level_for_push_before_type_cast will return the
 | 
			
		||||
            # enum's numeric value so we can correctly use .max on it.
 | 
			
		||||
            result.method(:"#{attribute}_before_type_cast").call.to_i,
 | 
			
		||||
            rule.method(:"#{attribute}_before_type_cast").call
 | 
			
		||||
          ].max
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Types
 | 
			
		||||
  module ContainerRegistry
 | 
			
		||||
    module Protection
 | 
			
		||||
      module AccessLevelInterface
 | 
			
		||||
        include BaseInterface
 | 
			
		||||
 | 
			
		||||
        field :minimum_access_level_for_delete,
 | 
			
		||||
          Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum,
 | 
			
		||||
          null: true,
 | 
			
		||||
          experiment: { milestone: '17.8' },
 | 
			
		||||
          description:
 | 
			
		||||
            'Minimum GitLab access level required to delete container image tags from the container repository. ' \
 | 
			
		||||
            'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
 | 
			
		||||
            'If the value is `nil`, no minimum access level is enforced. ' \
 | 
			
		||||
            'Users with the Developer role or higher can delete tags by default.'
 | 
			
		||||
 | 
			
		||||
        field :minimum_access_level_for_push,
 | 
			
		||||
          Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum,
 | 
			
		||||
          null: true,
 | 
			
		||||
          experiment: { milestone: '17.8' },
 | 
			
		||||
          description:
 | 
			
		||||
            'Minimum GitLab access level required to push container image tags to the container repository. ' \
 | 
			
		||||
            'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
 | 
			
		||||
            'If the value is `nil`, no minimum access level is enforced. ' \
 | 
			
		||||
            'Users with the Developer role or higher can push tags by default.'
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Types
 | 
			
		||||
  module ContainerRegistry
 | 
			
		||||
    module Protection
 | 
			
		||||
      class AccessLevelType < ::Types::BaseObject # rubocop:disable Graphql/AuthorizeTypes -- it inherits the same authorization as the caller
 | 
			
		||||
        graphql_name 'ContainerProtectionAccessLevel'
 | 
			
		||||
        description 'Represents the most restrictive permissions for a container image tag'
 | 
			
		||||
 | 
			
		||||
        implements Types::ContainerRegistry::Protection::AccessLevelInterface
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -8,6 +8,8 @@ module Types
 | 
			
		|||
        description 'A container repository tag protection rule designed to prevent users with a certain ' \
 | 
			
		||||
          'access level or lower from altering the container registry.'
 | 
			
		||||
 | 
			
		||||
        implements Types::ContainerRegistry::Protection::AccessLevelInterface
 | 
			
		||||
 | 
			
		||||
        authorize :admin_container_image
 | 
			
		||||
 | 
			
		||||
        field :id,
 | 
			
		||||
| 
						 | 
				
			
			@ -23,26 +25,6 @@ module Types
 | 
			
		|||
          description:
 | 
			
		||||
            'The pattern that matches container image tags to protect. ' \
 | 
			
		||||
            'For example, `v1.*`. Wildcard character `*` allowed.'
 | 
			
		||||
 | 
			
		||||
        # rubocop:disable GraphQL/ExtractType -- These are stored as separate fields
 | 
			
		||||
        field :minimum_access_level_for_delete,
 | 
			
		||||
          Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum,
 | 
			
		||||
          null: true,
 | 
			
		||||
          experiment: { milestone: '17.8' },
 | 
			
		||||
          description:
 | 
			
		||||
            'Minimum GitLab access level required to delete container image tags from the container repository. ' \
 | 
			
		||||
            'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
 | 
			
		||||
            'If the value is `nil`, the default minimum access level is `DEVELOPER`.'
 | 
			
		||||
 | 
			
		||||
        field :minimum_access_level_for_push,
 | 
			
		||||
          Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum,
 | 
			
		||||
          null: true,
 | 
			
		||||
          experiment: { milestone: '17.8' },
 | 
			
		||||
          description:
 | 
			
		||||
            'Minimum GitLab access level required to push container image tags to the container repository. ' \
 | 
			
		||||
            'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \
 | 
			
		||||
            'If the value is `nil`, the default minimum access level is `DEVELOPER`.'
 | 
			
		||||
        # rubocop:enable GraphQL/ExtractType -- These are stored as user preferences
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Integrations
 | 
			
		||||
  module Base
 | 
			
		||||
    module CustomIssueTracker
 | 
			
		||||
      extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
      include Base::IssueTracker
 | 
			
		||||
      include HasIssueTrackerFields
 | 
			
		||||
 | 
			
		||||
      class_methods do
 | 
			
		||||
        def title
 | 
			
		||||
          s_('IssueTracker|Custom issue tracker')
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def description
 | 
			
		||||
          s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def help
 | 
			
		||||
          build_help_page_url(
 | 
			
		||||
            'user/project/integrations/custom_issue_tracker.md',
 | 
			
		||||
            s_("IssueTracker|Use a custom issue tracker that is not in the integration list.")
 | 
			
		||||
          )
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def to_param
 | 
			
		||||
          'custom_issue_tracker'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      included do
 | 
			
		||||
        validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,134 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Integrations
 | 
			
		||||
  module Base
 | 
			
		||||
    module DiffblueCover
 | 
			
		||||
      extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
      class_methods do
 | 
			
		||||
        def title
 | 
			
		||||
          'Diffblue Cover'
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def description
 | 
			
		||||
          s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def to_param
 | 
			
		||||
          'diffblue_cover'
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def help
 | 
			
		||||
          s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def supported_events
 | 
			
		||||
          []
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def diffblue_link
 | 
			
		||||
          ActionController::Base.helpers.link_to(
 | 
			
		||||
            s_('DiffblueCover|Try Diffblue Cover'),
 | 
			
		||||
            'https://www.diffblue.com/try-cover/gitlab/',
 | 
			
		||||
            target: '_blank',
 | 
			
		||||
            rel: 'noopener noreferrer'
 | 
			
		||||
          )
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      included do
 | 
			
		||||
        field :diffblue_license_key,
 | 
			
		||||
          section: Integrations::Base::Integration::SECTION_TYPE_CONNECTION,
 | 
			
		||||
          type: :password,
 | 
			
		||||
          title: -> { s_('DiffblueCover|License key') },
 | 
			
		||||
          description: -> { s_('DiffblueCover|Diffblue Cover license key.') },
 | 
			
		||||
          non_empty_password_title: -> { s_('DiffblueCover|License key') },
 | 
			
		||||
          non_empty_password_help: -> {
 | 
			
		||||
            s_(
 | 
			
		||||
              'DiffblueCover|Leave blank to use your current license key.'
 | 
			
		||||
            )
 | 
			
		||||
          },
 | 
			
		||||
          exposes_secrets: true,
 | 
			
		||||
          required: true,
 | 
			
		||||
          is_secret: true,
 | 
			
		||||
          placeholder: 'XXXX-XXXX-XXXX-XXXX',
 | 
			
		||||
          help: -> {
 | 
			
		||||
            format(
 | 
			
		||||
              s_(
 | 
			
		||||
                'DiffblueCover|Enter your Diffblue Cover license key or ' \
 | 
			
		||||
                  'go to %{diffblue_link} to obtain a free trial license.'
 | 
			
		||||
              ),
 | 
			
		||||
              diffblue_link: diffblue_link
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        field :diffblue_access_token_name,
 | 
			
		||||
          section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
          title: -> { s_('DiffblueCover|Name') },
 | 
			
		||||
          description: -> { s_('DiffblueCover|Access token name used by Diffblue Cover in pipelines.') },
 | 
			
		||||
          required: true,
 | 
			
		||||
          placeholder: -> { s_('DiffblueCover|My token name') }
 | 
			
		||||
 | 
			
		||||
        field :diffblue_access_token_secret,
 | 
			
		||||
          section: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
          type: :password,
 | 
			
		||||
          title: -> { s_('DiffblueCover|Secret') },
 | 
			
		||||
          description: -> { s_('DiffblueCover|Access token secret used by Diffblue Cover in pipelines.') },
 | 
			
		||||
          non_empty_password_title: -> { s_('DiffblueCover|Secret') },
 | 
			
		||||
          non_empty_password_help: -> { s_('DiffblueCover|Leave blank to use your current secret value.') },
 | 
			
		||||
          required: true,
 | 
			
		||||
          is_secret: true,
 | 
			
		||||
          placeholder: 'glpat-XXXXXXXXXXXXXXXXXXXX' # gitleaks:allow
 | 
			
		||||
 | 
			
		||||
        with_options if: :activated? do
 | 
			
		||||
          validates :diffblue_license_key, presence: true
 | 
			
		||||
          validates :diffblue_access_token_name, presence: true
 | 
			
		||||
          validates :diffblue_access_token_secret, presence: true
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def avatar_url
 | 
			
		||||
        ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/diffblue.svg')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def sections
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            type: Integrations::Base::Integration::SECTION_TYPE_CONNECTION,
 | 
			
		||||
            title: s_('DiffblueCover|Integration details'),
 | 
			
		||||
            description:
 | 
			
		||||
              s_(
 | 
			
		||||
                'DiffblueCover|Diffblue Cover is a generative AI platform that automatically ' \
 | 
			
		||||
                  'writes comprehensive, human-like Java unit tests. Integrate Diffblue ' \
 | 
			
		||||
                  'Cover into your CI/CD workflow for fully autonomous operation.'
 | 
			
		||||
              )
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            type: Integrations::Base::Integration::SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
            title: s_('DiffblueCover|Access token'),
 | 
			
		||||
            description:
 | 
			
		||||
              'You must have a GitLab access token for Diffblue Cover to access your project. ' \
 | 
			
		||||
              'Use a GitLab access token with at least the Developer role and ' \
 | 
			
		||||
              'the <code>api</code> and <code>write_repository</code> permissions.'
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def execute(_data) end
 | 
			
		||||
 | 
			
		||||
      def ci_variables
 | 
			
		||||
        return [] unless activated?
 | 
			
		||||
 | 
			
		||||
        [
 | 
			
		||||
          { key: 'DIFFBLUE_LICENSE_KEY', value: diffblue_license_key, public: false, masked: true },
 | 
			
		||||
          { key: 'DIFFBLUE_ACCESS_TOKEN_NAME', value: diffblue_access_token_name, public: false, masked: true },
 | 
			
		||||
          { key: 'DIFFBLUE_ACCESS_TOKEN', value: diffblue_access_token_secret, public: false, masked: true }
 | 
			
		||||
        ]
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def testable?
 | 
			
		||||
        false
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module Integrations
 | 
			
		||||
  module Base
 | 
			
		||||
    module Ewm
 | 
			
		||||
      extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
      include Base::IssueTracker
 | 
			
		||||
      include HasIssueTrackerFields
 | 
			
		||||
 | 
			
		||||
      class_methods do
 | 
			
		||||
        def title
 | 
			
		||||
          'EWM'
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def description
 | 
			
		||||
          s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def help
 | 
			
		||||
          build_help_page_url(
 | 
			
		||||
            'user/project/integrations/ewm.md',
 | 
			
		||||
            s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
 | 
			
		||||
          )
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def to_param
 | 
			
		||||
          'ewm'
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      included do
 | 
			
		||||
        validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 | 
			
		||||
 | 
			
		||||
        def reference_pattern(*)
 | 
			
		||||
          @reference_pattern ||= %r{(?<issue>\b(?:bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        def issue_url(iid)
 | 
			
		||||
          issues_url.gsub(':id', iid.to_s.split(' ')[-1])
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -2,28 +2,6 @@
 | 
			
		|||
 | 
			
		||||
module Integrations
 | 
			
		||||
  class CustomIssueTracker < Integration
 | 
			
		||||
    include Base::IssueTracker
 | 
			
		||||
    include HasIssueTrackerFields
 | 
			
		||||
 | 
			
		||||
    validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 | 
			
		||||
 | 
			
		||||
    def self.title
 | 
			
		||||
      s_('IssueTracker|Custom issue tracker')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.description
 | 
			
		||||
      s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.help
 | 
			
		||||
      build_help_page_url(
 | 
			
		||||
        'user/project/integrations/custom_issue_tracker.md',
 | 
			
		||||
        s_("IssueTracker|Use a custom issue tracker that is not in the integration list.")
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.to_param
 | 
			
		||||
      'custom_issue_tracker'
 | 
			
		||||
    end
 | 
			
		||||
    include Integrations::Base::CustomIssueTracker
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,125 +2,6 @@
 | 
			
		|||
 | 
			
		||||
module Integrations
 | 
			
		||||
  class DiffblueCover < Integration
 | 
			
		||||
    field :diffblue_license_key,
 | 
			
		||||
      section: SECTION_TYPE_CONNECTION,
 | 
			
		||||
      type: :password,
 | 
			
		||||
      title: -> { s_('DiffblueCover|License key') },
 | 
			
		||||
      description: -> { s_('DiffblueCover|Diffblue Cover license key.') },
 | 
			
		||||
      non_empty_password_title: -> { s_('DiffblueCover|License key') },
 | 
			
		||||
      non_empty_password_help: -> {
 | 
			
		||||
        s_(
 | 
			
		||||
          'DiffblueCover|Leave blank to use your current license key.'
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      exposes_secrets: true,
 | 
			
		||||
      required: true,
 | 
			
		||||
      is_secret: true,
 | 
			
		||||
      placeholder: 'XXXX-XXXX-XXXX-XXXX',
 | 
			
		||||
      help: -> {
 | 
			
		||||
        format(
 | 
			
		||||
          s_(
 | 
			
		||||
            'DiffblueCover|Enter your Diffblue Cover license key or ' \
 | 
			
		||||
            'go to %{diffblue_link} to obtain a free trial license.'
 | 
			
		||||
          ),
 | 
			
		||||
          diffblue_link: diffblue_link
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    field :diffblue_access_token_name,
 | 
			
		||||
      section: SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
      title: -> { s_('DiffblueCover|Name') },
 | 
			
		||||
      description: -> { s_('DiffblueCover|Access token name used by Diffblue Cover in pipelines.') },
 | 
			
		||||
      required: true,
 | 
			
		||||
      placeholder: -> { s_('DiffblueCover|My token name') }
 | 
			
		||||
 | 
			
		||||
    field :diffblue_access_token_secret,
 | 
			
		||||
      section: SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
      type: :password,
 | 
			
		||||
      title: -> { s_('DiffblueCover|Secret') },
 | 
			
		||||
      description: -> { s_('DiffblueCover|Access token secret used by Diffblue Cover in pipelines.') },
 | 
			
		||||
      non_empty_password_title: -> { s_('DiffblueCover|Secret') },
 | 
			
		||||
      non_empty_password_help: -> { s_('DiffblueCover|Leave blank to use your current secret value.') },
 | 
			
		||||
      required: true,
 | 
			
		||||
      is_secret: true,
 | 
			
		||||
      placeholder: 'glpat-XXXXXXXXXXXXXXXXXXXX' # gitleaks:allow
 | 
			
		||||
 | 
			
		||||
    with_options if: :activated? do
 | 
			
		||||
      validates :diffblue_license_key, presence: true
 | 
			
		||||
      validates :diffblue_access_token_name, presence: true
 | 
			
		||||
      validates :diffblue_access_token_secret, presence: true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.title
 | 
			
		||||
      'Diffblue Cover'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.description
 | 
			
		||||
      s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.to_param
 | 
			
		||||
      'diffblue_cover'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.help
 | 
			
		||||
      s_('DiffblueCover|Automatically write comprehensive, human-like Java unit tests.')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def avatar_url
 | 
			
		||||
      ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/diffblue.svg')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.supported_events
 | 
			
		||||
      []
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def sections
 | 
			
		||||
      [
 | 
			
		||||
        {
 | 
			
		||||
          type: SECTION_TYPE_CONNECTION,
 | 
			
		||||
          title: s_('DiffblueCover|Integration details'),
 | 
			
		||||
          description:
 | 
			
		||||
            s_(
 | 
			
		||||
              'DiffblueCover|Diffblue Cover is a generative AI platform that automatically ' \
 | 
			
		||||
              'writes comprehensive, human-like Java unit tests. Integrate Diffblue ' \
 | 
			
		||||
              'Cover into your CI/CD workflow for fully autonomous operation.'
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: SECTION_TYPE_CONFIGURATION,
 | 
			
		||||
          title: s_('DiffblueCover|Access token'),
 | 
			
		||||
          description:
 | 
			
		||||
            'You must have a GitLab access token for Diffblue Cover to access your project. ' \
 | 
			
		||||
            'Use a GitLab access token with at least the Developer role and ' \
 | 
			
		||||
            'the <code>api</code> and <code>write_repository</code> permissions.'
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def execute(_data) end
 | 
			
		||||
 | 
			
		||||
    def ci_variables
 | 
			
		||||
      return [] unless activated?
 | 
			
		||||
 | 
			
		||||
      [
 | 
			
		||||
        { key: 'DIFFBLUE_LICENSE_KEY', value: diffblue_license_key, public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN_NAME', value: diffblue_access_token_name, public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN', value: diffblue_access_token_secret, public: false, masked: true }
 | 
			
		||||
      ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def testable?
 | 
			
		||||
      false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.diffblue_link
 | 
			
		||||
      ActionController::Base.helpers.link_to(
 | 
			
		||||
        s_('DiffblueCover|Try Diffblue Cover'),
 | 
			
		||||
        'https://www.diffblue.com/try-cover/gitlab/',
 | 
			
		||||
        target: '_blank',
 | 
			
		||||
        rel: 'noopener noreferrer'
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
    include Integrations::Base::DiffblueCover
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,36 +2,6 @@
 | 
			
		|||
 | 
			
		||||
module Integrations
 | 
			
		||||
  class Ewm < Integration
 | 
			
		||||
    include Base::IssueTracker
 | 
			
		||||
    include HasIssueTrackerFields
 | 
			
		||||
 | 
			
		||||
    validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 | 
			
		||||
 | 
			
		||||
    def reference_pattern(only_long: true)
 | 
			
		||||
      @reference_pattern ||= %r{(?<issue>\b(?:bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.title
 | 
			
		||||
      'EWM'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.description
 | 
			
		||||
      s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.help
 | 
			
		||||
      build_help_page_url(
 | 
			
		||||
        'user/project/integrations/ewm.md',
 | 
			
		||||
        s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def self.to_param
 | 
			
		||||
      'ewm'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def issue_url(iid)
 | 
			
		||||
      issues_url.gsub(':id', iid.to_s.split(' ')[-1])
 | 
			
		||||
    end
 | 
			
		||||
    include Integrations::Base::Ewm
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module Integrations
 | 
			
		||||
  module Instance
 | 
			
		||||
    class CustomIssueTracker < Integration
 | 
			
		||||
      # To be updated as part of https://gitlab.com/gitlab-org/gitlab/-/issues/474809
 | 
			
		||||
      include Integrations::Base::CustomIssueTracker
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module Integrations
 | 
			
		||||
  module Instance
 | 
			
		||||
    class DiffblueCover < Integration
 | 
			
		||||
      # To be updated as part of https://gitlab.com/gitlab-org/gitlab/-/issues/474809
 | 
			
		||||
      include Integrations::Base::DiffblueCover
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module Integrations
 | 
			
		||||
  module Instance
 | 
			
		||||
    class Ewm < Integration
 | 
			
		||||
      # To be updated as part of https://gitlab.com/gitlab-org/gitlab/-/issues/474809
 | 
			
		||||
      include Integrations::Base::Ewm
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
= gitlab_ui_form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'gl-show-field-errors' do |f|
 | 
			
		||||
  .gl-border-l-solid.gl-border-r-solid.gl-border-t-solid.gl-border-default.gl-border-1.gl-p-5.gl-mt-4
 | 
			
		||||
    .gl-flex.gl-items-center.gl-justify-between
 | 
			
		||||
      %h4.gl-flex
 | 
			
		||||
      %h2.gl-flex.gl-heading-2-fixed.gl-my-3
 | 
			
		||||
        = s_('GroupsNew|Import groups by direct transfer')
 | 
			
		||||
      = render Pajamas::ButtonComponent.new(href: history_import_bulk_imports_path, category: :secondary, variant: :confirm, size: :small) do
 | 
			
		||||
        = s_('BulkImport|View import history')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 | 
			
		||||
= gitlab_ui_form_for '', url: import_gitlab_group_path, namespace: 'import_group', class: 'group-form gl-show-field-errors', multipart: true do |f|
 | 
			
		||||
  .gl-border-l-solid.gl-border-r-solid.gl-border-default.gl-border-1.gl-p-5
 | 
			
		||||
    %h4
 | 
			
		||||
    %h2.gl-heading-2-fixed.gl-my-3
 | 
			
		||||
      = _('Import group from file')
 | 
			
		||||
    = render Pajamas::AlertComponent.new(variant: :warning,
 | 
			
		||||
      alert_options: { class: 'gl-mb-5' },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class CreateSiphonNotes < ClickHouse::Migration
 | 
			
		||||
  def up
 | 
			
		||||
    execute <<-SQL
 | 
			
		||||
      CREATE TABLE IF NOT EXISTS siphon_notes
 | 
			
		||||
      (
 | 
			
		||||
        note Nullable(String),
 | 
			
		||||
        noteable_type LowCardinality(String),
 | 
			
		||||
        author_id Nullable(Int64),
 | 
			
		||||
        created_at Nullable(DateTime64(6, 'UTC')),
 | 
			
		||||
        updated_at Nullable(DateTime64(6, 'UTC')),
 | 
			
		||||
        project_id Nullable(Int64),
 | 
			
		||||
        attachment Nullable(String) DEFAULT '',
 | 
			
		||||
        line_code Nullable(String),
 | 
			
		||||
        commit_id Nullable(String),
 | 
			
		||||
        noteable_id Nullable(Int64),
 | 
			
		||||
        system Bool DEFAULT false,
 | 
			
		||||
        st_diff Nullable(String),
 | 
			
		||||
        updated_by_id Nullable(Int64),
 | 
			
		||||
        type LowCardinality(String) DEFAULT '',
 | 
			
		||||
        position Nullable(String),
 | 
			
		||||
        original_position Nullable(String),
 | 
			
		||||
        resolved_at Nullable(DateTime64(6, 'UTC')),
 | 
			
		||||
        resolved_by_id Nullable(Int64),
 | 
			
		||||
        discussion_id Nullable(String),
 | 
			
		||||
        note_html Nullable(String),
 | 
			
		||||
        cached_markdown_version Nullable(Int64),
 | 
			
		||||
        change_position Nullable(String),
 | 
			
		||||
        resolved_by_push Nullable(Bool),
 | 
			
		||||
        review_id Nullable(Int64),
 | 
			
		||||
        confidential Nullable(Bool),
 | 
			
		||||
        last_edited_at Nullable(DateTime64(6, 'UTC')),
 | 
			
		||||
        internal Bool DEFAULT false,
 | 
			
		||||
        id Int64,
 | 
			
		||||
        namespace_id Nullable(Int64),
 | 
			
		||||
        imported_from Int8 DEFAULT 0,
 | 
			
		||||
        _siphon_replicated_at DateTime64(6, 'UTC') DEFAULT now(),
 | 
			
		||||
        _siphon_deleted Bool DEFAULT FALSE
 | 
			
		||||
      )
 | 
			
		||||
      ENGINE = ReplacingMergeTree(_siphon_replicated_at, _siphon_deleted)
 | 
			
		||||
      PRIMARY KEY id
 | 
			
		||||
    SQL
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down
 | 
			
		||||
    execute <<-SQL
 | 
			
		||||
      DROP TABLE IF EXISTS siphon_notes
 | 
			
		||||
    SQL
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -739,16 +739,37 @@ For Linux package (Omnibus):
 | 
			
		|||
   gitlab_rails['backup_upload_connection'] = {
 | 
			
		||||
     'provider' => 'AWS',
 | 
			
		||||
     'region' => 'eu-west-1',
 | 
			
		||||
     # Choose one authentication method
 | 
			
		||||
     # IAM Profile
 | 
			
		||||
     'use_iam_profile' => true
 | 
			
		||||
     # OR AWS Access and Secret key
 | 
			
		||||
     'aws_access_key_id' => 'AKIAKIAKI',
 | 
			
		||||
     'aws_secret_access_key' => 'secret123'
 | 
			
		||||
     # If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
 | 
			
		||||
     # 'use_iam_profile' => true
 | 
			
		||||
   }
 | 
			
		||||
   gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
 | 
			
		||||
   # Consider using multipart uploads when file size reaches 100MB. Enter a number in bytes.
 | 
			
		||||
   # gitlab_rails['backup_multipart_chunk_size'] = 104857600
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
1. If you're using the IAM Profile authentication method, ensure the instance where `backup-utility` is to be run has the following policy set (replace `<backups-bucket>` with the correct bucket name):
 | 
			
		||||
 | 
			
		||||
   ```json
 | 
			
		||||
   {
 | 
			
		||||
       "Version": "2012-10-17",
 | 
			
		||||
       "Statement": [
 | 
			
		||||
           {
 | 
			
		||||
               "Effect": "Allow",
 | 
			
		||||
               "Action": [
 | 
			
		||||
                   "s3:PutObject",
 | 
			
		||||
                   "s3:GetObject",
 | 
			
		||||
                   "s3:DeleteObject"
 | 
			
		||||
               ],
 | 
			
		||||
               "Resource": "arn:aws:s3:::<backups-bucket>/*"
 | 
			
		||||
           }
 | 
			
		||||
       ]
 | 
			
		||||
   }
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation)
 | 
			
		||||
   for the changes to take effect
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -889,57 +910,6 @@ For self-compiled installations:
 | 
			
		|||
1. [Restart GitLab](../restart_gitlab.md#self-compiled-installations)
 | 
			
		||||
   for the changes to take effect
 | 
			
		||||
 | 
			
		||||
If you're uploading your backups to S3, you should create a new
 | 
			
		||||
IAM user with restricted access rights. To give the upload user access only for
 | 
			
		||||
uploading backups create the following IAM profile, replacing `my.s3.bucket`
 | 
			
		||||
with the name of your bucket:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  "Version": "2012-10-17",
 | 
			
		||||
  "Statement": [
 | 
			
		||||
    {
 | 
			
		||||
      "Sid": "Stmt1412062044000",
 | 
			
		||||
      "Effect": "Allow",
 | 
			
		||||
      "Action": [
 | 
			
		||||
        "s3:AbortMultipartUpload",
 | 
			
		||||
        "s3:GetBucketAcl",
 | 
			
		||||
        "s3:GetBucketLocation",
 | 
			
		||||
        "s3:GetObject",
 | 
			
		||||
        "s3:GetObjectAcl",
 | 
			
		||||
        "s3:ListBucketMultipartUploads",
 | 
			
		||||
        "s3:PutObject",
 | 
			
		||||
        "s3:PutObjectAcl"
 | 
			
		||||
      ],
 | 
			
		||||
      "Resource": [
 | 
			
		||||
        "arn:aws:s3:::my.s3.bucket/*"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "Sid": "Stmt1412062097000",
 | 
			
		||||
      "Effect": "Allow",
 | 
			
		||||
      "Action": [
 | 
			
		||||
        "s3:GetBucketLocation",
 | 
			
		||||
        "s3:ListAllMyBuckets"
 | 
			
		||||
      ],
 | 
			
		||||
      "Resource": [
 | 
			
		||||
        "*"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "Sid": "Stmt1412062128000",
 | 
			
		||||
      "Effect": "Allow",
 | 
			
		||||
      "Action": [
 | 
			
		||||
        "s3:ListBucket"
 | 
			
		||||
      ],
 | 
			
		||||
      "Resource": [
 | 
			
		||||
        "arn:aws:s3:::my.s3.bucket"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
##### Using Google Cloud Storage
 | 
			
		||||
 | 
			
		||||
To use Google Cloud Storage to save backups, you must first create an
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -230,7 +230,6 @@ To set up an instance profile:
 | 
			
		|||
       "Version": "2012-10-17",
 | 
			
		||||
       "Statement": [
 | 
			
		||||
           {
 | 
			
		||||
               "Sid": "VisualEditor0",
 | 
			
		||||
               "Effect": "Allow",
 | 
			
		||||
               "Action": [
 | 
			
		||||
                   "s3:PutObject",
 | 
			
		||||
| 
						 | 
				
			
			@ -238,6 +237,13 @@ To set up an instance profile:
 | 
			
		|||
                   "s3:DeleteObject"
 | 
			
		||||
               ],
 | 
			
		||||
               "Resource": "arn:aws:s3:::test-bucket/*"
 | 
			
		||||
           },
 | 
			
		||||
           {
 | 
			
		||||
               "Effect": "Allow",
 | 
			
		||||
               "Action": [
 | 
			
		||||
                   "s3:ListBucket"
 | 
			
		||||
               ],
 | 
			
		||||
               "Resource": "arn:aws:s3:::test-bucket"
 | 
			
		||||
           }
 | 
			
		||||
       ]
 | 
			
		||||
   }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3825,8 +3825,8 @@ Input type: `createContainerProtectionTagRuleInput`
 | 
			
		|||
| Name | Type | Description |
 | 
			
		||||
| ---- | ---- | ----------- |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerProtectionTagRuleAccessLevel!`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the default minimum access level is `DEVELOPER`. Introduced in GitLab 17.8: **Status**: Experiment. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerProtectionTagRuleAccessLevel!`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the default minimum access level is `DEVELOPER`. Introduced in GitLab 17.8: **Status**: Experiment. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` | [`ContainerProtectionTagRuleAccessLevel!`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can delete tags by default. Introduced in GitLab 17.8: **Status**: Experiment. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` | [`ContainerProtectionTagRuleAccessLevel!`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can push tags by default. Introduced in GitLab 17.8: **Status**: Experiment. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruleprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project containing the container image tags. |
 | 
			
		||||
| <a id="mutationcreatecontainerprotectiontagruletagnamepattern"></a>`tagNamePattern` | [`String!`](#string) | The pattern that matches container image tags to protect. For example, `v1.*`. Wildcard character `*` allowed. Introduced in GitLab 17.8: **Status**: Experiment. |
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19255,6 +19255,8 @@ Extra metadata for AI message.
 | 
			
		|||
 | 
			
		||||
### `AiMetrics`
 | 
			
		||||
 | 
			
		||||
Requires ClickHouse. Premium and Ultimate with GitLab Duo Pro and Enterprise only.
 | 
			
		||||
 | 
			
		||||
#### Fields
 | 
			
		||||
 | 
			
		||||
| Name | Type | Description |
 | 
			
		||||
| 
						 | 
				
			
			@ -22007,6 +22009,17 @@ A tag expiration policy designed to keep only the images that matter most.
 | 
			
		|||
| <a id="containerexpirationpolicyolderthan"></a>`olderThan` | [`ContainerExpirationPolicyOlderThanEnum`](#containerexpirationpolicyolderthanenum) | Tags older than the given age will expire. |
 | 
			
		||||
| <a id="containerexpirationpolicyupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the container expiration policy was updated. |
 | 
			
		||||
 | 
			
		||||
### `ContainerProtectionAccessLevel`
 | 
			
		||||
 | 
			
		||||
Represents the most restrictive permissions for a container image tag.
 | 
			
		||||
 | 
			
		||||
#### Fields
 | 
			
		||||
 | 
			
		||||
| Name | Type | Description |
 | 
			
		||||
| ---- | ---- | ----------- |
 | 
			
		||||
| <a id="containerprotectionaccesslevelminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can delete tags by default. |
 | 
			
		||||
| <a id="containerprotectionaccesslevelminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can push tags by default. |
 | 
			
		||||
 | 
			
		||||
### `ContainerProtectionRepositoryRule`
 | 
			
		||||
 | 
			
		||||
A container repository protection rule designed to prevent users with a certain access level or lower from altering the container registry.
 | 
			
		||||
| 
						 | 
				
			
			@ -22029,8 +22042,8 @@ A container repository tag protection rule designed to prevent users with a cert
 | 
			
		|||
| Name | Type | Description |
 | 
			
		||||
| ---- | ---- | ----------- |
 | 
			
		||||
| <a id="containerprotectiontagruleid"></a>`id` **{warning-solid}** | [`ContainerRegistryProtectionTagRuleID!`](#containerregistryprotectiontagruleid) | **Introduced** in GitLab 17.8. **Status**: Experiment. ID of the container repository tag protection rule. |
 | 
			
		||||
| <a id="containerprotectiontagruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the default minimum access level is `DEVELOPER`. |
 | 
			
		||||
| <a id="containerprotectiontagruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, the default minimum access level is `DEVELOPER`. |
 | 
			
		||||
| <a id="containerprotectiontagruleminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can delete tags by default. |
 | 
			
		||||
| <a id="containerprotectiontagruleminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can push tags by default. |
 | 
			
		||||
| <a id="containerprotectiontagruletagnamepattern"></a>`tagNamePattern` **{warning-solid}** | [`String!`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. The pattern that matches container image tags to protect. For example, `v1.*`. Wildcard character `*` allowed. |
 | 
			
		||||
 | 
			
		||||
### `ContainerRepository`
 | 
			
		||||
| 
						 | 
				
			
			@ -22174,6 +22187,7 @@ A tag from a container repository.
 | 
			
		|||
| <a id="containerrepositorytagmediatype"></a>`mediaType` | [`String`](#string) | Media type of the tag. |
 | 
			
		||||
| <a id="containerrepositorytagname"></a>`name` | [`String!`](#string) | Name of the tag. |
 | 
			
		||||
| <a id="containerrepositorytagpath"></a>`path` | [`String!`](#string) | Path of the tag. |
 | 
			
		||||
| <a id="containerrepositorytagprotection"></a>`protection` **{warning-solid}** | [`ContainerProtectionAccessLevel`](#containerprotectionaccesslevel) | **Introduced** in GitLab 17.9. **Status**: Experiment. Minimum GitLab access level required to push and delete container image tags. If multiple protection rules match an image tag, the highest access levels are applied. |
 | 
			
		||||
| <a id="containerrepositorytagpublishedat"></a>`publishedAt` | [`Time`](#time) | Timestamp when the tag was published. |
 | 
			
		||||
| <a id="containerrepositorytagreferrers"></a>`referrers` | [`[ContainerRepositoryReferrer!]`](#containerrepositoryreferrer) | Referrers for the tag. |
 | 
			
		||||
| <a id="containerrepositorytagrevision"></a>`revision` | [`String`](#string) | Revision of the tag. |
 | 
			
		||||
| 
						 | 
				
			
			@ -43725,6 +43739,20 @@ One of:
 | 
			
		|||
 | 
			
		||||
### Interfaces
 | 
			
		||||
 | 
			
		||||
#### `AccessLevelInterface`
 | 
			
		||||
 | 
			
		||||
Implementations:
 | 
			
		||||
 | 
			
		||||
- [`ContainerProtectionAccessLevel`](#containerprotectionaccesslevel)
 | 
			
		||||
- [`ContainerProtectionTagRule`](#containerprotectiontagrule)
 | 
			
		||||
 | 
			
		||||
##### Fields
 | 
			
		||||
 | 
			
		||||
| Name | Type | Description |
 | 
			
		||||
| ---- | ---- | ----------- |
 | 
			
		||||
| <a id="accesslevelinterfaceminimumaccesslevelfordelete"></a>`minimumAccessLevelForDelete` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can delete tags by default. |
 | 
			
		||||
| <a id="accesslevelinterfaceminimumaccesslevelforpush"></a>`minimumAccessLevelForPush` **{warning-solid}** | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no minimum access level is enforced. Users with the Developer role or higher can push tags by default. |
 | 
			
		||||
 | 
			
		||||
#### `AlertManagementIntegration`
 | 
			
		||||
 | 
			
		||||
Implementations:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,14 +10,14 @@ module Gitlab
 | 
			
		|||
 | 
			
		||||
      def initialize(project, options = nil)
 | 
			
		||||
        @project = project
 | 
			
		||||
        @project_namespace, _, @project_path = project.full_path.downcase.partition('/')
 | 
			
		||||
        namespace, _, @project_path = project.full_path.partition('/')
 | 
			
		||||
        @project_namespace = namespace.downcase
 | 
			
		||||
        @options = options || {}
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def pages_url
 | 
			
		||||
        default_url
 | 
			
		||||
          .to_s
 | 
			
		||||
          .downcase
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def unique_host
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ module Gitlab
 | 
			
		|||
      # See https://docs.gitlab.com/ee/user/project/pages/getting_started_part_one.html#user-and-group-website-examples.
 | 
			
		||||
      def is_namespace_homepage? # rubocop:disable Naming/PredicateName -- namespace_homepage is not an
 | 
			
		||||
        # adjective, so adding "is_" improves understandability
 | 
			
		||||
        project_path == "#{project_namespace}.#{instance_pages_domain}"
 | 
			
		||||
        project_path.downcase == "#{project_namespace}.#{instance_pages_domain}"
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      def artifact_url(artifact, job)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3099,6 +3099,9 @@ msgstr ""
 | 
			
		|||
msgid "Action not allowed."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Action required: %{project_name} is read-only"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Action to take when receiving an alert. %{docsLink}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24742,6 +24745,12 @@ msgstr ""
 | 
			
		|||
msgid "For investigating IT service disruptions or outages"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "For more information %{support_link_start}contact support%{link_end}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "For more information contact support: %{support_link}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "For more information on how the number of active users is calculated, see the %{self_managed_subscriptions_doc_link} documentation."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59083,6 +59092,9 @@ msgstr ""
 | 
			
		|||
msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 60-day trial which includes unlimited users."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "To remove the read-only state, reduce git repository and git LFS storage."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "To resolve the conflicts, either use interactive mode to select %{use_ours} or %{use_theirs}, or edit the files inline. Commit these changes into %{branch_name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -65852,6 +65864,12 @@ msgstr ""
 | 
			
		|||
msgid "You have been unsubscribed from this thread."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "You have consumed all available storage and you can't push or add large files to %{project_name}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "You have consumed all available storage and you can't push or add large files to %{strong_start}%{project_name}%{strong_end}"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "You have declined the invitation to join %{title} %{name}."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,7 +65,7 @@
 | 
			
		|||
    "@gitlab/ui": "107.0.1",
 | 
			
		||||
    "@gitlab/vue-router-vue3": "npm:vue-router@4.1.6",
 | 
			
		||||
    "@gitlab/vuex-vue3": "npm:vuex@4.0.0",
 | 
			
		||||
    "@gitlab/web-ide": "^0.0.1-dev-20250109231656",
 | 
			
		||||
    "@gitlab/web-ide": "^0.0.1-dev-20250110172049",
 | 
			
		||||
    "@gleam-lang/highlight.js-gleam": "^1.5.0",
 | 
			
		||||
    "@mattiasbuelens/web-streams-adapter": "^0.1.0",
 | 
			
		||||
    "@rails/actioncable": "7.0.807",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -252,8 +252,14 @@ module QA
 | 
			
		|||
            within_vscode_editor do
 | 
			
		||||
              # VSCode eagerly removes the input[type='file'] from click on Upload.
 | 
			
		||||
              # We need to execute a script on the iframe to stub out the iframes body.removeChild to add it back in.
 | 
			
		||||
              page.execute_script("document.body.removeChild = function(){};")
 | 
			
		||||
              page.execute_script(
 | 
			
		||||
                <<~JAVASCRIPT
 | 
			
		||||
                window.__gl_old_remove = HTMLInputElement.prototype.remove;
 | 
			
		||||
                HTMLInputElement.prototype.remove = function(){};
 | 
			
		||||
                JAVASCRIPT
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
              begin
 | 
			
		||||
                # under some conditions the page may not be fully loaded and the right click
 | 
			
		||||
                # context menu can get closed prior to hitting 'upload' leading to failures
 | 
			
		||||
                Support::Retrier.retry_until(retry_on_exception: true, message: "Uploading a file in vscode") do
 | 
			
		||||
| 
						 | 
				
			
			@ -261,6 +267,13 @@ module QA
 | 
			
		|||
                  click_upload_menu_item
 | 
			
		||||
                  enter_file_input(file_path)
 | 
			
		||||
                end
 | 
			
		||||
              ensure
 | 
			
		||||
                page.execute_script(
 | 
			
		||||
                  <<~JAVASCRIPT
 | 
			
		||||
                  HTMLInputElement.prototype.remove = window.__gl_old_remove;
 | 
			
		||||
                  JAVASCRIPT
 | 
			
		||||
                )
 | 
			
		||||
              end
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -339,6 +352,12 @@ module QA
 | 
			
		|||
          end
 | 
			
		||||
 | 
			
		||||
          def right_click_file_explorer
 | 
			
		||||
            # NOTE: Web IDE prompts for clipboard permission to open the file explorer context menu
 | 
			
		||||
            # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177778#note_2295036716
 | 
			
		||||
            # https://gitlab.com/gitlab-org/gitlab-web-ide/-/issues/433
 | 
			
		||||
            page.driver.browser.add_permission("clipboard-read", "granted")
 | 
			
		||||
            page.driver.browser.add_permission("clipboard-write", "granted")
 | 
			
		||||
 | 
			
		||||
            page.find('.explorer-folders-view', visible: true).right_click
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ require 'spec_helper'
 | 
			
		|||
 | 
			
		||||
RSpec.describe GitlabSchema.types['ContainerRepositoryTag'], feature_category: :container_registry do
 | 
			
		||||
  fields = %i[name path location digest revision short_revision
 | 
			
		||||
    total_size created_at user_permissions referrers published_at media_type]
 | 
			
		||||
    total_size created_at user_permissions referrers published_at media_type protection]
 | 
			
		||||
 | 
			
		||||
  it { expect(described_class.graphql_name).to eq('ContainerRepositoryTag') }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,6 +103,12 @@ RSpec.describe Gitlab::Pages::UrlBuilder, feature_category: :pages do
 | 
			
		|||
          it { is_expected.to eq('http://unique-domain.example.com') }
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when the project path contains capitals' do
 | 
			
		||||
        let(:full_path) { 'group/MyProject' }
 | 
			
		||||
 | 
			
		||||
        it { is_expected.to eq('http://group.example.com/MyProject') }
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when namespace_in_path is true' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,29 +3,5 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::CustomIssueTracker, feature_category: :integrations do
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when integration is active' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:new_issue_url) }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :project_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :issues_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when integration is inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:new_issue_url) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
  it_behaves_like Integrations::Base::CustomIssueTracker
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,72 +3,5 @@
 | 
			
		|||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::DiffblueCover, feature_category: :integrations do
 | 
			
		||||
  let_it_be(:project) { build(:project) }
 | 
			
		||||
 | 
			
		||||
  subject(:integration) { build(:diffblue_cover_integration, project: project) }
 | 
			
		||||
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when active' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_license_key) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_access_token_name) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_access_token_secret) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_license_key) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_access_token_name) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_access_token_secret) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#avatar_url' do
 | 
			
		||||
    it 'returns the avatar image path' do
 | 
			
		||||
      expect(integration.avatar_url).to eq(ActionController::Base.helpers.image_path(
 | 
			
		||||
        'illustrations/third-party-logos/integrations-logos/diffblue.svg'
 | 
			
		||||
      ))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#ci-vars' do
 | 
			
		||||
    let(:ci_vars) do
 | 
			
		||||
      [
 | 
			
		||||
        { key: 'DIFFBLUE_LICENSE_KEY', value: '1234-ABCD-DCBA-4321', public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN_NAME', value: 'Diffblue CI', public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN',
 | 
			
		||||
          value: 'glpat-00112233445566778899', public: false, masked: true } # gitleaks:allow
 | 
			
		||||
      ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when active' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns the required pipeline vars' do
 | 
			
		||||
        expect(integration.ci_variables).to match_array(ci_vars)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not return the required pipeline vars' do
 | 
			
		||||
        expect(integration.ci_variables).to be_empty
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#diffblue_link' do
 | 
			
		||||
    it { expect(described_class.diffblue_link).to include("https://www.diffblue.com/try-cover/gitlab/") }
 | 
			
		||||
  end
 | 
			
		||||
  it_behaves_like Integrations::Base::DiffblueCover
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,56 +2,6 @@
 | 
			
		|||
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::Ewm do
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when integration is active' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:new_issue_url) }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :project_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :issues_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when integration is inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:new_issue_url) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "ReferencePatternValidation" do
 | 
			
		||||
    it "extracts bug" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is bug 123")[:issue]).to eq("bug 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts task" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is task 123.")[:issue]).to eq("task 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts work item" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is work item 123 now")[:issue]).to eq("work item 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts workitem" do
 | 
			
		||||
      expect(subject.reference_pattern.match("workitem 123 at the beginning")[:issue]).to eq("workitem 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts defect" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is defect 123 defect")[:issue]).to eq("defect 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts rtcwi" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is rtcwi 123")[:issue]).to eq("rtcwi 123")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
RSpec.describe Integrations::Ewm, feature_category: :integrations do
 | 
			
		||||
  it_behaves_like Integrations::Base::Ewm
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::Instance::CustomIssueTracker, feature_category: :integrations do
 | 
			
		||||
  it_behaves_like Integrations::Base::CustomIssueTracker
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::Instance::DiffblueCover, feature_category: :integrations do
 | 
			
		||||
  it_behaves_like Integrations::Base::DiffblueCover
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'spec_helper'
 | 
			
		||||
 | 
			
		||||
RSpec.describe Integrations::Instance::Ewm, feature_category: :integrations do
 | 
			
		||||
  it_behaves_like Integrations::Base::Ewm
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -131,8 +131,6 @@ RSpec.describe 'container repository details', feature_category: :container_regi
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it_behaves_like 'returning proper responses with different permissions', raw_tags: -> { tags }
 | 
			
		||||
 | 
			
		||||
  context 'with a giant size tag' do
 | 
			
		||||
    let(:tags) { %w[latest] }
 | 
			
		||||
    let(:giant_size) { 1.terabyte }
 | 
			
		||||
| 
						 | 
				
			
			@ -217,6 +215,10 @@ RSpec.describe 'container repository details', feature_category: :container_regi
 | 
			
		|||
      GQL
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      stub_container_registry_gitlab_api_support(supported: false)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'sorts the tags', :aggregate_failures do
 | 
			
		||||
      subject
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -385,7 +387,15 @@ RSpec.describe 'container repository details', feature_category: :container_regi
 | 
			
		|||
 | 
			
		||||
  it_behaves_like 'handling graphql network errors with the container registry'
 | 
			
		||||
 | 
			
		||||
  context 'when list tags API is enabled' do
 | 
			
		||||
  context 'when the Gitlab API is not supported' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_container_registry_gitlab_api_support(supported: false)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it_behaves_like 'returning proper responses with different permissions', raw_tags: -> { tags }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when the Gitlab API is supported' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_container_registry_config(enabled: true)
 | 
			
		||||
      allow_next_instances_of(ContainerRegistry::GitlabApiClient, nil) do |client|
 | 
			
		||||
| 
						 | 
				
			
			@ -648,4 +658,79 @@ RSpec.describe 'container repository details', feature_category: :container_regi
 | 
			
		|||
      expect(migration_state_response).to eq('')
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  context 'protection field' do
 | 
			
		||||
    let(:raw_tags_response) { [{ name: 'latest', digest: 'sha256:123' }] }
 | 
			
		||||
    let(:response_body) { { response_body: ::Gitlab::Json.parse(raw_tags_response.to_json) } }
 | 
			
		||||
 | 
			
		||||
    let(:query) do
 | 
			
		||||
      <<~GQL
 | 
			
		||||
        query($id: ContainerRepositoryID!) {
 | 
			
		||||
          containerRepository(id: $id) {
 | 
			
		||||
            tags(first: 5) {
 | 
			
		||||
              nodes {
 | 
			
		||||
                protection {
 | 
			
		||||
                  minimumAccessLevelForPush
 | 
			
		||||
                  minimumAccessLevelForDelete
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      GQL
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    let(:tag_permissions_response) do
 | 
			
		||||
      container_repository_details_response.dig('tags', 'nodes')[0]['protection']
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    before_all do
 | 
			
		||||
      create(
 | 
			
		||||
        :container_registry_protection_tag_rule,
 | 
			
		||||
        project: project,
 | 
			
		||||
        tag_name_pattern: 'latest',
 | 
			
		||||
        minimum_access_level_for_push: 'maintainer',
 | 
			
		||||
        minimum_access_level_for_delete: 'owner'
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      create(
 | 
			
		||||
        :container_registry_protection_tag_rule,
 | 
			
		||||
        project: project,
 | 
			
		||||
        tag_name_pattern: '.*',
 | 
			
		||||
        minimum_access_level_for_push: 'owner',
 | 
			
		||||
        minimum_access_level_for_delete: 'maintainer'
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      create(
 | 
			
		||||
        :container_registry_protection_tag_rule,
 | 
			
		||||
        project: project,
 | 
			
		||||
        tag_name_pattern: 'non-matching-pattern',
 | 
			
		||||
        minimum_access_level_for_push: 'admin',
 | 
			
		||||
        minimum_access_level_for_delete: 'admin'
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns the maximum access fields from the matching protection rules' do
 | 
			
		||||
      subject
 | 
			
		||||
 | 
			
		||||
      expect(tag_permissions_response).to eq(
 | 
			
		||||
        {
 | 
			
		||||
          'minimumAccessLevelForPush' => 'OWNER',
 | 
			
		||||
          'minimumAccessLevelForDelete' => 'OWNER'
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when the feature container_registry_protected_tags is disabled' do
 | 
			
		||||
      before do
 | 
			
		||||
        stub_feature_flags(container_registry_protected_tags: false)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns nil' do
 | 
			
		||||
        subject
 | 
			
		||||
 | 
			
		||||
        expect(tag_permissions_response).to be_nil
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,9 +39,7 @@ RSpec.describe 'Creating a todo for the alert', feature_category: :incident_mana
 | 
			
		|||
 | 
			
		||||
  context 'todo already exists' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_feature_flags(multiple_todos: false)
 | 
			
		||||
 | 
			
		||||
      create(:todo, :pending, project: project, user: user, target: alert)
 | 
			
		||||
      post_graphql_mutation(mutation, current_user: user)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'surfaces an error' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -388,9 +388,14 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  shared_examples 'an issuable' do |issuable_type|
 | 
			
		||||
  shared_examples 'an issuable' do |param|
 | 
			
		||||
    let(:issuable_type) { param }
 | 
			
		||||
    def create_todo_for_issuable(user, iid = issuable.iid)
 | 
			
		||||
      post api("/projects/#{project_1.id}/#{issuable_type}/#{iid}/todo", user)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'creates a todo on an issuable' do
 | 
			
		||||
      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
 | 
			
		||||
      create_todo_for_issuable(john_doe)
 | 
			
		||||
 | 
			
		||||
      expect(response).to have_gitlab_http_status(:created)
 | 
			
		||||
      expect(json_response['project']).to be_a Hash
 | 
			
		||||
| 
						 | 
				
			
			@ -406,11 +411,9 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
 | 
			
		|||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns 304 there already exist a todo on that issuable' do
 | 
			
		||||
      stub_feature_flags(multiple_todos: false)
 | 
			
		||||
      create_todo_for_issuable(john_doe)
 | 
			
		||||
 | 
			
		||||
      create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
 | 
			
		||||
 | 
			
		||||
      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", john_doe)
 | 
			
		||||
      create_todo_for_issuable(john_doe)
 | 
			
		||||
 | 
			
		||||
      expect(response).to have_gitlab_http_status(:not_modified)
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -418,7 +421,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
 | 
			
		|||
    it 'returns 404 if the issuable is not found' do
 | 
			
		||||
      unknown_id = 0
 | 
			
		||||
 | 
			
		||||
      post api("/projects/#{project_1.id}/#{issuable_type}/#{unknown_id}/todo", john_doe)
 | 
			
		||||
      create_todo_for_issuable(john_doe, unknown_id)
 | 
			
		||||
 | 
			
		||||
      expect(response).to have_gitlab_http_status(:not_found)
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -427,7 +430,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do
 | 
			
		|||
      guest = create(:user)
 | 
			
		||||
      project_1.add_guest(guest)
 | 
			
		||||
 | 
			
		||||
      post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", guest)
 | 
			
		||||
      create_todo_for_issuable(guest)
 | 
			
		||||
 | 
			
		||||
      if issuable_type == 'merge_requests'
 | 
			
		||||
        expect(response).to have_gitlab_http_status(:forbidden)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,10 +58,6 @@ RSpec.describe AlertManagement::Alerts::Todo::CreateService, feature_category: :
 | 
			
		|||
            create(:todo, :pending, **todo_params)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          before do
 | 
			
		||||
            stub_feature_flags(multiple_todos: false)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          it 'does not create a todo' do
 | 
			
		||||
            expect { result }.not_to change { Todo.count }
 | 
			
		||||
          end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,29 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples Integrations::Base::CustomIssueTracker do
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when integration is active' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:new_issue_url) }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :project_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :issues_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when integration is inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:new_issue_url) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,72 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples Integrations::Base::DiffblueCover do
 | 
			
		||||
  let_it_be(:project) { build(:project) }
 | 
			
		||||
 | 
			
		||||
  subject(:integration) { build(:diffblue_cover_integration, project: project) }
 | 
			
		||||
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when active' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_license_key) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_access_token_name) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:diffblue_access_token_secret) }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_license_key) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_access_token_name) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:diffblue_access_token_secret) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#avatar_url' do
 | 
			
		||||
    it 'returns the avatar image path' do
 | 
			
		||||
      expect(integration.avatar_url).to eq(ActionController::Base.helpers.image_path(
 | 
			
		||||
        'illustrations/third-party-logos/integrations-logos/diffblue.svg'
 | 
			
		||||
      ))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#ci-vars' do
 | 
			
		||||
    let(:ci_vars) do
 | 
			
		||||
      [
 | 
			
		||||
        { key: 'DIFFBLUE_LICENSE_KEY', value: '1234-ABCD-DCBA-4321', public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN_NAME', value: 'Diffblue CI', public: false, masked: true },
 | 
			
		||||
        { key: 'DIFFBLUE_ACCESS_TOKEN',
 | 
			
		||||
          value: 'glpat-00112233445566778899', public: false, masked: true } # gitleaks:allow
 | 
			
		||||
      ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when active' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'returns the required pipeline vars' do
 | 
			
		||||
        expect(integration.ci_variables).to match_array(ci_vars)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        integration.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'does not return the required pipeline vars' do
 | 
			
		||||
        expect(integration.ci_variables).to be_empty
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#diffblue_link' do
 | 
			
		||||
    it { expect(described_class.diffblue_link).to include("https://www.diffblue.com/try-cover/gitlab/") }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
RSpec.shared_examples Integrations::Base::Ewm do
 | 
			
		||||
  describe 'Validations' do
 | 
			
		||||
    context 'when integration is active' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.to validate_presence_of(:new_issue_url) }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :project_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :issues_url
 | 
			
		||||
      it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when integration is inactive' do
 | 
			
		||||
      before do
 | 
			
		||||
        subject.active = false
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:project_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:issues_url) }
 | 
			
		||||
      it { is_expected.not_to validate_presence_of(:new_issue_url) }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "ReferencePatternValidation" do
 | 
			
		||||
    it "extracts bug" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is bug 123")[:issue]).to eq("bug 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts task" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is task 123.")[:issue]).to eq("task 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts work item" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is work item 123 now")[:issue]).to eq("work item 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts work item" do
 | 
			
		||||
      expect(subject.reference_pattern.match("workitem 123 at the beginning")[:issue]).to eq("workitem 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts defect" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is defect 123 defect")[:issue]).to eq("defect 123")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it "extracts rtcwi" do
 | 
			
		||||
      expect(subject.reference_pattern.match("This is rtcwi 123")[:issue]).to eq("rtcwi 123")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -1463,10 +1463,10 @@
 | 
			
		|||
  resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0.tgz#ac877aa76a9c45368c979471e461b520d38e6cf5"
 | 
			
		||||
  integrity sha512-56VPujlHscP5q/e7Jlpqc40sja4vOhC4uJD1llBCWolVI8ND4+VzisDVkUMl+z5y0MpIImW6HjhNc+ZvuizgOw==
 | 
			
		||||
 | 
			
		||||
"@gitlab/web-ide@^0.0.1-dev-20250109231656":
 | 
			
		||||
  version "0.0.1-dev-20250109231656"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20250109231656.tgz#dd8afb853ae04dae09e53b37710869975092bea7"
 | 
			
		||||
  integrity sha512-AO6uo8fKkmlavWfqHgYNgzMz6U+ppl1uK23uiWEy3aU16xL1/MIGda4LVxzmWLVnxz3BGg0xdqA/Ipsd4VcxVw==
 | 
			
		||||
"@gitlab/web-ide@^0.0.1-dev-20250110172049":
 | 
			
		||||
  version "0.0.1-dev-20250110172049"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20250110172049.tgz#c10d85535f272039613866082d9aa6a9068f63b1"
 | 
			
		||||
  integrity sha512-3duzCxQGRqSoPnOMG8dtepIxHlE2KvysSVrOGDMijajDpi6N68loFaUSM2E/shy+NtSPKQgltYiG1JXPxZlWaw==
 | 
			
		||||
 | 
			
		||||
"@gleam-lang/highlight.js-gleam@^1.5.0":
 | 
			
		||||
  version "1.5.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue