Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-09 18:13:01 +00:00
parent e8d15002ba
commit ed79d7cc5b
106 changed files with 2099 additions and 290 deletions

View File

@ -1 +1 @@
7e94489bc9d892e3cb25f9f9e7f4f7ce15ac0ee8
d19af9f22edb454a615b9bb90fdcaa24f72eb488

View File

@ -2,6 +2,7 @@ import initSetHelperText, {
initOptionMetricsState,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
import initProductUsageData from '~/pages/admin/application_settings/metrics_and_profiling/product_usage_data';
export default () => {
Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach((trigger) => {
@ -11,3 +12,4 @@ export default () => {
initSetHelperText();
initOptionMetricsState();
initProductUsageData();

View File

@ -2,11 +2,13 @@
import {
GlBadge,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlTooltipDirective,
GlResizeObserverDirective,
} from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import JobDropdownItem from '~/ci/common/private/job_dropdown_item.vue';
import { FAILED_STATUS } from '~/ci/constants';
import { JOB_DROPDOWN } from '../constants';
import JobItem from './job_item.vue';
@ -22,6 +24,7 @@ export default {
JobItem,
GlBadge,
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -75,6 +78,15 @@ export default {
isFailed() {
return this.group?.status?.group === 'failed';
},
nonFailedJobs() {
return this.group?.jobs.filter((job) => job.status?.group !== FAILED_STATUS);
},
failedJobs() {
return this.group?.jobs.filter((job) => job.status?.group === FAILED_STATUS);
},
hasFailedJobs() {
return this.failedJobs.length > 0;
},
},
methods: {
handleResize() {
@ -122,12 +134,26 @@ export default {
</button>
</template>
<ul class="gl-m-0 gl-w-34 gl-overflow-y-auto gl-p-0" @click.stop>
<job-dropdown-item
v-for="job in group.jobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('pipelineActionRequestComplete')"
/>
<gl-disclosure-dropdown-group v-if="hasFailedJobs" data-testid="failed-jobs">
<template #group-label>
{{ s__('Pipelines|Failed jobs') }}
</template>
<job-dropdown-item
v-for="job in failedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('pipelineActionRequestComplete')"
/>
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group :bordered="hasFailedJobs">
<job-dropdown-item
v-for="job in nonFailedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('pipelineActionRequestComplete')"
/>
</gl-disclosure-dropdown-group>
</ul>
</gl-disclosure-dropdown>
</template>

View File

@ -22,4 +22,6 @@ export const ELEMENT_IDS = Object.freeze({
USAGE_PING_FEATURES_ENABLED: 'application_setting_usage_ping_features_enabled',
USAGE_PING_ENABLED: 'application_setting_usage_ping_enabled',
OPTIONAL_METRICS_IN_SERVICE_PING: 'application_setting_include_optional_metrics_in_service_ping',
PRODUCT_USAGE_DATA: 'application_setting_gitlab_product_usage_data_enabled',
SNOWPLOW_ENABLED: 'application_setting_snowplow_enabled',
});

View File

@ -0,0 +1,33 @@
import { ELEMENT_IDS } from './constants';
export default function initProductUsageData() {
const productUsageCheckbox = document.getElementById(ELEMENT_IDS.PRODUCT_USAGE_DATA);
const snowplowCheckbox = document.getElementById(ELEMENT_IDS.SNOWPLOW_ENABLED);
const snowplowSettings = document.getElementById('js-snowplow-settings');
const toggleSnowplowSettings = () => {
if (!snowplowCheckbox || !snowplowSettings) return;
if (snowplowCheckbox.checked) {
snowplowSettings.style.display = 'block';
} else {
snowplowSettings.style.display = 'none';
}
};
toggleSnowplowSettings();
productUsageCheckbox?.addEventListener('change', () => {
if (productUsageCheckbox.checked) {
snowplowCheckbox.checked = false;
toggleSnowplowSettings();
}
});
snowplowCheckbox?.addEventListener('change', () => {
if (snowplowCheckbox.checked) {
productUsageCheckbox.checked = false;
}
toggleSnowplowSettings();
});
}

View File

@ -340,6 +340,9 @@ export default {
canSummarizeComments() {
return this.workItem.userPermissions?.summarizeComments;
},
hasBlockedWorkItemsFeature() {
return this.workItem.userPermissions?.blockedWorkItems;
},
isDiscussionLocked() {
return this.workItemNotes?.discussionLocked;
},
@ -1208,6 +1211,7 @@ export default {
:work-item-type="workItem.workItemType.name"
:can-admin-work-item-link="canAdminWorkItemLink"
:active-child-item-id="activeChildItemId"
:has-blocked-work-items-feature="hasBlockedWorkItemsFeature"
@showModal="openContextualView"
/>

View File

@ -52,6 +52,11 @@ export default {
required: false,
default: () => [],
},
hasBlockedWorkItemsFeature: {
type: Boolean,
required: true,
default: false,
},
},
data() {
return {
@ -182,22 +187,24 @@ export default {
<gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
{{ error }}
</gl-alert>
<gl-form-group
:label="linkItemFormHeaderLabel"
label-for="linked-item-type-radio"
label-class="label-bold"
class="gl-mb-3"
>
<gl-form-radio-group
id="linked-item-type-radio"
v-model="linkedItemType"
:options="linkedItemTypes"
:checked="linkedItemType"
/>
</gl-form-group>
<p class="gl-mb-2 gl-font-bold">
{{ $options.i18n.linkItemInputLabel }}
</p>
<template v-if="hasBlockedWorkItemsFeature">
<gl-form-group
:label="linkItemFormHeaderLabel"
label-for="linked-item-type-radio"
label-class="label-bold"
class="gl-mb-3"
>
<gl-form-radio-group
id="linked-item-type-radio"
v-model="linkedItemType"
:options="linkedItemTypes"
:checked="linkedItemType"
/>
</gl-form-group>
<p class="gl-mb-2 gl-font-bold">
{{ $options.i18n.linkItemInputLabel }}
</p>
</template>
<div class="gl-mb-5">
<work-item-token-input
v-model="workItemsToAdd"

View File

@ -78,6 +78,11 @@ export default {
required: false,
default: null,
},
hasBlockedWorkItemsFeature: {
type: Boolean,
required: false,
default: false,
},
},
apollo: {
linkedWorkItems: {
@ -146,6 +151,13 @@ export default {
isEmptyRelatedWorkItems() {
return !this.error && this.linkedWorkItems.length === 0;
},
emptyStateMessage() {
return this.hasBlockedWorkItemsFeature
? s__(
"WorkItem|Link items together to show that they're related or that one is blocking others.",
)
: s__("WorkItem|Link items together to show that they're related.");
},
displayableLinksCount() {
return this.displayableLinks(this.linkedWorkItems)?.length;
},
@ -307,9 +319,6 @@ export default {
i18n: {
title: s__('WorkItem|Linked items'),
fetchError: s__('WorkItem|Something went wrong when fetching items. Please refresh this page.'),
emptyStateMessage: s__(
"WorkItem|Link items together to show that they're related or that one is blocking others.",
),
noLinkedItemsOpen: s__('WorkItem|No linked items are currently open.'),
removeLinkedItemErrorMessage: s__(
'WorkItem|Something went wrong when removing item. Please refresh this page.',
@ -371,13 +380,14 @@ export default {
:work-item-full-path="workItemFullPath"
:children-ids="childrenIds"
:work-item-type="workItemType"
:has-blocked-work-items-feature="hasBlockedWorkItemsFeature"
@submitted="hideLinkItemForm"
@cancel="hideLinkItemForm"
/>
</template>
<template v-if="isEmptyRelatedWorkItems" #empty>
{{ $options.i18n.emptyStateMessage }}
{{ emptyStateMessage }}
</template>
<template #default>

View File

@ -223,7 +223,8 @@
border-color: var(--gl-control-border-color-error);
}
.ci-job-item-failed {
.ci-job-component > .ci-job-item-failed,
.ci-job-component.ci-job-item-failed:not(:hover):not(:focus) > a {
@apply gl-bg-feedback-danger;
// stylelint-disable-next-line gitlab/no-gl-class

View File

@ -6,8 +6,9 @@
%p.rd-no-preview-paragraph
= no_preview_reason
.rd-no-preview-actions
- if @diff_file.collapsed? && expandable?
= action_button(button_options: { data: { click: 'showChanges' } }) do
- if @diff_file.collapsed? && expandable? || @diff_file.whitespace_only?
- click = @diff_file.whitespace_only? ? 'showWhitespaceChanges' : 'showChanges'
= action_button(button_options: { data: { click: click } }) do
= _('Show changes')
- elsif expandable?
= action_button(button_options: { data: { click: 'showFileContents' } }) do

View File

@ -37,6 +37,8 @@ module RapidDiffs
_("Preview size limit exceeded, changes collapsed.")
elsif !@diff_file.diffable?
_("Preview suppressed by a .gitattributes entry or the file's encoding is unsupported.")
elsif @diff_file.whitespace_only?
_("Contains only whitespace changes.")
elsif @diff_file.new_file? || @diff_file.content_changed?
_("No diff preview for this file type.")
end

View File

@ -140,7 +140,11 @@ class SearchController < ApplicationController
def authenticate?
return false if action_name == 'opensearch'
return true if public_visibility_restricted?
return true if search_service.global_search? && ::Feature.enabled?(:block_anonymous_global_searches, type: :ops)
if search_service.global_search? && ::Gitlab::CurrentSettings.global_search_block_anonymous_searches_enabled?
return true
end
return true if ::Feature.disabled?(:allow_anonymous_searches, type: :ops)
false

View File

@ -74,6 +74,13 @@ module ApplicationSettingsHelper
def global_search_settings_checkboxes(form)
[
form.gitlab_ui_checkbox_component(
:global_search_block_anonymous_searches_enabled,
_("Enable blocking of anonymous global search requests"),
checkbox_options: {
checked: @application_setting.global_search_block_anonymous_searches_enabled, multiple: false
}
),
form.gitlab_ui_checkbox_component(
:global_search_issues_enabled,
_("Enable issues tab in global search results"),
@ -488,6 +495,7 @@ module ApplicationSettingsHelper
:snowplow_database_collector_hostname,
:snowplow_enabled,
:snowplow_app_id,
:gitlab_product_usage_data_enabled,
:push_event_hooks_limit,
:push_event_activities_limit,
:custom_http_clone_url_root,

View File

@ -28,6 +28,8 @@ class ApplicationSetting < ApplicationRecord
lock_pypi_package_requests_forwarding
], remove_with: '18.1', remove_after: '2025-05-20'
ignore_column :duo_nano_features_enabled, remove_with: '18.1', remove_after: '2025-06-19'
KROKI_URL_ERROR_MESSAGE = 'Please check your Kroki URL setting in ' \
'Admin area > Settings > General > Kroki'
@ -460,6 +462,8 @@ class ApplicationSetting < ApplicationRecord
validate :check_valid_runner_registrars
validate :snowplow_and_product_usage_data_are_mutually_exclusive
validate :terms_exist, if: :enforce_terms?
validates :external_authorization_service_default_label,
@ -680,7 +684,8 @@ class ApplicationSetting < ApplicationRecord
validates :clickhouse, json_schema: { filename: "application_setting_clickhouse" }
jsonb_accessor :service_ping_settings,
gitlab_environment_toolkit_instance: [:boolean, { default: false }]
gitlab_environment_toolkit_instance: [:boolean, { default: false }],
gitlab_product_usage_data_enabled: [:boolean, { default: true }]
jsonb_accessor :rate_limits_unauthenticated_git_http,
throttle_unauthenticated_git_http_enabled: [:boolean, { default: false }],
@ -1157,6 +1162,15 @@ class ApplicationSetting < ApplicationRecord
)
end
def snowplow_and_product_usage_data_are_mutually_exclusive
return unless gitlab_product_usage_data_enabled_changed? || snowplow_enabled_changed?
return unless snowplow_enabled && gitlab_product_usage_data_enabled
message = _('Snowplow tracking and Product event tracking cannot be enabled at the same time. ' \
'Please disable one of them.')
errors.add(:base, message)
end
def validate_url(parsed_url, name, error_message)
return if parsed_url

View File

@ -103,6 +103,7 @@ module ApplicationSettingImplementation
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitlab_product_usage_data_enabled: Settings.gitlab['initial_gitlab_product_usage_data'],
gitpod_enabled: false,
gitpod_url: 'https://gitpod.io/',
gravatar_enabled: Settings.gravatar['enabled'],
@ -593,10 +594,6 @@ module ApplicationSettingImplementation
signup_enabled? && password_authentication_enabled_for_web?
end
def product_usage_data_enabled?
true
end
def password_authentication_enabled?
password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
end

View File

@ -5,6 +5,7 @@ module Ci
include Presentable
include Limitable
include Expirable
include Gitlab::EncryptedAttribute
TRIGGER_TOKEN_PREFIX = 'glptt-'
@ -26,7 +27,7 @@ module Ci
attribute: :encrypted_token,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32,
key: :db_key_base_32,
encode: false
before_validation :set_default_values

View File

@ -5,6 +5,8 @@ module Ci
extend ActiveSupport::Concern
included do
include Gitlab::EncryptedAttribute
enum variable_type: {
env_var: 1,
file: 2
@ -23,7 +25,7 @@ module Ci
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Settings.attr_encrypted_db_key_base,
key: :db_key_base,
algorithm: 'aes-256-cbc'
def key=(new_key)

View File

@ -6,10 +6,12 @@ module Ci
include Ci::HasVariable
included do
include Gitlab::EncryptedAttribute
attr_encrypted :value,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32,
key: :db_key_base_32,
insecure_mode: false
end
end

View File

@ -49,7 +49,7 @@ class Event < ApplicationRecord
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
CONTRIBUTABLE_TARGET_TYPES = %w[MergeRequest Issue WorkItem].freeze
CONTRIBUTABLE_TARGET_TYPES = %w[MergeRequest Issue WorkItem DesignManagement::Design].freeze
sha_attribute :fingerprint
@ -98,7 +98,9 @@ class Event < ApplicationRecord
scope :contributions, -> do
contribution_actions = [actions[:pushed], actions[:commented]]
target_contribution_actions = [actions[:created], actions[:closed], actions[:merged], actions[:approved]]
target_contribution_actions = [
actions[:created], actions[:closed], actions[:merged], actions[:approved], actions[:updated], actions[:destroyed]
]
where(
'action IN (?) OR (target_type IN (?) AND action IN (?))',

View File

@ -4,16 +4,22 @@ class SystemHook < WebHook
extend ::Gitlab::Utils::Override
include TriggerableHooks
self.allow_legacy_sti_class = true
has_many :web_hook_logs, foreign_key: 'web_hook_id', inverse_of: :web_hook
triggerable_hooks [
AVAILABLE_HOOKS = [
:repository_update_hooks,
:push_hooks,
:tag_push_hooks,
:merge_request_hooks
]
].freeze
self.allow_legacy_sti_class = true
has_many :web_hook_logs, foreign_key: 'web_hook_id', inverse_of: :web_hook
def self.available_hooks
AVAILABLE_HOOKS
end
triggerable_hooks available_hooks
attribute :push_events, default: false
attribute :repository_update_events, default: true
@ -39,3 +45,5 @@ class SystemHook < WebHook
false
end
end
SystemHook.prepend_mod_with('SystemHook')

View File

@ -186,6 +186,8 @@ class PersonalAccessToken < ApplicationRecord
def expires_at_before_instance_max_expiry_date
return unless expires_at
return unless Gitlab::CurrentSettings.require_personal_access_token_expiry?
max_expiry_date = Date.current.advance(days: max_expiration_lifetime_in_days)
return unless expires_at > max_expiry_date

View File

@ -1378,6 +1378,7 @@ class Project < ApplicationRecord
def ancestors(hierarchy_order: nil)
group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none
end
alias_method :group_and_ancestors, :ancestors
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)

View File

@ -5,8 +5,13 @@ class ApplicationSettingPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedC
rule { admin }.policy do
enable :read_application_setting
enable :read_runners_registration_token
enable :update_runners_registration_token
end
rule { ~runner_registration_token_enabled }.prevent :update_runners_registration_token
rule { ~runner_registration_token_enabled }.policy do
prevent :read_runners_registration_token
prevent :update_runners_registration_token
end
end

View File

@ -303,6 +303,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :update_default_branch_protection
enable :create_deploy_token
enable :destroy_deploy_token
enable :read_runners_registration_token
enable :update_runners_registration_token
enable :owner_access
enable :update_git_access_protocol
@ -432,6 +433,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
rule { ~runner_registration_token_enabled }.policy do
prevent :register_group_runners
prevent :read_runners_registration_token
prevent :update_runners_registration_token
end

View File

@ -643,6 +643,7 @@ class ProjectPolicy < BasePolicy
enable :create_runner
enable :admin_project_runners
enable :read_project_runners
enable :read_runners_registration_token
enable :update_runners_registration_token
enable :admin_project_google_cloud
enable :admin_project_aws
@ -1041,6 +1042,7 @@ class ProjectPolicy < BasePolicy
rule { ~runner_registration_token_enabled }.policy do
prevent :register_project_runners
prevent :read_runners_registration_token
prevent :update_runners_registration_token
end

View File

@ -25,7 +25,12 @@ module BulkImports
private
def process_bulk_import
bulk_import.start! if bulk_import.created?
if bulk_import.created?
bulk_import.start!
# Fetch and cache the source ghost user id to avoid repeated API calls.
# This also avoids inconsistent ghost user mapping if concurrent API responses occasionally fail.
BulkImports::SourceInternalUserFinder.new(bulk_import.configuration).set_ghost_user_id
end
created_entities.first(next_batch_size).each do |entity|
create_tracker(entity)

View File

@ -0,0 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Application Setting Duo Chat",
"type": "object",
"additionalProperties": false,
"properties": {
"duo_chat_expiration_days": {
"type": "integer",
"minimum": 1,
"maximum": 30,
"description": "Number of days thread should count as expired"
},
"duo_chat_expiration_column": {
"type": "string",
"description": "Column used to determine expired thread"
}
}
}

View File

@ -3,11 +3,29 @@
%fieldset
.form-group
- label = s_('AdminSettings|Enable product usage tracking')
= f.gitlab_ui_checkbox_component :product_usage_data_enabled?, '%{label}'.html_safe % { label: label },
checkbox_options: { id: 'application_setting_product_usage_data_enabled' },
label_options: { id: 'service_ping_features_label' }
- label = s_('AdminSettings|Enable event tracking')
= f.gitlab_ui_checkbox_component :gitlab_product_usage_data_enabled, '%{label}'.html_safe % { label: label },
help_text: s_('AdminSettings|Send event data to GitLab.')
.form-group
= f.gitlab_ui_checkbox_component :snowplow_enabled, _('Enable Snowplow tracking'),
help_text: s_('AdminSettings|Send event data to your own Snowplow collector.'),
checkbox_options: { data: { testid: 'snowplow-enabled-checkbox' } }
#js-snowplow-settings.gl-ml-6
.form-group
= f.label :snowplow_collector_hostname, _('Collector hostname')
= f.text_field :snowplow_collector_hostname, class: 'form-control gl-form-input', placeholder: 'snowplow.example.com'
.form-text.gl-text-subtle
= _('The hostname of your Snowplow collector.')
.form-group
= f.label :snowplow_app_id, _('App ID')
= f.text_field :snowplow_app_id, class: 'form-control gl-form-input', placeholder: 'gitlab'
.form-text.gl-text-subtle
= _('The ID of the application.')
.form-group
= f.label :snowplow_cookie_domain, _('Cookie domain')
= f.text_field :snowplow_cookie_domain, class: 'form-control gl-form-input', placeholder: '.your-gitlab-instance.com'
.form-text.gl-text-subtle
= _('The Snowplow cookie domain.')
= f.submit _('Save changes'), pajamas_button: true

View File

@ -100,7 +100,7 @@
-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/640
= render_if_exists 'admin/application_settings/feishu_integration'
= render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow'
= render 'admin/application_settings/snowplow' if Feature.disabled?(:product_usage_data, :instance)
= render_if_exists 'admin/application_settings/product_analytics'
= render 'admin/application_settings/error_tracking' if Feature.enabled?(:gitlab_error_tracking)
= render 'admin/application_settings/eks'

View File

@ -43,12 +43,15 @@
= render 'usage'
- if Feature.enabled?(:product_usage_data, :instance)
= render ::Layouts::SettingsBlockComponent.new(_('Product usage data tracking'),
= render ::Layouts::SettingsBlockComponent.new(_('Event tracking'),
id: 'js-product-usage-data-settings',
testid: 'product-usage-data-settings-content',
expanded: expanded_by_default?) do |c|
- c.with_description do
= _('Control whether events are sent to GitLab.')
- snowplow_link = link_to(_('Snowplow'), 'https://snowplow.io/', target: '_blank', rel: 'noopener noreferrer')
- help_link = link_to(_('Learn more.'), help_page_path('development/internal_analytics/internal_event_instrumentation/_index.md'), target: '_blank', rel: 'noopener noreferrer')
= safe_format(_('Control whether events are sent to GitLab or your own %{snowplow_link_start}Snowplow%{snowplow_link_end} collector. Only one can be selected at a time and enabling one will disable the other. %{help_link_start}Learn more.%{help_link_end}'),
tag_pair(snowplow_link, :snowplow_link_start, :snowplow_link_end), tag_pair(help_link, :help_link_start, :help_link_end))
- c.with_body do
= render 'product_usage_data'

View File

@ -0,0 +1,12 @@
---
api_type:
attr: duo_chat
clusterwide: true
column: duo_chat
db_type: jsonb
default: "'{}'::jsonb"
description: A hash containing Duo Chat related settings.
encrypted: false
gitlab_com_different_than_default: false
jihu: false
not_null: true

View File

@ -1,9 +0,0 @@
---
name: fireworks_qwen_code_completion
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/500742
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170503
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/500744
milestone: '17.6'
group: group::code creation
type: beta
default_enabled: false

View File

@ -206,6 +206,12 @@ production: &base
# plaintext. This can be a security risk.
# display_initial_root_password: false
## Product Usage Data
# This setting enables or disables product usage data in the GitLab instance.
# It will be read on initial installation only. Once GitLab is up and running,
# this setting should be toggled from the admin pages in the UI.
# initial_gitlab_product_usage_data: true
# Allows delivery of emails using Microsoft Graph API with OAuth 2.0 client credentials flow.
microsoft_graph_mailer:
enabled: false

View File

@ -328,6 +328,10 @@ geo_node_namespace_links:
- table: namespaces
column: namespace_id
on_delete: async_delete
group_push_rules:
- table: namespaces
column: group_id
on_delete: async_delete
group_security_exclusions:
- table: namespaces
column: group_id

View File

@ -251,6 +251,7 @@ Settings.gitlab['max_request_duration_seconds'] ||= 57
Settings.gitlab['display_initial_root_password'] = false if Settings.gitlab['display_initial_root_password'].nil?
Settings.gitlab['weak_passwords_digest_set'] ||= YAML.safe_load(File.open(Rails.root.join('config', 'weak_password_digests.yml')), permitted_classes: [String]).to_set.freeze
Settings.gitlab['log_decompressed_response_bytesize'] = ENV["GITLAB_LOG_DECOMPRESSED_RESPONSE_BYTESIZE"].to_i > 0 ? ENV["GITLAB_LOG_DECOMPRESSED_RESPONSE_BYTESIZE"].to_i : 0
Settings.gitlab['initial_gitlab_product_usage_data'] = true if Settings.gitlab['initial_gitlab_product_usage_data'].nil?
Gitlab.ee do
Settings.gitlab['mirror_max_delay'] ||= 300
@ -996,7 +997,7 @@ Gitlab.ee do
Settings.cron_jobs['members_schedule_prune_deletions_worker']['cron'] ||= "*/5 * * * *"
Settings.cron_jobs['members_schedule_prune_deletions_worker']['job_class'] = 'Members::SchedulePruneDeletionsWorker'
Settings.cron_jobs['ai_conversation_cleanup_cron_worker'] ||= {}
Settings.cron_jobs['ai_conversation_cleanup_cron_worker']['cron'] ||= '30 2 * * *'
Settings.cron_jobs['ai_conversation_cleanup_cron_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['ai_conversation_cleanup_cron_worker']['job_class'] = 'Ai::Conversation::CleanupCronWorker'
Settings.cron_jobs['ai_active_context_bulk_process_worker'] ||= {}
Settings.cron_jobs['ai_active_context_bulk_process_worker']['cron'] ||= '*/1 * * * *'

View File

@ -9,7 +9,7 @@ time_frame: none
data_source: system
instrumentation_class: GitlabSettingsMetric
options:
setting_method: product_usage_data_enabled?
setting_method: gitlab_product_usage_data_enabled?
tiers:
- free
- premium

View File

@ -11,4 +11,4 @@ milestone: '13.0'
gitlab_schema: gitlab_ci
sharding_key:
project_id: projects
table_size: small
table_size: medium

View File

@ -9,4 +9,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75264
milestone: '14.8'
gitlab_schema: gitlab_ci_cell_local
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/493768
table_size: medium
table_size: large

View File

@ -10,4 +10,4 @@ milestone: '13.2'
gitlab_schema: gitlab_ci
sharding_key:
project_id: projects
table_size: large
table_size: over_limit

View File

@ -8,4 +8,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107801
milestone: '15.8'
gitlab_schema: gitlab_ci
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/460084
table_size: small
table_size: medium

View File

@ -10,4 +10,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/046b28312704f31
milestone: '8.0'
gitlab_schema: gitlab_ci
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442395
table_size: small
table_size: medium

View File

@ -0,0 +1,12 @@
---
table_name: group_push_rules
classes:
- GroupPushRule
feature_categories:
- source_code_management
description: Store push rules for groups
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185982
milestone: '17.11'
gitlab_schema: gitlab_main_cell
sharding_key:
group_id: namespaces

View File

@ -11,4 +11,4 @@ milestone: '17.6'
gitlab_schema: gitlab_ci
sharding_key:
sharding_key_id: namespaces
table_size: small
table_size: medium

View File

@ -4,6 +4,8 @@ class AddPartitionIdToCiPipelineMessage < Gitlab::Database::Migration[2.2]
milestone '17.1'
def change
# rubocop:disable Migration/PreventAddingColumns -- Legacy migration
add_column(:ci_pipeline_messages, :partition_id, :bigint, default: 100, null: false)
# rubocop:enable Migration/PreventAddingColumns -- Legacy migration
end
end

View File

@ -4,6 +4,8 @@ class AddProjectIdToCiPipelineMessages < Gitlab::Database::Migration[2.2]
milestone '17.6'
def change
# rubocop:disable Migration/PreventAddingColumns -- Legacy migration
add_column(:ci_pipeline_messages, :project_id, :bigint)
# rubocop:enable Migration/PreventAddingColumns -- Legacy migration
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class IndexAiConversationThreadsOnCreatedAt < Gitlab::Database::Migration[2.2]
INDEX_NAME = 'index_ai_conversation_threads_on_created_at'
disable_ddl_transaction!
milestone '17.11'
def up
add_concurrent_index :ai_conversation_threads, :created_at, name: INDEX_NAME
end
def down
remove_concurrent_index :ai_conversation_threads, :created_at, name: INDEX_NAME
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddDuoChatToApplicationSettings < Gitlab::Database::Migration[2.2]
milestone '17.11'
disable_ddl_transaction!
CONSTRAINT_NAME = 'check_application_settings_duo_chat_is_hash'
def up
add_column :application_settings, :duo_chat, :jsonb, default: {}, null: false
add_check_constraint(
:application_settings,
"(jsonb_typeof(duo_chat) = 'object')",
CONSTRAINT_NAME
)
end
def down
remove_column :application_settings, :duo_chat
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class CreateGroupPushRules < Gitlab::Database::Migration[2.2]
milestone '17.11'
def up
create_table :group_push_rules, if_not_exists: true do |t|
t.timestamps_with_timezone null: false
t.references :group, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
t.integer :max_file_size, null: false, default: 0
t.boolean :member_check, null: false, default: false
t.boolean :prevent_secrets, null: false, default: false
t.boolean :commit_committer_name_check, null: false, default: false
t.boolean :deny_delete_tag
t.boolean :reject_unsigned_commits
t.boolean :commit_committer_check
t.boolean :reject_non_dco_commits
t.text :commit_message_regex, limit: 511
t.text :branch_name_regex, limit: 511
t.text :commit_message_negative_regex, limit: 2047
t.text :author_email_regex, limit: 511
t.text :file_name_regex, limit: 511
end
end
def down
drop_table :group_push_rules, if_exists: true
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class EnsureGitlabProductUsageDataEnabledInServicePingSettings < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '17.11'
def up
default_value = true
execute <<~SQL
UPDATE application_settings
SET service_ping_settings =
CASE
WHEN snowplow_enabled = TRUE THEN
COALESCE(service_ping_settings, '{}'::jsonb) ||
jsonb_build_object('gitlab_product_usage_data_enabled', FALSE)
ELSE
COALESCE(service_ping_settings, '{}'::jsonb) ||
jsonb_build_object('gitlab_product_usage_data_enabled', #{default_value})
END
SQL
end
def down
# no-op
end
end

View File

@ -0,0 +1 @@
dc1fbc25f53ef725eb4b05220486d5452c9377699bd914ee55c567dfab6674b0

View File

@ -0,0 +1 @@
a3b4525eea40fde40c92803d5368f4ada21abe79d5de01b5e5507b2e108fc197

View File

@ -0,0 +1 @@
ff0832e10754451488a921cfb0bfe68390448c1cd4c53d5175d6a378c418a6ce

View File

@ -0,0 +1 @@
b506cb61c1b71884da28f9e10c9ba164f9f76de8bc608019235cd1a85a38bea2

View File

@ -9093,6 +9093,7 @@ CREATE TABLE application_settings (
ci_cd_settings jsonb DEFAULT '{}'::jsonb NOT NULL,
duo_nano_features_enabled boolean,
database_reindexing jsonb DEFAULT '{}'::jsonb NOT NULL,
duo_chat jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
@ -9148,6 +9149,7 @@ CREATE TABLE application_settings (
CONSTRAINT check_application_settings_cluster_agents_is_hash CHECK ((jsonb_typeof(cluster_agents) = 'object'::text)),
CONSTRAINT check_application_settings_code_creation_is_hash CHECK ((jsonb_typeof(code_creation) = 'object'::text)),
CONSTRAINT check_application_settings_database_reindexing_is_hash CHECK ((jsonb_typeof(database_reindexing) = 'object'::text)),
CONSTRAINT check_application_settings_duo_chat_is_hash CHECK ((jsonb_typeof(duo_chat) = 'object'::text)),
CONSTRAINT check_application_settings_duo_workflow_is_hash CHECK ((jsonb_typeof(duo_workflow) = 'object'::text)),
CONSTRAINT check_application_settings_elasticsearch_is_hash CHECK ((jsonb_typeof(elasticsearch) = 'object'::text)),
CONSTRAINT check_application_settings_importers_is_hash CHECK ((jsonb_typeof(importers) = 'object'::text)),
@ -15136,6 +15138,40 @@ CREATE TABLE group_merge_request_approval_settings (
require_reauthentication_to_approve boolean DEFAULT false NOT NULL
);
CREATE TABLE group_push_rules (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
group_id bigint NOT NULL,
max_file_size integer DEFAULT 0 NOT NULL,
member_check boolean DEFAULT false NOT NULL,
prevent_secrets boolean DEFAULT false NOT NULL,
commit_committer_name_check boolean DEFAULT false NOT NULL,
deny_delete_tag boolean,
reject_unsigned_commits boolean,
commit_committer_check boolean,
reject_non_dco_commits boolean,
commit_message_regex text,
branch_name_regex text,
commit_message_negative_regex text,
author_email_regex text,
file_name_regex text,
CONSTRAINT check_0bba2c16da CHECK ((char_length(commit_message_negative_regex) <= 2047)),
CONSTRAINT check_41c1a11ab8 CHECK ((char_length(file_name_regex) <= 511)),
CONSTRAINT check_6f0da85c6c CHECK ((char_length(commit_message_regex) <= 511)),
CONSTRAINT check_710cf4213a CHECK ((char_length(author_email_regex) <= 511)),
CONSTRAINT check_b02376d0ad CHECK ((char_length(branch_name_regex) <= 511))
);
CREATE SEQUENCE group_push_rules_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE group_push_rules_id_seq OWNED BY group_push_rules.id;
CREATE TABLE group_repository_storage_moves (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -27156,6 +27192,8 @@ ALTER TABLE ONLY group_group_links ALTER COLUMN id SET DEFAULT nextval('group_gr
ALTER TABLE ONLY group_import_states ALTER COLUMN group_id SET DEFAULT nextval('group_import_states_group_id_seq'::regclass);
ALTER TABLE ONLY group_push_rules ALTER COLUMN id SET DEFAULT nextval('group_push_rules_id_seq'::regclass);
ALTER TABLE ONLY group_repository_storage_moves ALTER COLUMN id SET DEFAULT nextval('group_repository_storage_moves_id_seq'::regclass);
ALTER TABLE ONLY group_saved_replies ALTER COLUMN id SET DEFAULT nextval('group_saved_replies_id_seq'::regclass);
@ -29650,6 +29688,9 @@ ALTER TABLE ONLY group_import_states
ALTER TABLE ONLY group_merge_request_approval_settings
ADD CONSTRAINT group_merge_request_approval_settings_pkey PRIMARY KEY (group_id);
ALTER TABLE ONLY group_push_rules
ADD CONSTRAINT group_push_rules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY group_repository_storage_moves
ADD CONSTRAINT group_repository_storage_moves_pkey PRIMARY KEY (id);
@ -33537,6 +33578,8 @@ CREATE INDEX index_ai_conversation_messages_on_organization_id ON ai_conversatio
CREATE INDEX index_ai_conversation_messages_on_thread_id_and_created_at ON ai_conversation_messages USING btree (thread_id, created_at);
CREATE INDEX index_ai_conversation_threads_on_created_at ON ai_conversation_threads USING btree (created_at);
CREATE INDEX index_ai_conversation_threads_on_last_updated_at ON ai_conversation_threads USING btree (last_updated_at);
CREATE INDEX index_ai_conversation_threads_on_organization_id ON ai_conversation_threads USING btree (organization_id);
@ -35183,6 +35226,8 @@ CREATE INDEX index_group_import_states_on_user_id ON group_import_states USING b
CREATE UNIQUE INDEX index_group_microsoft_applications_on_temp_source_id ON system_access_group_microsoft_applications USING btree (temp_source_id);
CREATE INDEX index_group_push_rules_on_group_id ON group_push_rules USING btree (group_id);
CREATE INDEX index_group_repository_storage_moves_on_group_id ON group_repository_storage_moves USING btree (group_id);
CREATE INDEX index_group_saved_replies_on_group_id ON group_saved_replies USING btree (group_id);
@ -44048,6 +44093,9 @@ ALTER TABLE ONLY dast_profiles
ALTER TABLE ONLY group_custom_attributes
ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_push_rules
ADD CONSTRAINT fk_rails_2515e57aa7 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_oncall_rotations
ADD CONSTRAINT fk_rails_256e0bc604 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE;

View File

@ -46745,9 +46745,6 @@ definitions:
type: boolean
ci_pipeline_variables_minimum_override_role:
type: string
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
runner_token_expiration_interval:
type: integer
format: int32
@ -46768,6 +46765,9 @@ definitions:
example: continuous
ci_push_repository_for_job_token_allowed:
type: boolean
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
ci_config_path:
type: string
example: ''
@ -62326,9 +62326,6 @@ definitions:
type: boolean
ci_pipeline_variables_minimum_override_role:
type: string
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
runner_token_expiration_interval:
type: integer
format: int32
@ -62349,6 +62346,9 @@ definitions:
example: continuous
ci_push_repository_for_job_token_allowed:
type: boolean
runners_token:
type: string
example: b8547b1dc37721d05889db52fa2f02
ci_config_path:
type: string
example: ''

View File

@ -14,12 +14,12 @@ title: Application Settings analysis
## Statistics
- Number of attributes: 493
- Number of attributes: 495
- Number of encrypted attributes: 41 (8.0%)
- Number of attributes documented: 298 (60.0%)
- Number of attributes on GitLab.com different from the defaults: 222 (45.0%)
- Number of attributes with `clusterwide` set: 493 (100.0%)
- Number of attributes with `clusterwide: true` set: 125 (25.0%)
- Number of attributes with `clusterwide` set: 495 (100.0%)
- Number of attributes with `clusterwide: true` set: 126 (25.0%)
## Individual columns
@ -147,6 +147,7 @@ title: Application Settings analysis
| `domain_denylist` | `false` | `text` | `array of strings` | `false` | `null` | `true` | `true`| `true` |
| `domain_denylist_enabled` | `false` | `boolean` | `boolean` | `false` | `false` | `true` | `true`| `true` |
| `dsa_key_restriction` | `false` | `integer` | `integer` | `true` | `'-1'::integer` | `false` | `false`| `true` |
| `duo_chat` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `true` | `true`| `false` |
| `duo_features_enabled` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` |
| `duo_workflow` | `false` | `jsonb` | `` | `false` | `'{}'::jsonb` | `true` | `true`| `false` |
| `ecdsa_key_restriction` | `false` | `integer` | `integer` | `true` | `0` | `false` | `false`| `true` |

View File

@ -596,6 +596,7 @@ only project records are indexed and no associated data can be searched.
- Global search for limited indexing [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41041) in GitLab 13.4 [with a flag](../../administration/feature_flags.md) named `advanced_global_search_for_limited_indexing`. Disabled by default.
- [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/244276) in GitLab 14.2.
- Global search for limited indexing [generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186727) in GitLab 17.11 as a UI option, instead of the `advanced_global_search_for_limited_indexing` flag.
{{< /history >}}
@ -609,12 +610,15 @@ When you index only some namespaces:
For example, if you index two separate groups, you must run separate code searches on each group individually.
{{< alert type="warning" >}}
To enable global search for limited indexing:
If you've already indexed your instance, you must [reindex the instance](#index-the-instance)
to delete all existing data for filtering to work correctly.
{{< /alert >}}
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Advanced search**
1. Select **Enable global search for limited indexing**.
1. Select **Save changes**.
1. If you've already indexed your instance, you must [reindex the instance](#index-the-instance).
This deletes existing search data, to enable filtering to work correctly.
## Enable custom language analyzers

View File

@ -18,11 +18,12 @@ pricing for additional compute minutes on the [GitLab Pricing page](https://abou
Additional compute minutes:
- Are valid for 12 months from date of purchase or until all compute minutes are consumed,
whichever comes first. Expiry of compute minutes is not enforced.
- Are used only after the monthly quota included in your subscription runs out.
- Are [carried over to the next month](#monthly-rollover-of-purchased-compute-minutes),
if any remain at the end of the month.
- Are valid for 12 months from date of purchase if not consumed earlier.
- Expiry of compute minutes is not yet enforced, which allows their use even after the expiry date.
However, GitLab does not guarantee that compute minutes will remain valid after the expiry date.
- Bought on a trial subscription are available after the trial ends or upgrading to a paid plan.
- Remain available when you change subscription tiers, including changes between paid tiers or to the Free tier.

View File

@ -180,31 +180,69 @@ Each control includes logic that GitLab uses during scheduled or triggered scans
The following controls are available to use in framework requirements:
- **SAST running**
- **At least two approvals**
- **Author approved merge request**
- **Committers approved merge request**
- **Internal visibility is forbidden**
- **Default branch protected**
- **Auth SSO enabled**
- **Secret detection running**
- **Dependency scanning running**
- **Container scanning running**
- **License compliance running**
- **DAST running**
- **API security running**
- **Fuzz testing running**
- **Code quality running**
- **IaC scanning running**
- **Code changes requires code owners**
- **Reset approvals on push**
- **Status checks required**
- **Require branch up to date**
- **Resolve discussions required**
- **Require linear history**
- **Restrict push/merge access**
- **Force push disabled**
- **Terraform enabled**
### GitLab Compliance Controls
This table documents all available controls that can be used in GitLab compliance frameworks. Controls are checks against the configuration or behavior of projects that are assigned to a compliance framework.
| Name | ID | Description | Documentation Link |
|------|----|-----------|--------------------|
| SAST running | `scanner_sast_running` | Ensures Static Application Security Testing (SAST) is configured and running in the project pipelines. | [SAST Configuration](../../user/application_security/sast/_index.md) |
| At least two approvals | `minimum_approvals_required_2` | Ensures that merge requests require at least two approvals before merging. | [Merge request approvals](../../user/project/merge_requests/approvals/_index.md) |
| Author approved merge request | `merge_request_prevent_author_approval` | Ensures that the author of a merge request cannot approve their own changes. | [Merge request approvals](../../user/project/merge_requests/approvals/_index.md) |
| Committers approved merge request | `merge_request_prevent_committers_approval` | Ensures that users who have committed to a merge request cannot approve it. | [Merge Request Approvals](../../user/project/merge_requests/approvals/_index.md) |
| Internal visibility is forbidden | `project_visibility_not_internal` | Ensures projects are not set to internal visibility. | [Project Visibility](../../user/public_access.md) |
| Default branch protected | `default_branch_protected` | Ensures the default branch has protection rules enabled. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Auth SSO enabled | `auth_sso_enabled` | Ensures Single Sign-On (SSO) authentication is enabled for the project. | [SSO for GitLab.com Groups](../../user/group/saml_sso/_index.md) |
| Secret detection running | `scanner_secret_detection_running` | Ensures secret detection scanning is configured and running in the project pipelines. | [Secret Detection](../../user/application_security/secret_detection/_index.md) |
| Dependency scanning running | `scanner_dep_scanning_running` | Ensures dependency scanning is configured and running in the project pipelines. | [Dependency Scanning](../../user/application_security/dependency_scanning/_index.md) |
| Container scanning running | `scanner_container_scanning_running` | Ensures container scanning is configured and running in the project pipelines. | [Container Scanning](../../user/application_security/container_scanning/_index.md) |
| License compliance running | `scanner_license_compliance_running` | Ensures license compliance scanning is configured and running in the project pipelines. | [License Compliance](../../user/compliance/license_approval_policies.md) |
| DAST running | `scanner_dast_running` | Ensures Dynamic Application Security Testing (DAST) is configured and running in the project pipelines. | [DAST Configuration](../../user/application_security/dast/_index.md) |
| API security running | `scanner_api_security_running` | Ensures API security scanning is configured and running in the project pipelines. | [API Security](../../user/application_security/api_security/_index.md) |
| Fuzz testing running | `scanner_fuzz_testing_running` | Ensures fuzz testing is configured and running in the project pipelines. | [Fuzz Testing](../../user/application_security/coverage_fuzzing/_index.md) |
| Code quality running | `scanner_code_quality_running` | Ensures code quality scanning is configured and running in the project pipelines. | [Code Quality](../../ci/testing/code_quality.md) |
| IaC scanning running | `scanner_iac_running` | Ensures Infrastructure as Code (IaC) scanning is configured and running in the project pipelines. | [IaC Security](../../user/application_security/iac_scanning/_index.md) |
| Code changes requires code owners | `code_changes_requires_code_owners` | Ensures code changes require approval from code owners. | [Code Owners](../../user/project/codeowners/_index.md) |
| Reset approvals on push | `reset_approvals_on_push` | Ensures approvals are reset when new commits are pushed to the merge request. | [Reset Approvals on Push](../../user/project/merge_requests/approvals/settings.md) |
| Status checks required | `status_checks_required` | Ensures status checks must pass before merging is allowed. | [Status Checks](../../user/project/merge_requests/status_checks.md) |
| Require branch up to date | `require_branch_up_to_date` | Ensures the source branch is up to date with the target branch before merging. | [Merge Requests](../../user/project/merge_requests/methods/_index.md) |
| Resolve discussions required | `resolve_discussions_required` | Ensures all discussions must be resolved before merging is allowed. | [Resolve Discussions](../../user/discussions/_index.md) |
| Require linear history | `require_linear_history` | Ensures a linear commit history by forbidding merge commits. | [Merge Request Fast-forward Merges](../../user/project/merge_requests/methods/_index.md#fast-forward-merge) |
| Restrict push/merge access | `restrict_push_merge_access` | Restricts who can push to or merge into protected branches. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Force push disabled | `force_push_disabled` | Prevents force pushing to repositories. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Terraform enabled | `terraform_enabled` | Ensures Terraform integration is enabled for the project. | [Terraform in GitLab](../../administration/terraform_state.md) |
| Version control enabled | `version_control_enabled` | Ensures version control functionality is enabled for the project. | [Git in GitLab](../../topics/git/_index.md) |
| Issue tracking enabled | `issue_tracking_enabled` | Ensures issue tracking functionality is enabled for the project. | [GitLab Issues](../../user/project/issues/_index.md) |
| Stale branch cleanup enabled | `stale_branch_cleanup_enabled` | Ensures automatic cleanup of stale branches is enabled. | [Deleting Branches](../../user/project/repository/branches/_index.md) |
| Branch deletion disabled | `branch_deletion_disabled` | Prevents deletion of branches. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Review and archive stale repositories | `review_and_archive_stale_repos` | Ensures stale repositories are reviewed and archived. | [Archiving Projects](../../user/project/settings/_index.md) |
| Review and remove inactive users | `review_and_remove_inactive_users` | Ensures inactive users are reviewed and removed. | [Managing Users](../../administration/admin_area.md) |
| Minimum number of admins | `minimum_number_of_admins` | Ensures a minimum number of administrators are assigned to the project. | [Project Members](../../user/project/members/_index.md) |
| Require MFA for contributors | `require_mfa_for_contributors` | Ensures contributors have Multi-Factor Authentication enabled. | [MFA for Contributors](../../user/profile/account/two_factor_authentication.md) |
| Require MFA at org level | `require_mfa_at_org_level` | Ensures Multi-Factor Authentication is required at the organization level. | [Group-level MFA Enforcement](../../user/profile/account/two_factor_authentication.md) |
| Ensure 2 admins per repository | `ensure_2_admins_per_repo` | Ensures at least two administrators are assigned to each repository. | [Project Members](../../user/project/members/_index.md) |
| Strict permission for repository | `strict_permissions_for_repo` | Ensures strict permissions are set for repository access. | [Project Members Permissions](../../user/permissions.md) |
| Secure webhooks | `secure_webhooks` | Ensures webhooks are securely configured. | [Webhooks](../../user/project/integrations/webhooks.md) |
| Restricted build access | `restricted_build_access` | Restricts access to build artifacts and pipeline outputs. | [Pipeline Security](../../ci/pipelines/settings.md) |
| GitLab license level ultimate | `gitlab_license_level_ultimate` | Ensures the GitLab instance is using an Ultimate license level. | [GitLab Licensing](https://about.gitlab.com/pricing/feature-comparison/) |
| Status page configured | `status_page_configured` | Ensures a status page is configured for the project. | [Status Page](../../operations/incident_management/status_page.md) |
| Has valid CI config | `has_valid_ci_config` | Ensures the project has a valid CI/CD configuration. | [CI/CD Pipeline Configuration](../../ci/yaml/_index.md) |
| Error tracking enabled | `error_tracking_enabled` | Ensures error tracking is enabled for the project. | [Error Tracking](../../operations/error_tracking.md) |
| Default branch users can push | `default_branch_users_can_push` | Controls whether users can push directly to the default branch. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Default branch protected from direct push | `default_branch_protected_from_direct_push` | Prevents direct pushes to the default branch. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Push protection enabled | `push_protection_enabled` | Ensures push protection is enabled for sensitive files. | [Push Rules](../../user/project/repository/push_rules.md) |
| Project marked for deletion | `project_marked_for_deletion` | Checks if project is marked for deletion (false is compliant). | [Project Settings](../../user/project/settings/_index.md) |
| Project archived | `project_archived` | Checks if project is archived (typically false is compliant). | [Archiving Projects](../../user/project/settings/_index.md) |
| Default branch users can merge | `default_branch_users_can_merge` | Controls whether users can merge changes to the default branch. | [Protected Branches](../../user/project/repository/branches/protected.md) |
| Merge request commit reset approvals | `merge_request_commit_reset_approvals` | Ensures new commits to merge requests reset approvals. | [Reset Approvals on Push](../../user/project/merge_requests/approvals/settings.md) |
| Project visibility not public | `project_visibility_not_public` | Ensures projects are not set to public visibility. | [Project Visibility](../../user/public_access.md) |
| Package hunter no findings untriaged | `package_hunter_no_findings_untriaged` | Ensures all package hunter findings are triaged. | [Package Hunter](../../user/application_security/triage/_index.md) |
| Project pipelines not public | `project_pipelines_not_public` | Ensures project pipelines are not publicly visible. | [Pipeline Settings](../../ci/pipelines/settings.md) |
| Vulnerabilities SLO days over threshold | `vulnerabilities_slo_days_over_threshold` | Ensures vulnerabilities are addressed within SLO thresholds. | [Vulnerability Management](../../user/application_security/vulnerabilities/_index.md) |
| Merge requests approval rules prevent editing | `merge_requests_approval_rules_prevent_editing` | Prevents editing of merge request approval rules. | [Merge Request Approvals Settings](../../user/project/merge_requests/approvals/settings.md) |
| Project user defined variables restricted to maintainers | `project_user_defined_variables_restricted_to_maintainers` | Restricts creation of project variables to maintainers only. | [Project CI/CD Variables](../../ci/variables/_index.md) |
| Merge requests require code owner approval | `merge_requests_require_code_owner_approval` | Ensures merge requests require approval from code owners. | [Code Owners](../../user/project/codeowners/_index.md) |
| CI/CD job token scope enabled | `cicd_job_token_scope_enabled` | Ensures CI/CD job token scope restrictions are enabled. | [CI/CD Job Token](../../ci/jobs/ci_job_token.md) |
#### External controls
@ -230,8 +268,8 @@ To add an external control when creating or editing a framework:
1. Select **New framework** or edit an existing one.
1. In the **Requirements** section, select **New requirement**.
1. Select **Add an external control**.
1. In the feilds edit **External URL** and **HMAC shared secret**.
1. Select **Save changes to the framework** to save the requirment.
1. In the fields edit **External URL** and **`HMAC` shared secret**.
1. Select **Save changes to the framework** to save the requirement.
#### External control lifecycle
@ -265,10 +303,10 @@ To add a requirement when creating or editing a framework:
1. On the page, select the **Frameworks** tab.
1. Select **New framework** or edit an existing one.
1. In the **Requirements** section, select **New requirement**.
1. In the popup add **Name** and **Description**.
1. In the dialog add **Name** and **Description**.
1. Select **Add a GitLab control** to add more controls.
1. In the control dropdown search and select a control.
1. Select **Save changes to the framework** to save the requirment.
1. In the control dropdown list search and select a control.
1. Select **Save changes to the framework** to save the requirement.
### Edit requirements
@ -279,8 +317,8 @@ To edit a requirement when creating or editing a framework:
1. On the page, select the **Frameworks** tab.
1. Select **New framework** or edit an existing one.
1. In the **Requirements** section, select **Action** > **Edit**.
1. In the popup edit **Name** and **Description**.
1. In the dialog edit **Name** and **Description**.
1. Select **Add a GitLab control** to add more controls.
1. In the control dropdown search and select a control.
1. In the control dropdown list search and select a control.
1. Select {{< icon name="remove" >}} to remove a control.
1. Select **Save changes to the framework** to save the requirment.
1. Select **Save changes to the framework** to save the requirement.

View File

@ -11,8 +11,7 @@ title: Code Suggestions
- Tier: Premium, Ultimate
- Add-on: GitLab Duo Pro or Enterprise, GitLab Duo with Amazon Q
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
- LLMs: For code completion, Vertex AI-hosted [`Codestral`](https://console.cloud.google.com/vertex-ai/publishers/mistralai/model-garden/codestral-2501) and Fireworks AI-hosted [`Qwen2.5 7B`](https://fireworks.ai/models/fireworks/qwen2p5-coder-7b). For code generation, Anthropic [Claude 3.7 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-7-sonnet).
- To opt out of Qwen2.5 7B for a group, the feature flag `code_completion_model_opt_out_from_fireworks_qwen` is available.
- LLMs: For code completion, Vertex AI-hosted [`Codestral`](https://console.cloud.google.com/vertex-ai/publishers/mistralai/model-garden/codestral-2501) and Fireworks AI-hosted [`Codestral`](https://mistral.ai/news/codestral-2501). For code generation, Anthropic [Claude 3.7 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-7-sonnet).
- LLM for Amazon Q: Amazon Q Developer
{{< /details >}}
@ -26,6 +25,7 @@ title: Code Suggestions
- [Changed](https://gitlab.com/gitlab-org/fulfillment/meta/-/issues/2031) to require the GitLab Duo Pro add-on on February 15, 2024. Previously, this feature was included with Premium and Ultimate subscriptions.
- [Changed](https://gitlab.com/gitlab-org/fulfillment/meta/-/issues/2031) to require the GitLab Duo Pro or GitLab Duo Enterprise add-on for all supported GitLab versions starting October 17, 2024.
- [Introduced support for Fireworks AI-hosted Qwen2.5 code completion model](https://gitlab.com/groups/gitlab-org/-/epics/15850) in GitLab 17.6, with a flag named `fireworks_qwen_code_completion`.
- Removed support for Qwen2.5 code completion model
{{< /history >}}

View File

@ -50,6 +50,14 @@ For more information, see [issue 477333](https://gitlab.com/gitlab-org/gitlab/-/
{{< /details >}}
{{< history >}}
- Restricting global search to authenticated users [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41041) in GitLab 13.4 [with a flag](../../administration/feature_flags.md) named `block_anonymous_global_searches`. Disabled by default.
- Enabling or disabling anonymous searches [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138975) in GitLab 16.7 [with a flag](../../administration/feature_flags.md) named `allow_anonymous_searches`. Enabled by default.
- Enabling or disabling anonymous searches [generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186727) in GitLab 17.11 as a UI option, instead of the `block_anonymous_global_searches` flag.
{{< /history >}}
Prerequisites:
- You must have administrator access to the instance.
@ -59,12 +67,16 @@ By default, requests to `/search` and global search are available for unauthenti
To restrict `/search` to authenticated users only, do one of the following:
- [Restrict public visibility](../../administration/settings/visibility_and_access_controls.md#restrict-visibility-levels)
([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171368) in GitLab 17.6).
- Disable the `ops` feature flag `allow_anonymous_searches`
([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138975) in GitLab 16.7).
of the project or group.
- Disable the [feature flag](../../administration/feature_flags.md) `allow_anonymous_searches`.
To restrict global search to authenticated users only,
enable the `ops` feature flag `block_anonymous_global_searches`.
To restrict global search to authenticated users only:
1. On the left sidebar, at the bottom, select **Admin**.
1. Select **Settings > Search**.
1. Expand **Global search**
1. Select **Enable blocking of anonymous global search requests**.
1. Select **Save changes**.
## Disable global search scopes

View File

@ -126,7 +126,6 @@ module API
expose :keep_latest_artifacts_available?, as: :keep_latest_artifact, documentation: { type: 'boolean' }
expose :restrict_user_defined_variables, documentation: { type: 'boolean' }
expose :ci_pipeline_variables_minimum_override_role, documentation: { type: 'string' }
expose :runners_token, documentation: { type: 'string', example: 'b8547b1dc37721d05889db52fa2f02' }
expose :runner_token_expiration_interval, documentation: { type: 'integer', example: 3600 }
expose :group_runners_enabled, documentation: { type: 'boolean' }
expose :auto_cancel_pending_pipelines, documentation: { type: 'string', example: 'enabled' }
@ -138,6 +137,11 @@ module API
expose :ci_push_repository_for_job_token_allowed, documentation: { type: 'boolean' }
end
with_options if: ->(_, _) { Ability.allowed?(options[:current_user], :read_runners_registration_token, project) } do
# Runner token settings
expose :runners_token, documentation: { type: 'string', example: 'b8547b1dc37721d05889db52fa2f02' }
end
expose :ci_config_path, documentation: { type: 'string', example: '' }, if: ->(project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
expose :public_builds, as: :public_jobs, documentation: { type: 'boolean' }

View File

@ -199,6 +199,10 @@ module BulkImports
# as they lack a foreign key constraint.
next if IGNORE_PLACEHOLDER_USER_CREATION[relation_key]&.include?(reference)
# Skip creating placeholder users for imported ghost users.
# Ghost user contributions are assigned directly to the destination ghost user
next if context.source_ghost_user_id.to_s == relation_hash[reference].to_s
source_user_mapper.find_or_create_source_user(
source_name: nil,
source_username: nil,

View File

@ -46,6 +46,10 @@ module BulkImports
@configuration ||= bulk_import.configuration
end
def source_ghost_user_id
@source_ghost_user_id ||= BulkImports::SourceInternalUserFinder.new(configuration).cached_ghost_user_id
end
def source_user_mapper
@source_user_mapper ||= Gitlab::Import::SourceUserMapper.new(
namespace: portable.root_ancestor,

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
module BulkImports # rubocop:disable Gitlab/BoundedContexts -- legacy use
class SourceInternalUserFinder
include Gitlab::Utils::StrongMemoize
GHOST_USER_CACHE_KEY = 'bulk_imports/ghost_user_id/%{bulk_import_id}'
MAX_RETRIES = 3
# @param [BulkImports::Configuration] configuration
def initialize(configuration)
@configuration = configuration
end
# @return [Hash, nil]
def fetch_ghost_user
attempt = 0
begin
attempt += 1
query = <<~GRAPHQL
{
users(usernames: ["ghost", "ghost1", "ghost2", "ghost3", "ghost4", "ghost5", "ghost6"], humans: false) {
nodes {
id
username
type
}
}
}
GRAPHQL
response = client.execute(query: query)
response = response.dig('data', 'users', 'nodes') || []
response.find { |user| user["type"] == 'GHOST' }
rescue StandardError => e
if attempt < MAX_RETRIES
delay = 2**attempt # Exponential backoff (2, 4, 8...)
sleep(delay)
retry
end
Gitlab::ErrorTracking.track_exception(e,
{ message: "Failed to fetch ghost user after #{MAX_RETRIES} attempts",
bulk_import_id: configuration.bulk_import_id }
)
nil
end
end
# @return [String, nil]
def set_ghost_user_id
return if Gitlab::Cache::Import::Caching.read(cache_key).present?
ghost_user = fetch_ghost_user
return unless ghost_user # since fetch_ghost_user returns nil if it's not in the API response
model_id = GlobalID.parse(ghost_user['id']).model_id
Gitlab::Cache::Import::Caching.write(cache_key, model_id)
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e,
{ message: "Failed to set source ghost user ID", bulk_import_id: configuration.bulk_import_id }
)
nil
end
# Returns the cached ID of the ghost user from the source instance, if it exists.
#
# @return [String, nil]
def cached_ghost_user_id
Gitlab::Cache::Import::Caching.read(cache_key)
end
private
attr_reader :configuration
def client
@client ||= BulkImports::Clients::Graphql.new(
url: configuration.url,
token: configuration.access_token
)
end
def cache_key
format(GHOST_USER_CACHE_KEY, bulk_import_id: configuration.bulk_import_id)
end
strong_memoize_attr :cache_key
end
end

View File

@ -64,6 +64,11 @@ module Gitlab
.for_issue
.for_action(%i[created closed])
design_events =
project_events_created_between(start_time, end_time, features: :issues)
.for_design
.for_action(%i[created updated destroyed])
mr_events =
project_events_created_between(start_time, end_time, features: :merge_requests)
.for_merge_request
@ -73,7 +78,7 @@ module Gitlab
project_events_created_between(start_time, end_time, features: %i[issues merge_requests])
.for_action(:commented)
[repo_events, issue_events, mr_events, project_note_events]
[repo_events, issue_events, design_events, mr_events, project_note_events]
end
def can_read_cross_project?

View File

@ -455,7 +455,11 @@ module Gitlab
end
def diffable_text?
!too_large? && diffable? && text?
!too_large? && diffable? && text? && !whitespace_only?
end
def whitespace_only?
!collapsed? && diff_lines_for_serializer.nil? && (added_lines != 0 || removed_lines != 0)
end
private

View File

@ -22,7 +22,7 @@ module Gitlab
end
def send_usage_data?
Gitlab::CurrentSettings.product_usage_data_enabled?
Gitlab::CurrentSettings.gitlab_product_usage_data_enabled?
end
def duo_event?(event_name)

View File

@ -17,13 +17,23 @@ module Import
# returned. In this case SourceUsersMapper#map returns a class that responds
# to [].
class MockedHash
def initialize(source_user_mapper)
include Gitlab::Utils::StrongMemoize
def initialize(source_user_mapper, source_ghost_user_id)
@source_user_mapper = source_user_mapper
@source_ghost_user_id = source_ghost_user_id
end
def [](user_identifier)
return ghost_user_id if @source_ghost_user_id.to_s == user_identifier.to_s
@source_user_mapper.find_source_user(user_identifier)&.mapped_user_id
end
def ghost_user_id
Users::Internal.ghost.id
end
strong_memoize_attr :ghost_user_id
end
def initialize(context:)
@ -31,7 +41,7 @@ module Import
end
def map
@map ||= MockedHash.new(source_user_mapper)
@map ||= MockedHash.new(source_user_mapper, source_ghost_user_id)
end
def include?(user_identifier)
@ -42,7 +52,7 @@ module Import
attr_reader :context
delegate :source_user_mapper, to: :context
delegate :source_user_mapper, :source_ghost_user_id, to: :context
end
end
end

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.5.2",
"@gitlab/svgs": "3.126.0",
"@gitlab/ui": "112.2.1",
"@gitlab/ui": "112.2.2",
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
"@gitlab/web-ide": "^0.0.1-dev-20250401183248",

View File

@ -18,6 +18,7 @@ Migration/UpdateLargeTable:
- :ci_job_artifacts
- :ci_job_artifacts_102
- :ci_job_variables
- :ci_pipeline_messages
- :ci_pipeline_variables
- :ci_pipeline_variables_102
- :ci_pipelines
@ -57,6 +58,10 @@ Migration/UpdateLargeTable:
- :ci_job_artifact_states
- :ci_pipeline_messages
- :namespaces
- :approval_merge_request_rules_users
- :audit_events
- :ci_job_artifact_states
- :ci_pipeline_messages
- :packages_package_files
- :personal_access_tokens
- :projects
@ -149,4 +154,4 @@ Migration/UpdateLargeTable:
DeniedMethods:
- :change_column_type_concurrently
- :rename_column_concurrently
- :update_column_in_batches
- :update_column_in_batches

View File

@ -136,6 +136,17 @@ RSpec.describe RapidDiffs::Viewers::NoPreviewComponent, type: :component, featur
expect(page).to have_text("No diff preview for this file type.")
end
end
context 'with a whitespace only diff' do
before do
allow(diff_file).to receive(:whitespace_only?).and_return(true)
end
it 'shows no preview message' do
render_component
expect(page).to have_text("Contains only whitespace changes.")
end
end
end
describe 'actions' do
@ -151,7 +162,7 @@ RSpec.describe RapidDiffs::Viewers::NoPreviewComponent, type: :component, featur
allow(diff_file).to receive(:diffable_text?).and_return(true)
end
it 'shows no preview message' do
it 'shows preview button' do
render_component
expect(page).to have_button("Show file contents")
end
@ -161,7 +172,18 @@ RSpec.describe RapidDiffs::Viewers::NoPreviewComponent, type: :component, featur
allow(diff_file).to receive(:collapsed?).and_return(true)
end
it 'shows no preview message' do
it 'shows preview button' do
render_component
expect(page).to have_button("Show changes")
end
end
context 'when diff is whitespace only' do
before do
allow(diff_file).to receive(:whitespace_only?).and_return(true)
end
it 'shows preview button' do
render_component
expect(page).to have_button("Show changes")
end

View File

@ -155,9 +155,9 @@ RSpec.describe SearchController, feature_category: :global_search do
using RSpec::Parameterized::TableSyntax
render_views
context 'when block_anonymous_global_searches is disabled' do
context 'when global_search_block_anonymous_searches_enabled is disabled' do
before do
stub_feature_flags(block_anonymous_global_searches: false)
stub_application_setting(global_search_block_anonymous_searches_enabled: false)
end
it 'omits pipeline status from load' do
@ -212,7 +212,11 @@ RSpec.describe SearchController, feature_category: :global_search do
end
end
context 'when block_anonymous_global_searches is enabled' do
context 'when global_search_block_anonymous_searches_enabled is enabled' do
before do
stub_application_setting(global_search_block_anonymous_searches_enabled: true)
end
context 'for unauthenticated user' do
before do
sign_out(user)
@ -763,7 +767,7 @@ RSpec.describe SearchController, feature_category: :global_search do
before do
stub_application_setting(restricted_visibility_levels: restricted_visibility_levels)
stub_feature_flags(allow_anonymous_searches: allow_anonymous_searches)
stub_feature_flags(block_anonymous_global_searches: block_anonymous_global_searches)
stub_application_setting(global_search_block_anonymous_searches_enabled: block_anonymous_global_searches)
end
it 'redirects to the sign in/sign up page when it should' do

View File

@ -136,7 +136,11 @@ RSpec.describe 'User searches for code', :js, :disable_rate_limiter, feature_cat
end
context 'when signed out' do
context 'when block_anonymous_global_searches is enabled' do
context 'when global_search_block_anonymous_searches_enabled is enabled' do
before do
stub_application_setting(global_search_block_anonymous_searches_enabled: true)
end
it 'is redirected to login page' do
visit(search_path)

View File

@ -131,11 +131,11 @@ RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limitin
end
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
context 'when global_search_block_anonymous_searches_enabled is disabled' do
let_it_be(:project) { create(:project, :public) }
before do
stub_feature_flags(block_anonymous_global_searches: false)
stub_application_setting(global_search_block_anonymous_searches_enabled: false)
visit(search_path)
end
@ -152,7 +152,11 @@ RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limitin
end
end
context 'when block_anonymous_global_searches is enabled' do
context 'when global_search_block_anonymous_searches_enabled is enabled' do
before do
stub_application_setting(global_search_block_anonymous_searches_enabled: true)
end
it 'is redirected to login page' do
visit(search_path)

View File

@ -6,9 +6,9 @@ RSpec.describe 'User searches for projects', :js, :disable_rate_limiter, feature
let!(:project) { create(:project, :public, name: 'Shop') }
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
context 'when global_search_block_anonymous_searches_enabled is disabled' do
before do
stub_feature_flags(block_anonymous_global_searches: false)
stub_application_setting(global_search_block_anonymous_searches_enabled: false)
end
include_examples 'top right search form'
@ -46,7 +46,11 @@ RSpec.describe 'User searches for projects', :js, :disable_rate_limiter, feature
end
end
context 'when block_anonymous_global_searches is enabled' do
context 'when global_search_block_anonymous_searches_enabled is enabled' do
before do
stub_application_setting(global_search_block_anonymous_searches_enabled: true)
end
it 'is redirected to login page' do
visit(search_path)
expect(page).to have_content('You need to sign in or sign up before continuing.')

View File

@ -71,6 +71,7 @@ describe('job group dropdown component', () => {
const findTriggerButton = () => wrapper.find('button');
const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findJobDropdownItems = () => wrapper.findAllComponents(JobDropdownItem);
const findFailedJobs = () => wrapper.find('[data-testid="failed-jobs"]');
const createComponent = ({ props, mountFn = shallowMount } = {}) => {
wrapper = mountFn(JobGroupDropdown, {
@ -207,4 +208,45 @@ describe('job group dropdown component', () => {
},
);
});
describe('failed jobs', () => {
it('shows failed jobs grouped if there are any', () => {
const failedJob = {
id: 5000,
name: 'rspec:linux 1/3',
status: {
icon: 'status_failed',
text: 'failed',
label: 'failed',
tooltip: 'failed',
group: 'failed',
},
};
createComponent({
props: {
group: {
...group,
status: {
status: 'failed',
tooltip: 'Failed - (stuck or timeout failure) (allowed to fail)',
text: 'Failed text',
},
jobs: [...group.jobs, failedJob],
},
},
mountFn: mount,
});
expect(findFailedJobs().exists()).toBe(true);
expect(findFailedJobs().text()).toContain('Failed jobs');
expect(findFailedJobs().text()).toContain('rspec:linux 1/3');
});
it('does not show failed jobs if there aren`t any', () => {
createComponent();
expect(findFailedJobs().exists()).toBe(false);
});
});
});

View File

@ -0,0 +1,72 @@
import initProductUsageData from '~/pages/admin/application_settings/metrics_and_profiling/product_usage_data';
import { ELEMENT_IDS } from '~/pages/admin/application_settings/metrics_and_profiling/constants';
describe('Product Usage Data', () => {
beforeEach(() => {
document.body.innerHTML = `
<input type="checkbox" id="${ELEMENT_IDS.PRODUCT_USAGE_DATA}" />
<input type="checkbox" id="${ELEMENT_IDS.SNOWPLOW_ENABLED}" />
<div id="js-snowplow-settings" style="display: none;"></div>
`;
});
describe('initProductUsageData Functionality', () => {
it('should hide snowplow settings when snowplow is not checked', () => {
initProductUsageData();
const snowplowSettings = document.getElementById('js-snowplow-settings');
expect(snowplowSettings.style.display).toBe('none');
});
it('should show snowplow settings when snowplow is checked', () => {
const snowplowCheckbox = document.getElementById(ELEMENT_IDS.SNOWPLOW_ENABLED);
snowplowCheckbox.checked = true;
initProductUsageData();
const snowplowSettings = document.getElementById('js-snowplow-settings');
expect(snowplowSettings.style.display).toBe('block');
});
it('should uncheck snowplow when product usage is checked', () => {
initProductUsageData();
const productUsageCheckbox = document.getElementById(ELEMENT_IDS.PRODUCT_USAGE_DATA);
const snowplowCheckbox = document.getElementById(ELEMENT_IDS.SNOWPLOW_ENABLED);
snowplowCheckbox.checked = true;
productUsageCheckbox.checked = true;
productUsageCheckbox.dispatchEvent(new Event('change'));
expect(snowplowCheckbox.checked).toBe(false);
});
it('should uncheck product usage when snowplow is checked', () => {
initProductUsageData();
const productUsageCheckbox = document.getElementById(ELEMENT_IDS.PRODUCT_USAGE_DATA);
const snowplowCheckbox = document.getElementById(ELEMENT_IDS.SNOWPLOW_ENABLED);
productUsageCheckbox.checked = true;
snowplowCheckbox.checked = true;
snowplowCheckbox.dispatchEvent(new Event('change'));
expect(productUsageCheckbox.checked).toBe(false);
});
it('should toggle snowplow settings visibility when snowplow checkbox changes', () => {
initProductUsageData();
const snowplowCheckbox = document.getElementById(ELEMENT_IDS.SNOWPLOW_ENABLED);
const snowplowSettings = document.getElementById('js-snowplow-settings');
snowplowCheckbox.checked = true;
snowplowCheckbox.dispatchEvent(new Event('change'));
expect(snowplowSettings.style.display).toBe('block');
snowplowCheckbox.checked = false;
snowplowCheckbox.dispatchEvent(new Event('change'));
expect(snowplowSettings.style.display).toBe('none');
});
});
});

View File

@ -30,6 +30,7 @@ describe('WorkItemAddRelationshipForm', () => {
workItemType = 'Objective',
childrenIds = [],
linkedWorkItemsMutationHandler = linkedWorkItemsSuccessMutationHandler,
hasBlockedWorkItemsFeature = true,
} = {}) => {
const mockApolloProvider = createMockApollo([
[addLinkedItemsMutation, linkedWorkItemsMutationHandler],
@ -43,6 +44,7 @@ describe('WorkItemAddRelationshipForm', () => {
workItemFullPath: 'test-project-path',
workItemType,
childrenIds,
hasBlockedWorkItemsFeature,
},
});
@ -71,6 +73,13 @@ describe('WorkItemAddRelationshipForm', () => {
expect(findMaxWorkItemNote().text()).toBe('Add up to 10 items at a time.');
});
it('does not render relationship type radio options when hasBlockedWorkItemsFeature is false', async () => {
await createComponent({ hasBlockedWorkItemsFeature: false });
expect(findLinkWorkItemForm().exists()).toBe(true);
expect(findRadioGroup().exists()).toBe(false);
});
it('renders work item token input with default props', () => {
expect(findWorkItemTokenInput().props()).toMatchObject({
value: [],

View File

@ -52,6 +52,7 @@ describe('WorkItemRelationships', () => {
workItemLinkedItemsHandler = workItemLinkedItemsSuccessHandler,
removeLinkedWorkItemMutationHandler = removeLinkedWorkItemSuccessMutationHandler,
canAdminWorkItemLink = true,
hasBlockedWorkItemsFeature = true,
} = {}) => {
const mockApollo = createMockApollo([
[workItemLinkedItemsQuery, workItemLinkedItemsHandler],
@ -66,6 +67,7 @@ describe('WorkItemRelationships', () => {
workItemFullPath: 'gitlab-org/gitlab-test',
canAdminWorkItemLink,
workItemType,
hasBlockedWorkItemsFeature,
},
mocks: {
$toast,
@ -118,6 +120,23 @@ describe('WorkItemRelationships', () => {
expect(findWorkItemRelationshipForm().exists()).toBe(false);
});
it.each`
hasBlockedWorkItemsFeature | emptyStateMessage
${true} | ${"Link items together to show that they're related or that one is blocking others."}
${false} | ${"Link items together to show that they're related."}
`(
'renders the component with correct empty state message when hasBlockedWorkItemsFeature is $hasBlockedWorkItemsFeature',
async ({ hasBlockedWorkItemsFeature, emptyStateMessage }) => {
await createComponent({
workItemLinkedItemsHandler: jest.fn().mockResolvedValue(workItemEmptyLinkedItemsResponse),
hasBlockedWorkItemsFeature,
});
expect(findEmptyRelatedMessageContainer().exists()).toBe(true);
expect(findEmptyRelatedMessageContainer().text()).toBe(emptyStateMessage);
},
);
it('renders blocking, blocked by and related to linked item lists with proper count', async () => {
await createComponent();
@ -148,6 +167,7 @@ describe('WorkItemRelationships', () => {
await findAddButton().vm.$emit('click');
expect(findWorkItemRelationshipForm().exists()).toBe(true);
expect(findWorkItemRelationshipForm().props('hasBlockedWorkItemsFeature')).toBe(true);
await findWorkItemRelationshipForm().vm.$emit('cancel');
expect(findWorkItemRelationshipForm().exists()).toBe(false);

View File

@ -51,6 +51,11 @@ RSpec.describe ApplicationSettingsHelper, feature_category: :shared do
.to include(*%i[snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id])
end
it 'contains product usage data setting' do
expect(helper.visible_attributes)
.to include(:gitlab_product_usage_data_enabled)
end
it 'contains :resource_usage_limits' do
expect(helper.visible_attributes).to include(:resource_usage_limits)
end
@ -387,16 +392,18 @@ RSpec.describe ApplicationSettingsHelper, feature_category: :shared do
application_setting.global_search_merge_requests_enabled = false
application_setting.global_search_users_enabled = false
application_setting.global_search_snippet_titles_enabled = true
application_setting.global_search_block_anonymous_searches_enabled = true
helper.instance_variable_set(:@application_setting, application_setting)
end
it 'returns correctly checked checkboxes' do
helper.gitlab_ui_form_for(application_setting, url: search_admin_application_settings_path) do |form|
result = helper.global_search_settings_checkboxes(form)
expect(result[0]).to have_checked_field('Enable issues tab in global search results', with: 1)
expect(result[1]).not_to have_checked_field('Enable merge requests tab in global search results', with: 1)
expect(result[2]).to have_checked_field('Enable snippet tab in global search results', with: 1)
expect(result[3]).not_to have_checked_field('Enable users tab in global search results', with: 1)
expect(result[0]).to have_checked_field('Enable blocking of anonymous global search requests', with: 1)
expect(result[1]).to have_checked_field('Enable issues tab in global search results', with: 1)
expect(result[2]).not_to have_checked_field('Enable merge requests tab in global search results', with: 1)
expect(result[3]).to have_checked_field('Enable snippet tab in global search results', with: 1)
expect(result[4]).not_to have_checked_field('Enable users tab in global search results', with: 1)
end
end
end

View File

@ -55,6 +55,26 @@ RSpec.describe '1_settings', feature_category: :shared do
end
end
describe 'initial_gitlab_product_usage_data' do
it 'is enabled by default' do
Settings.gitlab['initial_gitlab_product_usage_data'] = nil
load_settings
expect(Settings.gitlab.initial_gitlab_product_usage_data).to be(true)
end
context 'when explicitly set' do
before do
Settings.gitlab['initial_gitlab_product_usage_data'] = false
load_settings
end
it 'uses the configured value' do
expect(Settings.gitlab.initial_gitlab_product_usage_data).to be(false)
end
end
end
describe 'cell configuration' do
let(:config) do
{

View File

@ -3,10 +3,11 @@
require 'spec_helper'
RSpec.describe ::API::Entities::Project do
let(:project) { create(:project, :public) }
let(:current_user) { create(:user) }
let(:options) { { current_user: current_user } }
let_it_be(:current_user) { create(:user) }
let_it_be(:parent_group) { create(:group, :allow_runner_registration_token) }
let_it_be(:project) { create(:project, :public, group: parent_group) }
let(:options) { { current_user: current_user } }
let(:entity) do
described_class.new(project, options)
end
@ -14,7 +15,7 @@ RSpec.describe ::API::Entities::Project do
subject(:json) { entity.as_json }
context 'without project feature' do
before do
before_all do
project.project_feature.destroy!
project.reload
end
@ -27,12 +28,14 @@ RSpec.describe ::API::Entities::Project do
end
describe '.service_desk_address', feature_category: :service_desk do
let_it_be(:project) { create(:project, :public) }
before do
allow(::ServiceDesk).to receive(:enabled?).and_return(true)
allow(::ServiceDesk).to receive(:enabled?).with(project).and_return(true)
end
context 'when a user can admin issues' do
before do
before_all do
project.add_reporter(current_user)
end
@ -49,9 +52,9 @@ RSpec.describe ::API::Entities::Project do
end
describe '.shared_with_groups' do
let(:group) { create(:group, :private) }
let_it_be(:group) { create(:group, :private) }
before do
before_all do
project.project_group_links.create!(group: group)
end
@ -62,7 +65,7 @@ RSpec.describe ::API::Entities::Project do
end
context 'when the current user has access to the group' do
before do
before_all do
group.add_guest(current_user)
end
@ -74,7 +77,7 @@ RSpec.describe ::API::Entities::Project do
describe '.ci/cd settings' do
context 'when the user is not an admin' do
before do
before_all do
project.add_reporter(current_user)
end
@ -84,7 +87,7 @@ RSpec.describe ::API::Entities::Project do
end
context 'when the user has admin privileges' do
before do
before_all do
project.add_maintainer(current_user)
end
@ -93,4 +96,26 @@ RSpec.describe ::API::Entities::Project do
end
end
end
describe 'runner token settings', feature_category: :runner do
context 'when the user is not an admin' do
before_all do
project.add_reporter(current_user)
end
it 'does not return runner token settings' do
expect(json[:runners_token]).to be_nil
end
end
context 'when the user has admin privileges' do
before_all do
project.add_maintainer(current_user)
end
it 'returns runner token settings' do
expect(json[:runners_token]).to be_present
end
end
end
end

View File

@ -179,6 +179,19 @@ RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
expect(Import::SourceUser.pluck(:source_user_identifier)).to match_array(%w[101 102 103 104 105 106])
end
end
context 'when relation hash includes attributes from source ghost user' do
before do
allow(context).to receive(:source_ghost_user_id).and_return('107')
end
it 'skips creating source user and placeholder user for ghost user references' do
expect { subject.deep_transform_relation!(relation_hash, 'test', relation_definition) { |a, _b| a } }
.to change { Import::SourceUser.count }.by(4).and change { User.count }.by(4)
expect(Import::SourceUser.pluck(:source_user_identifier)).not_to include('107')
end
end
end
context 'when subrelations is an array' do

View File

@ -110,4 +110,26 @@ RSpec.describe BulkImports::Pipeline::Context, feature_category: :importers do
it { is_expected.to eq(true) }
end
end
describe '#source_ghost_user_id' do
let(:source_internal_user_finder) { instance_double(BulkImports::SourceInternalUserFinder) }
before do
allow(BulkImports::SourceInternalUserFinder).to receive(:new)
.with(bulk_import.configuration)
.and_return(source_internal_user_finder)
end
it 'returns the ghost user ID' do
expect(source_internal_user_finder).to receive(:cached_ghost_user_id).and_return('10')
expect(subject.source_ghost_user_id).to eq('10')
end
it 'memoizes the result' do
expect(source_internal_user_finder).to receive(:cached_ghost_user_id).once.and_return('10')
2.times { subject.source_ghost_user_id }
end
end
end

View File

@ -0,0 +1,175 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::SourceInternalUserFinder, feature_category: :importers do
let(:url) { 'https://gitlab.example.com' }
let(:token) { 'token' }
let(:bulk_import) { create(:bulk_import, :started, configuration: configuration) }
let(:configuration) { build(:bulk_import_configuration, url: url, access_token: token) }
let(:client) { instance_double(BulkImports::Clients::Graphql) }
let(:service) { described_class.new(configuration) }
let(:cache_key) { format(described_class::GHOST_USER_CACHE_KEY, bulk_import_id: bulk_import.id) }
before do
allow(BulkImports::Clients::Graphql).to receive(:new).with(url: url, token: token).and_return(client)
end
describe '#fetch_ghost_user' do
let(:query) do
<<~GRAPHQL
{
users(usernames: ["ghost", "ghost1", "ghost2", "ghost3", "ghost4", "ghost5", "ghost6"], humans: false) {
nodes {
id
username
type
}
}
}
GRAPHQL
end
let(:users) do
[
{ 'id' => 'gid://gitlab/User/210', 'username' => 'ghost1', 'type' => 'GHOST' }
]
end
context 'when ghost user is found' do
let(:response) { { 'data' => { 'users' => { 'nodes' => users } } } }
it 'returns the ghost user from the GraphQL response' do
expect(client).to receive(:execute).with(query: query).and_return(response)
result = service.fetch_ghost_user
expect(result).to eq(users[0])
end
end
context 'when no ghost user is found' do
let(:response) { { 'data' => { 'users' => { 'nodes' => [] } } } }
it 'returns nil' do
expect(client).to receive(:execute).with(query: query).and_return(response)
result = service.fetch_ghost_user
expect(result).to be_nil
end
end
context 'when nodes is nil' do
let(:response) { { 'data' => { 'users' => { 'nodes' => nil } } } }
it 'returns nil' do
expect(client).to receive(:execute).with(query: query).and_return(response)
result = service.fetch_ghost_user
expect(result).to be_nil
end
end
context 'when the response structure is unexpected' do
let(:response) { { 'data' => {} } }
it 'returns nil' do
expect(client).to receive(:execute).with(query: query).and_return(response)
result = service.fetch_ghost_user
expect(result).to be_nil
end
end
context 'when API call fails' do
let(:error) { StandardError.new('API error') }
let(:response) { { 'data' => { 'users' => { 'nodes' => users } } } }
it 'retries the API call with exponential backoff' do
# First attempt fails
expect(client).to receive(:execute).with(query: query).and_raise(error).ordered
# Second attempt fails
expect(service).to receive(:sleep).with(2) # Exponential backoff 2^1
expect(client).to receive(:execute).with(query: query).and_raise(error).ordered
# Third attempt succeeds
expect(service).to receive(:sleep).with(4).ordered # Exponential backoff 2^2
expect(client).to receive(:execute).with(query: query).and_return(response).ordered
result = service.fetch_ghost_user
expect(result).to eq(users[0])
end
it 'gives up after MAX_RETRIES attempts' do
expect(service).to receive(:sleep).exactly(described_class::MAX_RETRIES - 1).times
expect(client).to receive(:execute).exactly(described_class::MAX_RETRIES).times.with(query: query)
.and_raise(error)
allow(Gitlab::ErrorTracking).to receive(:track_exception).once
result = service.fetch_ghost_user
expect(result).to be_nil
end
end
end
describe '#set_ghost_user_id' do
let(:ghost_user) { { 'id' => 'gid://gitlab/User/210', 'username' => 'ghost', 'type' => 'GHOST' } }
context 'when ghost user is found' do
it 'extracts the ID and caches it' do
expect(service).to receive(:fetch_ghost_user).and_return(ghost_user)
expect(Gitlab::Cache::Import::Caching).to receive(:write).with(cache_key, '210')
service.set_ghost_user_id
end
end
context 'when ghost user is not found' do
it 'returns nil without caching' do
expect(service).to receive(:fetch_ghost_user).and_return(nil)
expect(Gitlab::Cache::Import::Caching).not_to receive(:write)
result = service.set_ghost_user_id
expect(result).to be_nil
end
end
context 'when an error occurs' do
let(:error) { StandardError.new('Error setting ghost user ID') }
it 'tracks the exception and returns nil' do
expect(service).to receive(:fetch_ghost_user).and_raise(error)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
error,
{ message: "Failed to set source ghost user ID", bulk_import_id: bulk_import.id }
)
result = service.set_ghost_user_id
expect(result).to be_nil
end
end
end
describe '#cached_ghost_user_id' do
it 'returns the cached ghost user ID' do
expect(Gitlab::Cache::Import::Caching).to receive(:read).with(cache_key).and_return('210')
expect(service.cached_ghost_user_id).to eq('210')
end
it 'returns nil when no cached value exists' do
expect(Gitlab::Cache::Import::Caching).to receive(:read).with(cache_key).and_return(nil)
expect(service.cached_ghost_user_id).to be_nil
end
end
end

View File

@ -128,6 +128,14 @@ RSpec.describe Gitlab::ContributionsCalendar, feature_category: :user_profile do
expect(calendar(contributor).activity_dates[today]).to eq(4)
end
it "counts design events" do
create_event(public_project, today, 0, :created, :design)
create_event(public_project, today, 1, :updated, :design)
create_event(public_project, today, 2, :destroyed, :design)
expect(calendar(contributor).activity_dates[today]).to eq(3)
end
context "when events fall under different dates depending on the system time zone" do
before do
create_event(public_project, today, 1)

View File

@ -1304,6 +1304,18 @@ RSpec.describe Gitlab::Diff::File, feature_category: :shared do
end
end
describe '#whitespace_only?' do
subject(:whitespace_only?) { diff_file.whitespace_only? }
it 'returns true for non-collapsed empty diffs' do
allow(diff_file).to receive(:collapsed?).and_return(false)
allow(diff_file).to receive(:diff_lines_for_serializer).and_return(nil)
allow(diff_file).to receive(:added_lines).and_return(2)
allow(diff_file).to receive(:removed_lines).and_return(2)
expect(whitespace_only?).to eq(true)
end
end
describe '#modified_file?' do
subject(:modified_file?) { diff_file.modified_file? }

View File

@ -10,23 +10,34 @@ RSpec.describe Gitlab::Tracking::EventEligibilityChecker, feature_category: :ser
subject { checker.eligible?(event_name) }
where(:event_name, :product_usage_data_enabled, :snowplow_enabled, :result) do
'perform_completion_worker' | true | false | true
'perform_completion_worker' | false | false | true
'some_other_event' | true | false | true
'some_other_event' | false | true | true
'some_other_event' | false | false | false
context 'when fully eligible due to produce usage data' do
let(:event_name) { 'perform_completion_worker' }
before do
create(:application_setting, snowplow_enabled: false, gitlab_product_usage_data_enabled: true)
end
it { is_expected.to be(true) }
end
before do
allow(Gitlab::CurrentSettings).to receive_messages(
snowplow_enabled?: snowplow_enabled,
product_usage_data_enabled?: product_usage_data_enabled
)
end
context 'for all permutations' do
where(:event_name, :product_usage_data_enabled, :snowplow_enabled, :result) do
'perform_completion_worker' | true | false | true
'perform_completion_worker' | false | false | true
'some_other_event' | true | false | true
'some_other_event' | false | true | true
'some_other_event' | false | false | false
end
with_them do
it { is_expected.to eq(result) }
before do
stub_application_setting(
snowplow_enabled?: snowplow_enabled, gitlab_product_usage_data_enabled?: product_usage_data_enabled
)
end
with_them do
it { is_expected.to eq(result) }
end
end
context 'when collect_product_usage_events feature flag is disabled' do
@ -40,9 +51,8 @@ RSpec.describe Gitlab::Tracking::EventEligibilityChecker, feature_category: :ser
before do
stub_feature_flags(collect_product_usage_events: false)
allow(Gitlab::CurrentSettings).to receive_messages(
snowplow_enabled?: snowplow_enabled,
product_usage_data_enabled?: product_usage_data_enabled
stub_application_setting(
snowplow_enabled?: snowplow_enabled, gitlab_product_usage_data_enabled?: product_usage_data_enabled
)
end

View File

@ -40,6 +40,10 @@ RSpec.describe Import::BulkImports::SourceUsersMapper, feature_category: :import
subject(:mapper) { described_class.new(context: context) }
before do
allow(context).to receive(:source_ghost_user_id).and_return('')
end
describe '#map' do
it 'returns placeholder user id' do
expect(mapper.map['101']).to eq(import_source_user_1.placeholder_user_id)
@ -56,6 +60,17 @@ RSpec.describe Import::BulkImports::SourceUsersMapper, feature_category: :import
expect(mapper.map['-1']).to eq(nil)
end
end
context 'when source user is a ghost user' do
before do
allow(context).to receive(:source_ghost_user_id).and_return('10')
end
it 'returns the destination ghost user ID' do
expect(mapper.map['10']).to eq(Users::Internal.ghost.id)
expect(mapper.map[10]).to eq(Users::Internal.ghost.id)
end
end
end
describe '#include?' do
@ -70,5 +85,34 @@ RSpec.describe Import::BulkImports::SourceUsersMapper, feature_category: :import
expect(mapper.include?(-1)).to eq(false)
end
end
context 'when a source_user_identifier matches the ghost user ID from source instance' do
before do
allow(context).to receive(:source_ghost_user_id).and_return('10')
end
it 'returns true for the source ghost user ID without creating a new source user' do
expect(mapper.include?(10)).to eq(true)
expect(Import::SourceUser.find_by(source_user_identifier: 10)).to be_nil
end
end
end
describe Import::BulkImports::SourceUsersMapper::MockedHash do
let(:source_user_mapper) { instance_double(Import::BulkImports::SourceUsersMapper) }
let(:source_ghost_user_id) { '123' }
let(:mocked_hash) { described_class.new(source_user_mapper, source_ghost_user_id) }
describe '#ghost_user_id' do
it 'returns the ghost user ID' do
expect(mocked_hash.ghost_user_id).to eq(Users::Internal.ghost.id)
end
it 'memoizes the ghost user ID' do
expect(Users::Internal).to receive(:ghost).once.and_call_original
2.times { mocked_hash.ghost_user_id }
end
end
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe EnsureGitlabProductUsageDataEnabledInServicePingSettings, feature_category: :database do
let(:application_settings_table) { table(:application_settings) }
describe '#up' do
context 'when there are no application settings' do
it 'does not raise an error' do
expect { migrate! }.not_to raise_error
end
end
context 'when snowplow_enabled is TRUE' do
let!(:application_settings) do
application_settings_table.create!(snowplow_enabled: true)
end
it 'sets gitlab_product_usage_data_enabled to FALSE' do
expect { migrate! }
.to change { application_settings.reload.service_ping_settings }
.from({})
.to({ 'gitlab_product_usage_data_enabled' => false })
end
context 'and service_ping_settings has existing values' do
before do
application_settings_table.update!(
service_ping_settings: { 'existing_setting' => 'value' }
)
end
it 'preserves existing settings and sets gitlab_product_usage_data_enabled to FALSE' do
expect { migrate! }
.to change { application_settings.reload.service_ping_settings }
.from({ 'existing_setting' => 'value' })
.to({ 'existing_setting' => 'value', 'gitlab_product_usage_data_enabled' => false })
end
end
end
context 'when snowplow_enabled is FALSE' do
let!(:application_settings) do
application_settings_table.create!(snowplow_enabled: false)
end
it 'sets gitlab_product_usage_data_enabled to TRUE' do
expect { migrate! }
.to change { application_settings.reload.service_ping_settings }
.from({})
.to({ 'gitlab_product_usage_data_enabled' => true })
end
context 'and service_ping_settings has existing values' do
before do
application_settings_table.update!(
service_ping_settings: { 'existing_setting' => 'value' }
)
end
it 'preserves existing settings and sets gitlab_product_usage_data_enabled to TRUE' do
expect { migrate! }
.to change { application_settings.reload.service_ping_settings }
.from({ 'existing_setting' => 'value' })
.to({ 'existing_setting' => 'value', 'gitlab_product_usage_data_enabled' => true })
end
end
end
context 'when working with default snowplow_enabled value' do
let!(:application_settings) do
application_settings_table.create!(snowplow_enabled: false)
end
it 'sets gitlab_product_usage_data_enabled to TRUE' do
expect { migrate! }
.to change { application_settings.reload.service_ping_settings }
.from({})
.to({ 'gitlab_product_usage_data_enabled' => true })
end
end
context 'with multiple application settings records' do
let!(:application_settings1) do
application_settings_table.create!(snowplow_enabled: true)
end
let!(:application_settings2) do
application_settings_table.create!(snowplow_enabled: false)
end
it 'updates all records according to their snowplow_enabled value' do
migrate!
expect(application_settings1.reload.service_ping_settings)
.to eq({ 'gitlab_product_usage_data_enabled' => false })
expect(application_settings2.reload.service_ping_settings)
.to eq({ 'gitlab_product_usage_data_enabled' => true })
end
end
end
describe '#down' do
it 'is a no-op' do
application_settings_table.create!(
snowplow_enabled: true,
service_ping_settings: { 'gitlab_product_usage_data_enabled' => false }
)
schema_migrate_down!
expect(application_settings_table.first.service_ping_settings)
.to eq({ 'gitlab_product_usage_data_enabled' => false })
end
end
end

View File

@ -1499,6 +1499,100 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
end
end
describe 'snowplow_and_product_usage_data_are_mutually_exclusive validation' do
context 'when both snowplow and product usage data tracking are enabled' do
before do
setting.snowplow_enabled = true
setting.gitlab_product_usage_data_enabled = true
end
it 'is invalid' do
expect(setting).to be_invalid
expect(setting.errors[:base]).to include(
/Snowplow tracking and Product event tracking cannot be enabled at the same time/
)
end
end
context 'when only snowplow tracking is enabled' do
before do
setting.snowplow_enabled = true
setting.gitlab_product_usage_data_enabled = false
end
it 'is valid' do
expect(setting).to be_valid
end
end
context 'when only product usage data tracking is enabled' do
before do
setting.snowplow_enabled = false
setting.gitlab_product_usage_data_enabled = true
end
it 'is valid' do
expect(setting).to be_valid
end
end
context 'when neither snowplow nor product usage data tracking is enabled' do
before do
setting.snowplow_enabled = false
setting.gitlab_product_usage_data_enabled = false
end
it 'is valid' do
expect(setting).to be_valid
end
end
context 'when changing snowplow_enabled' do
before do
setting.gitlab_product_usage_data_enabled = true
end
it 'is invalid when enabling snowplow while product usage data is enabled' do
setting.snowplow_enabled = true
expect(setting).to be_invalid
expect(setting.errors[:base]).to include(
/Snowplow tracking and Product event tracking cannot be enabled at the same time/
)
end
end
context 'when changing gitlab_product_usage_data_enabled' do
before do
setting.snowplow_enabled = true
end
it 'is invalid when enabling product usage data while snowplow is enabled' do
setting.gitlab_product_usage_data_enabled = true
expect(setting).to be_invalid
expect(setting.errors[:base]).to include(
/Snowplow tracking and Product event tracking cannot be enabled at the same time/
)
end
end
context 'when changing an unrelated attribute' do
before do
setting.snowplow_enabled = true
setting.gitlab_product_usage_data_enabled = true
setting.save!(validate: false)
end
it 'skips the validation and allows saving' do
setting.home_page_url = 'https://example.com'
expect(setting.save).to be true
expect(setting.reload.home_page_url).to eq('https://example.com')
end
end
end
describe '#runners_registration_token' do
context 'when allowed by application setting' do
before do

View File

@ -11,6 +11,10 @@ RSpec.describe Ci::InstanceVariable do
it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
it { is_expected.to validate_length_of(:value).is_at_most(10_000).with_message(/The value of the provided variable exceeds the 10000 character limit/) }
it_behaves_like 'encrypted attribute', :value, :db_key_base_32 do
let(:record) { subject }
end
it_behaves_like 'includes Limitable concern' do
subject { build(:ci_instance_variable) }
end

View File

@ -13,6 +13,10 @@ RSpec.describe Ci::JobVariable, feature_category: :continuous_integration do
it { is_expected.to validate_presence_of(:project_id) }
end
it_behaves_like 'encrypted attribute', :value, :db_key_base_32 do
let(:record) { create(:ci_job_variable) }
end
describe 'partitioning' do
let(:job_variable) { build(:ci_job_variable, job: ci_build) }

View File

@ -31,6 +31,10 @@ RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
end
end
it_behaves_like 'encrypted attribute', :encrypted_token_tmp, :db_key_base_32 do
let(:record) { create(:ci_trigger_without_token) }
end
describe '#last_used' do
let_it_be(:project) { create :project }
let_it_be_with_refind(:trigger) { create(:ci_trigger, project: project) }

View File

@ -11,6 +11,10 @@ RSpec.describe Ci::HasVariable, feature_category: :continuous_integration do
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
it_behaves_like 'encrypted attribute', :value, :db_key_base do
let(:record) { subject }
end
describe 'scopes' do
describe '.by_key' do
let!(:matching_variable) { create(:ci_variable, key: 'example') }

View File

@ -191,14 +191,12 @@ RSpec.describe Event, feature_category: :user_profile do
let!(:push_event) { create_push_event(project, project.owner) }
let!(:comment_event) { create(:event, :commented, project: project) }
before do
create(:design_event, project: project) # should not be in scope
end
let!(:design_event) { create(:design_event, project: project) }
it 'returns events for MergeRequest, Issue, WorkItem and push, comment events' do
expect(described_class.contributions).to contain_exactly(
*merge_request_events, *issue_events, work_item_event,
push_event, comment_event
push_event, comment_event, design_event
)
end
end

View File

@ -399,6 +399,12 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
expect(personal_access_token).to be_valid
end
it 'is valid with expiration date set beyond 400 days' do
personal_access_token.expires_at = 2.years.from_now
expect(personal_access_token).to be_valid
end
end
end

View File

@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe ApplicationSettingPolicy do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let_it_be(:current_user) { create(:user) }
let_it_be(:user) { create(:user) }
subject { described_class.new(current_user, [user]) }
describe 'update_runners_registration_token' do
describe 'runner registration token settings' do
let(:allow_runner_registration_token) { true }
before do
@ -18,33 +18,39 @@ RSpec.describe ApplicationSettingPolicy do
context 'when anonymous' do
let(:current_user) { nil }
it { is_expected.not_to be_allowed(:read_runners_registration_token) }
it { is_expected.not_to be_allowed(:update_runners_registration_token) }
end
context 'regular user' do
it { is_expected.not_to be_allowed(:read_runners_registration_token) }
it { is_expected.not_to be_allowed(:update_runners_registration_token) }
end
context 'when external' do
let(:current_user) { build(:user, :external) }
let_it_be(:current_user) { build(:user, :external) }
it { is_expected.not_to be_allowed(:read_runners_registration_token) }
it { is_expected.not_to be_allowed(:update_runners_registration_token) }
end
context 'admin' do
let(:current_user) { create(:admin) }
let_it_be(:current_user) { create(:admin) }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_runners_registration_token) }
it { is_expected.to be_allowed(:update_runners_registration_token) }
context 'with registration tokens disabled' do
let(:allow_runner_registration_token) { false }
it { is_expected.to be_disallowed(:read_runners_registration_token) }
it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:read_runners_registration_token) }
it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
end

Some files were not shown because too many files have changed in this diff Show More