Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e8d15002ba
commit
ed79d7cc5b
|
|
@ -1 +1 @@
|
|||
7e94489bc9d892e3cb25f9f9e7f4f7ce15ac0ee8
|
||||
d19af9f22edb454a615b9bb90fdcaa24f72eb488
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (?))',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 * * * *'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ milestone: '13.0'
|
|||
gitlab_schema: gitlab_ci
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: small
|
||||
table_size: medium
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ milestone: '13.2'
|
|||
gitlab_schema: gitlab_ci
|
||||
sharding_key:
|
||||
project_id: projects
|
||||
table_size: large
|
||||
table_size: over_limit
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -11,4 +11,4 @@ milestone: '17.6'
|
|||
gitlab_schema: gitlab_ci
|
||||
sharding_key:
|
||||
sharding_key_id: namespaces
|
||||
table_size: small
|
||||
table_size: medium
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
dc1fbc25f53ef725eb4b05220486d5452c9377699bd914ee55c567dfab6674b0
|
||||
|
|
@ -0,0 +1 @@
|
|||
a3b4525eea40fde40c92803d5368f4ada21abe79d5de01b5e5507b2e108fc197
|
||||
|
|
@ -0,0 +1 @@
|
|||
ff0832e10754451488a921cfb0bfe68390448c1cd4c53d5175d6a378c418a6ce
|
||||
|
|
@ -0,0 +1 @@
|
|||
b506cb61c1b71884da28f9e10c9ba164f9f76de8bc608019235cd1a85a38bea2
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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: ''
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 >}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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? }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue