Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-18 18:07:22 +00:00
parent e9c877121f
commit c13c8ff01f
64 changed files with 597 additions and 631 deletions

View File

@ -343,44 +343,6 @@
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.9
alias: zoekt-ci-image
.ai-gateway-variables:
variables:
AIGW_AUTH__BYPASS_EXTERNAL: true
AIGW_GOOGLE_CLOUD_PLATFORM__PROJECT: $VERTEX_AI_PROJECT
AIGW_GOOGLE_CLOUD_PLATFORM__SERVICE_ACCOUNT_JSON_KEY: $VERTEX_AI_CREDENTIALS
AIGW_FASTAPI__DOCS_URL: "/docs"
AIGW_FASTAPI__OPENAPI_URL: "/openapi.json"
AIGW_FASTAPI__API_PORT: 5052
ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY_FOR_SERVICE
# CI_DEBUG_SERVICES: "true" # Enable this variable when you debug ai-gateway boot failure.
.ai-gateway-services:
services:
- name: registry.gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/model-gateway:v1.18.0
alias: ai-gateway
.litellm-proxy-variables:
variables:
LITELLM_CONFIG_CONTENT: |
model_list:
- model_name: codestral
litellm_params:
model: ollama/codestral
mock_response: "Mock response from codestral"
- model_name: mistral
litellm_params:
model: ollama/mistral
mock_response: "Mock response from mistral"
.litellm-proxy-services:
services:
- name: ghcr.io/berriai/litellm:main-latest
alias: litellm-proxy
entrypoint: ["/bin/sh", "-c"]
command:
- |
mkdir -p /tmp && echo "${LITELLM_CONFIG_CONTENT}" > /tmp/config.yaml && litellm --config /tmp/config.yaml
.use-pg14:
extends:
- .pg-base-variables
@ -431,13 +393,9 @@
extends:
- .use-pg14
- .zoekt-variables
- .ai-gateway-variables
- .litellm-proxy-variables
services:
- !reference [.db-services-with-auto-explain, services]
- !reference [.es7-services, services]
- !reference [.ai-gateway-services, services]
- !reference [.litellm-proxy-services, services]
.use-pg15-es7-ee:
extends:

View File

@ -895,21 +895,6 @@ rspec-ee integration pg16 single-db-sec-connection:
- .rspec-ee-integration-parallel
- .rails:rules:single-db-sec-connection-ee
.custom-models-variables:
variables:
AIGW_CUSTOM_MODELS__ENABLED: true
AI_GATEWAY_URL: http://ai-gateway:5052
LITELLM_PROXY_URL: http://litellm-proxy:4000
rspec-ee system custom-models pg16:
extends:
- .rspec-ee-base-pg16
- .rails:rules:ee-only-system
- .custom-models-variables
script:
- !reference [.base-script, script]
- rspec_parallelized_job "--tag requires_custom_models_setup"
rspec-ee system pg16:
extends:
- .rspec-ee-base-pg16
@ -1224,7 +1209,7 @@ rspec-ee system pg17:
stage: test
script:
- !reference [.base-script, script]
- rspec_section rspec_fail_fast "${MATCHING_TESTS_PATH}" "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~zoekt --tag ~click_house --tag ~real_ai_request"
- rspec_section rspec_fail_fast "${MATCHING_TESTS_PATH}" "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~zoekt --tag ~click_house"
rspec fail-fast:
extends:

View File

@ -107,7 +107,7 @@ include:
# spec/lib, yet background migration tests are also sitting there,
# and they should run on their own jobs so we don't need to run them
# in unit tests again.
- rspec_section rspec_parallelized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~level:background_migration --tag ~click_house --tag ~real_ai_request"
- rspec_section rspec_parallelized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~level:background_migration --tag ~click_house"
after_script:
- source scripts/utils.sh
- log_disk_usage # https://gitlab.com/gitlab-org/gitlab/-/issues/478880
@ -205,7 +205,7 @@ include:
.rspec-base-migration:
script:
- !reference [.base-script, script]
- rspec_section rspec_parallelized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~zoekt --tag ~click_house --tag ~real_ai_request"
- rspec_section rspec_parallelized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag ~quarantine --tag ~zoekt --tag ~click_house"
after_script:
- !reference [.rspec-base, after_script]

View File

@ -2384,36 +2384,6 @@
- <<: *if-default-refs
changes: *code-backstage-patterns
.rails:rules:ee-gitlab-duo-chat-base:
rules:
- !reference [".strict-ee-only-rules", rules]
- if: '$ANTHROPIC_API_KEY == null'
when: never
- <<: *if-fork-merge-request
when: never
.rails:rules:ee-gitlab-duo-chat-optional:
rules:
- !reference [".rails:rules:ee-gitlab-duo-chat-base", rules]
- <<: *if-merge-request
changes: *backend-patterns
when: never
allow_failure: true
.rails:rules:ee-gitlab-duo-chat-always:
rules:
- !reference [".rails:rules:ee-gitlab-duo-chat-base", rules]
- <<: *if-merge-request
changes: *ai-patterns
.rails:rules:ee-gitlab-duo-chat-qa-full:
rules:
- !reference [".rails:rules:ee-gitlab-duo-chat-optional", rules]
- <<: *if-default-branch-refs
changes: *setup-test-env-patterns
when: never
allow_failure: true
.rails:rules:db:check-schema:
rules:
- <<: *if-not-ee

View File

@ -149,7 +149,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'lib/gitlab/git/diff.rb'
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/user.rb'
- 'lib/gitlab/grape_logging/loggers/response_logger.rb'
- 'lib/gitlab/internal_events.rb'
- 'lib/gitlab/lograge/custom_options.rb'
- 'lib/gitlab/memory/reports/heap_dump.rb'

View File

@ -306,7 +306,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/spec/services/protected_environments/update_service_spec.rb'
- 'ee/spec/services/security/security_orchestration_policies/ci_action/template_spec.rb'
- 'ee/spec/services/security/token_revocation_service_spec.rb'
- 'ee/spec/support/llm.rb'
- 'ee/spec/support/shared_examples/lib/gitlab/llm/chain/slash_command_tool_shared_examples.rb'
- 'ee/spec/support/shared_examples/requests/api/graphql/ci/queueing_history_shared_examples.rb'
- 'ee/spec/support/shared_examples/requests/identity_verification_shared_examples.rb'

View File

@ -2498,7 +2498,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb'
- 'spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb'

View File

@ -1966,7 +1966,6 @@ RSpec/NamedSubject:
- 'spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb'
- 'spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
- 'spec/lib/gitlab/graphql/batch_key_spec.rb'
- 'spec/lib/gitlab/graphql/copy_field_description_spec.rb'

View File

@ -3,8 +3,6 @@
RSpec/VerifiedDoubleReference:
Exclude:
- 'ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb'
- 'ee/spec/features/custom_models/code_suggestions_spec.rb'
- 'ee/spec/features/custom_models/duo_chat_spec.rb'
- 'ee/spec/features/merge_request/user_sees_security_policy_rules_licence_compliance_spec.rb'
- 'ee/spec/features/projects/google_cloud/artifact_registry_spec.rb'
- 'ee/spec/features/projects/integrations/google_cloud_platform/user_activates_artifact_management_spec.rb'

View File

@ -1 +0,0 @@
1dbddb919823f69cb58bf36343c84c52ec4a3418

View File

@ -1,15 +1,12 @@
<script>
import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import { get } from 'lodash';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import ProjectsListEmptyState from '~/vue_shared/components/projects_list/projects_list_empty_state.vue';
import { DEFAULT_PER_PAGE } from '~/api';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import { TIMESTAMP_TYPES } from '~/vue_shared/components/resource_lists/constants';
import { FILTERED_SEARCH_TERM_KEY } from '~/projects/filtered_search_and_sort/constants';
import { ACCESS_LEVELS_INTEGER_TO_STRING } from '~/access_level/constants';
import { formatProjects } from '~/projects/your_work/utils';
import {
FILTERED_SEARCH_TOKEN_LANGUAGE,
FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL,
@ -26,8 +23,6 @@ export default {
components: {
GlLoadingIcon,
GlKeysetPagination,
ProjectsList,
ProjectsListEmptyState,
},
inject: ['programmingLanguages'],
props: {
@ -63,11 +58,11 @@ export default {
},
data() {
return {
projects: {},
items: {},
};
},
apollo: {
projects() {
items() {
return {
query: this.tab.query,
variables() {
@ -91,7 +86,7 @@ export default {
const { nodes, pageInfo } = get(response, this.tab.queryPath);
return {
nodes: formatProjects(nodes),
nodes: this.tab.formatter(nodes),
pageInfo,
};
},
@ -103,10 +98,10 @@ export default {
},
computed: {
nodes() {
return this.projects.nodes || [];
return this.items.nodes || [];
},
pageInfo() {
return this.projects.pageInfo || {};
return this.items.pageInfo || {};
},
pagination() {
if (!this.startCursor && !this.endCursor) {
@ -126,7 +121,7 @@ export default {
};
},
isLoading() {
return this.$apollo.queries.projects.loading;
return this.$apollo.queries.items.loading;
},
search() {
return this.filters[FILTERED_SEARCH_TERM_KEY];
@ -147,14 +142,24 @@ export default {
apolloClient() {
return this.$apollo.provider.defaultClient;
},
emptyState() {
return this.tab.emptyState || {};
emptyStateComponentProps() {
return {
search: this.search,
...this.tab.emptyStateComponentProps,
};
},
listComponentProps() {
return {
items: this.nodes,
timestampType: this.timestampType,
...this.tab.listComponentProps,
};
},
},
methods: {
onRefetch() {
this.apolloClient.resetStore();
this.$apollo.queries.projects.refetch();
this.$apollo.queries.items.refetch();
},
onNext(endCursor) {
this.$emit('page-change', {
@ -175,21 +180,10 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<div v-else-if="nodes.length">
<projects-list
:projects="nodes"
show-project-icon
list-item-class="gl-px-5"
:timestamp-type="timestampType"
@refetch="onRefetch"
/>
<component :is="tab.listComponent" v-bind="listComponentProps" @refetch="onRefetch" />
<div v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage" class="gl-mt-5 gl-text-center">
<gl-keyset-pagination v-bind="pageInfo" @prev="onPrev" @next="onNext" />
</div>
</div>
<projects-list-empty-state
v-else
:title="emptyState.title"
:description="emptyState.description"
:search="search"
/>
<component :is="tab.emptyStateComponent" v-else v-bind="emptyStateComponentProps" />
</template>

View File

@ -23,11 +23,7 @@ import {
FILTERED_SEARCH_TERM_KEY,
FILTERED_SEARCH_NAMESPACE,
} from '~/projects/filtered_search_and_sort/constants';
import {
CONTRIBUTED_TAB,
CUSTOM_DASHBOARD_ROUTE_NAMES,
PROJECT_DASHBOARD_TABS,
} from '~/projects/your_work/constants';
import { CUSTOM_DASHBOARD_ROUTE_NAMES } from '~/projects/your_work/constants';
import projectCountsQuery from '~/projects/your_work/graphql/queries/project_counts.query.graphql';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
@ -40,7 +36,6 @@ import TabView from './tab_view.vue';
// Will be made more generic to work with groups and projects in future commits
export default {
name: 'TabsWithList',
PROJECT_DASHBOARD_TABS,
i18n: {
projectCountError: __('An error occurred loading the project counts.'),
},
@ -58,10 +53,16 @@ export default {
FilteredSearchAndSort,
},
inject: ['initialSort', 'programmingLanguages'],
props: {
tabs: {
type: Array,
required: true,
},
},
data() {
return {
activeTabIndex: this.initActiveTabIndex(),
counts: PROJECT_DASHBOARD_TABS.reduce((accumulator, tab) => {
counts: this.tabs.reduce((accumulator, tab) => {
return {
...accumulator,
[tab.value]: undefined,
@ -207,7 +208,7 @@ export default {
initActiveTabIndex() {
return CUSTOM_DASHBOARD_ROUTE_NAMES.includes(this.$route.name)
? 0
: PROJECT_DASHBOARD_TABS.findIndex((tab) => tab.value === this.$route.name);
: this.tabs.findIndex((tab) => tab.value === this.$route.name);
},
onTabUpdate(index) {
// This return will prevent us overwriting the root `/` and `/dashboard/projects` paths
@ -216,7 +217,7 @@ export default {
this.activeTabIndex = index;
const tab = PROJECT_DASHBOARD_TABS[index] || CONTRIBUTED_TAB;
const tab = this.tabs[index] || this.tabs[0];
this.$router.push({ name: tab.value });
},
tabCount(tab) {
@ -270,7 +271,7 @@ export default {
<template>
<gl-tabs :value="activeTabIndex" @input="onTabUpdate">
<gl-tab v-for="tab in $options.PROJECT_DASHBOARD_TABS" :key="tab.text" lazy>
<gl-tab v-for="tab in tabs" :key="tab.text" lazy>
<template #title>
<div class="gl-flex gl-items-center gl-gap-2" data-testid="projects-dashboard-tab-title">
<span>{{ tab.text }}</span>

View File

@ -13,7 +13,7 @@ export default {
},
data() {
return {
dropzoneAllowList: ['.JSON'],
dropzoneAllowList: ['.json'],
};
},
i18n: {

View File

@ -32,7 +32,7 @@ export const validateImageName = (file) => {
export const validateFileFromAllowList = (fileName, allowList) => {
const parts = fileName.split('.');
const ext = `.${parts[parts.length - 1]}`;
const ext = `.${parts[parts.length - 1]}`.toLowerCase();
return allowList.includes(ext);
return allowList.map((fileExt) => fileExt.toLowerCase()).includes(ext);
};

View File

@ -30,6 +30,7 @@ export default {
data() {
return {
file: null,
showModal: false,
fileName: null,
uploadError: false,
};
@ -41,6 +42,12 @@ export default {
},
methods: {
reassignContributions() {
if (this.file === null) {
this.uploadError = s__('UserMapping|Please upload a valid CSV file.');
return;
}
this.showModal = false;
const formData = new FormData();
formData.append('file', this.file);
@ -64,6 +71,9 @@ export default {
message: s__('UserMapping|Something went wrong while uploading the CSV file.'),
});
}
})
.finally(() => {
this.clearFile();
});
},
isValidFileType(file) {
@ -78,11 +88,16 @@ export default {
this.file = file;
},
onError() {
this.clearFile();
this.uploadError = this.$options.i18n.errorMessage;
},
close() {
this.showModal = false;
this.clearError();
this.$refs[this.modalId].hide();
},
clearFile() {
this.fileName = null;
this.file = null;
},
},
dropzoneAllowList: ['.csv'],
@ -110,12 +125,13 @@ export default {
</script>
<template>
<gl-modal
:ref="modalId"
v-model="showModal"
:modal-id="modalId"
:title="s__('UserMapping|Reassign with CSV file')"
:action-primary="$options.primaryAction"
:action-cancel="$options.cancelAction"
@primary="reassignContributions"
@primary.prevent="reassignContributions"
@cancel="close"
>
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">

View File

@ -192,7 +192,7 @@ export default {
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<div v-else-if="nodes.length">
<groups-list
:groups="nodes"
:items="nodes"
show-group-icon
:list-item-class="listItemClass"
:timestamp-type="timestampType"

View File

@ -168,7 +168,7 @@ export default {
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<div v-else-if="nodes.length">
<projects-list
:projects="nodes"
:items="nodes"
show-project-icon
:list-item-class="listItemClass"
:timestamp-type="timestampType"

View File

@ -70,7 +70,7 @@ export default {
<gl-link href="">{{ $options.i18n.viewAll }}</gl-link>
</div>
<gl-loading-icon v-if="personalProjectsLoading" class="gl-mt-5" size="md" />
<projects-list v-else :projects="personalProjects" />
<projects-list v-else :items="personalProjects" />
</div>
</div>
</gl-tab>

View File

@ -1,7 +1,9 @@
<script>
import TabsWithList from '~/groups_projects/components/tabs_with_list.vue';
import { PROJECT_DASHBOARD_TABS } from '../constants';
export default {
PROJECT_DASHBOARD_TABS,
name: 'YourWorkProjectsApp',
components: {
TabsWithList,
@ -10,5 +12,5 @@ export default {
</script>
<template>
<tabs-with-list />
<tabs-with-list :tabs="$options.PROJECT_DASHBOARD_TABS" />
</template>

View File

@ -1,4 +1,7 @@
import { __, s__ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import ProjectsListEmptyState from '~/vue_shared/components/projects_list/projects_list_empty_state.vue';
import { formatProjects } from '~/projects/your_work/utils';
import projectsQuery from './graphql/queries/projects.query.graphql';
import userProjectsQuery from './graphql/queries/user_projects.query.graphql';
@ -7,13 +10,24 @@ const transformSortToUpperCase = (variables) => ({
sort: variables.sort.toUpperCase(),
});
const baseTab = {
listComponent: ProjectsList,
listComponentProps: {
listItemClass: 'gl-px-5',
showProjectIcon: true,
},
emptyStateComponent: ProjectsListEmptyState,
formatter: formatProjects,
};
export const CONTRIBUTED_TAB = {
...baseTab,
text: __('Contributed'),
value: 'contributed',
query: userProjectsQuery,
variables: { contributed: true },
queryPath: 'currentUser.contributedProjects',
emptyState: {
emptyStateComponentProps: {
title: s__("Projects|You haven't contributed to any projects yet."),
description: s__(
'Projects|Projects where you contribute code, create issues or epics, or participate in discussions will appear here.',
@ -23,12 +37,13 @@ export const CONTRIBUTED_TAB = {
};
export const STARRED_TAB = {
...baseTab,
text: __('Starred'),
value: 'starred',
query: userProjectsQuery,
variables: { starred: true },
queryPath: 'currentUser.starredProjects',
emptyState: {
emptyStateComponentProps: {
title: s__("Projects|You haven't starred any projects yet."),
description: s__(
'Projects|Visit a project and select the star icon to save projects you want to find later.',
@ -38,34 +53,37 @@ export const STARRED_TAB = {
};
export const PERSONAL_TAB = {
...baseTab,
text: __('Personal'),
value: 'personal',
query: projectsQuery,
variables: { personal: true },
queryPath: 'projects',
emptyState: {
emptyStateComponentProps: {
title: s__("Projects|You don't have any personal projects yet."),
},
};
export const MEMBER_TAB = {
...baseTab,
text: __('Member'),
value: 'member',
query: projectsQuery,
variables: { membership: true },
queryPath: 'projects',
emptyState: {
emptyStateComponentProps: {
title: s__("Projects|You aren't a member of any projects yet."),
},
};
export const INACTIVE_TAB = {
...baseTab,
text: __('Inactive'),
value: 'inactive',
query: projectsQuery,
variables: { archived: 'ONLY', membership: true },
queryPath: 'projects',
emptyState: {
emptyStateComponentProps: {
title: s__("Projects|You don't have any inactive projects."),
description: s__('Projects|Projects that are archived or pending deletion will appear here.'),
},

View File

@ -19,7 +19,7 @@ import { SCOPE_BLOB } from '~/search/sidebar/constants';
import { SYNTAX_OPTIONS_ADVANCED_DOCUMENT, SYNTAX_OPTIONS_ZOEKT_DOCUMENT } from '../constants';
import SearchTypeIndicator from './search_type_indicator.vue';
import GlSearchBoxByType from './search_box_by_type.vue';
import GlobalSearchInput from './global_search_input.vue';
const trackingMixin = InternalEvents.mixin();
@ -34,7 +34,7 @@ export default {
},
components: {
GlButton,
GlSearchBoxByType,
GlobalSearchInput,
MarkdownDrawer,
SearchTypeIndicator,
},
@ -130,7 +130,7 @@ export default {
</div>
<markdown-drawer ref="markdownDrawer" :document-path="documentBasedOnSearchType" />
</template>
<gl-search-box-by-type
<global-search-input
id="dashboard_search"
v-model="search"
name="search"

View File

@ -4,7 +4,7 @@ import { __ } from '~/locale';
import GlClearIconButton from './clear_icon_button.vue';
export default {
name: 'GlSearchBoxByType',
name: 'GlobalSearchInput',
components: {
GlClearIconButton,
GlIcon,

View File

@ -14,6 +14,6 @@ const Template = (args, { argTypes }) => ({
export const Default = Template.bind({});
Default.args = {
groups,
items: groups,
showGroupIcon: true,
};

View File

@ -6,9 +6,10 @@ import {
import GroupsListItem from './groups_list_item.vue';
export default {
name: 'GroupsList',
components: { GroupsListItem },
props: {
groups: {
items: {
type: Array,
required: true,
},
@ -37,7 +38,7 @@ export default {
<template>
<ul class="gl-list-none gl-p-0">
<groups-list-item
v-for="group in groups"
v-for="group in items"
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"

View File

@ -5,6 +5,7 @@ import {
} from '~/vue_shared/components/resource_lists/constants';
export default {
name: 'NestedGroupsProjectsList',
props: {
items: {
type: Array,

View File

@ -6,6 +6,7 @@ import {
import ProjectsListItem from './projects_list_item.vue';
export default {
name: 'ProjectsList',
components: { ProjectsListItem },
props: {
/**
@ -31,7 +32,7 @@ export default {
* createdAt: string;
* }[]
*/
projects: {
items: {
type: Array,
required: true,
},
@ -60,7 +61,7 @@ export default {
<template>
<ul class="gl-list-none gl-p-0">
<projects-list-item
v-for="project in projects"
v-for="project in items"
:key="project.id"
:project="project"
:show-project-icon="showProjectIcon"

View File

@ -181,6 +181,11 @@ export default {
this.$refs.fileUpload.click();
},
onFileInputChange(e) {
if (!this.isValidUpload(Array.from(e.target.files))) {
this.$emit('error');
return;
}
this.$emit('change', this.singleFileSelection ? e.target.files[0] : e.target.files);
},
onMouseEnter() {

View File

@ -25,7 +25,6 @@ module RequestPayloadLogger
end
payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
payload[:response_bytes] = response.body_parts.sum(&:bytesize) if Feature.enabled?(:log_response_length)
store_cloudflare_headers!(payload, request)
end

View File

@ -21,13 +21,16 @@
"type": "object",
"properties": {
"service_url": {
"type": "string"
"type": "string",
"pattern": "^(https?://)"
},
"item_url": {
"type": "string"
"type": "string",
"pattern": "^(https?://)"
},
"resource_url_template": {
"type": "string"
"type": "string",
"pattern": "^(https?://)"
}
},
"required": [

View File

@ -1,8 +0,0 @@
---
name: log_response_length
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91448
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366854
milestone: '15.3'
type: development
group: group::tenant scale
default_enabled: false

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class FixSecureflagDescription < Gitlab::Database::Migration[2.2]
milestone '17.11'
restrict_gitlab_migration gitlab_schema: :gitlab_main
def up
execute <<~SQL
UPDATE security_training_providers SET description = 'Get remediation advice with example code and recommended hands-on labs in a fully
interactive virtualized environment.'
WHERE name = 'SecureFlag'
SQL
end
def down
# No-op
end
end

View File

@ -0,0 +1 @@
40be9de397e29d705fe84dd0af966d0df7c57174838650ac60c81a5e777afa64

View File

@ -5,8 +5,14 @@ info: Any user with at least the Maintainer role can merge updates to this conte
title: AI features based on 3rd-party integrations
---
GitLab Duo features are powered by AI models and integrations. This document provides an overview of developing with AI features in GitLab.
For detailed instructions on setting up GitLab Duo licensing in your development environment, see [GitLab Duo licensing for local development](ai_development_license.md).
## Instructions for setting up GitLab Duo features in the local development environment
For complete setup instructions, see [GitLab Duo licensing for local development](ai_development_license.md).
### Required: Install AI gateway
**Why:** Duo features (except for Duo Workflow) route LLM requests through the AI gateway.
@ -23,167 +29,9 @@ You can also install AI gateway by:
We only recommend this for users who have a specific reason for *not* running
the AI gateway through GDK.
### Required: Setup Licenses in GitLab-Rails
**Why:** GitLab Duo is available to Premium and Ultimate customers only. You
likely want an Ultimate license for your GDK. Ultimate gets you access to
all GitLab Duo features.
**How:**
Follow [the process to obtain an EE license](https://handbook.gitlab.com/handbook/engineering/developer-onboarding/#working-on-gitlab-ee-developer-licenses)
for your local instance and [upload the license](../../administration/license_file.md).
To verify that the license is applied, go to **Admin area** > **Subscription**
and check the subscription plan.
### Set up and run GDK
#### Option A: in GitLab.com Mode
**Why:** Most Duo features are available on GitLab.com first, so emulating a multi-tenant environment.
**How:**
Run the Rake task to set up Duo features for a group:
```shell
GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup'
```
```shell
gdk restart
```
Replace `test-group-name` with the name of any top-level group. Duo will
be configured for that group. If the group doesn't exist, it creates a new
one.
Make sure the script succeeds. It prints error messages with links on how
to resolve any errors. You can re-run the script until it succeeds.
In multi-tennent mode, membership to a group with Duo features enabled is what enables
many AI features. Make sure that your test user is a member of the group with
Duo features enabled (`test-group-name`).
This Rake task creates Duo Enterprise add-on attached to that group.
In case you need Duo Pro add-on attached, please use:
```shell
GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[duo_pro]'
```
Duo Pro add-on serves smaller scope of features. Usage of add-on depends on what features you want to use.
This Rake task will assign Duo Add-on seat to the 'root' user.
#### Option B: in Self-managed Mode
**Why:** If you want to test something specific to self-managed setups or validate that a Duo feature
also works for our self-managed customers.
**How:**
Run the Rake task to set up Duo features for the instance:
```shell
GITLAB_SIMULATE_SAAS=0 bundle exec 'rake gitlab:duo:setup'
```
```shell
gdk restart
```
This Rake task creates Duo Enterprise add-on attached to your instance.
In case you need Duo Pro add-on attached, please use:
```shell
GITLAB_SIMULATE_SAAS=0 bundle exec 'rake gitlab:duo:setup[duo_pro]'
```
Duo Pro add-on serves smaller scope of features. Usage of add-on depends on what features you want to use.
This Rake task will assign Duo Add-on seat to the 'root' user.
### Optional: Set `CLOUD_CONNECTOR_SELF_SIGN_TOKENS` environment variable
**Why:** Setting this environment variable will allow the local GitLab instance to
issue tokens itself, without syncing with CustomersDot first, which simplifies your local setup.
With this set, you can skip the
CustomersDot setup. However, note that this now requires the AI gateway to fetch token validation keys from your
GitLab instance instead of CustomersDot, as explained [here](#option-1-use-your-gitlab-instance-as-a-provider).
**Caveat:** Setting `CLOUD_CONNECTOR_SELF_SIGN_TOKENS` during local development creates a configuration that differs from
the production customer environment.
This divergence could mask potential issues that would only surface in the customer's setup.
**How:** The following should be set in the `env.runit` file in your GDK root:
```shell
# <GDK-root>/env.runit
export CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1
```
You need to restart GDK to apply the change.
### Testing the setup
In the Admin Area, you can run [a health check](../../user/gitlab_duo/setup.md#run-a-health-check-for-gitlab-duo)
to see if Duo is correctly set up.
### Enabled by default: Authentication and authorization in AI gateway
**Why:** The AI gateway has [authentication and authorization](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/auth.md)
flow to verify if clients have permission to access the features. Auth is
enforced in any live environments hosted by GitLab infra team.
To disable authorization checks (for debugging purposes only), set `AIGW_AUTH__BYPASS_EXTERNAL` to `true` in the
`<GDK-root>/env.runit` file in GDK.
#### Option 1: Use your GitLab instance as a provider
**Why:** this is the simplest method of testing authentication and reflects our setup on GitLab.com.
**How:**
Assuming that you are running the [AI gateway with GDK](#required-install-ai-gateway),
apply the following configuration to GDK:
```shell
# <GDK-root>/env.runit
export GITLAB_SIMULATE_SAAS=1
export CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1
export AIGW_AUTH__BYPASS_EXTERNAL=false
```
and `gdk restart`.
#### Option 2: Use your customersDot instance as a provider
**Why**: CustomersDot setup is required when you want to test or update functionality
related to [cloud licensing](https://about.gitlab.com/pricing/licensing-faq/cloud-licensing/).
This is the configuration that all self-managed customers use who do not self-host the AI gateway
but rely on our cloud-hosted AI gateway instances instead.
{{< alert type="note" >}}
This setup is challenging. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/463341)
for discussing how to make it easier to test the customersDot integration locally.
Until that is addressed, this setup process is time-consuming.
{{< /alert >}}
If you need to get customersDot working for your local GitLab Rails instance for
any reason, reach out to `#s_fulfillment_engineering` in Slack. For questions around the integration of CDot with other systems to deliver AI use cases, reach out to `#g_cloud_connector`.
assistance.
### Help
- [Here's how to reach us!](https://handbook.gitlab.com/handbook/engineering/development/data-science/ai-powered/ai-framework/#-how-to-reach-us)
- View [guidelines](duo_chat.md) for working with GitLab Duo Chat.
For detailed instructions on setting up your GDK for GitLab Duo development, see [GitLab Duo licensing for local development](ai_development_license.md).
## Tips for local development
@ -380,3 +228,8 @@ An [example](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/
## Security
Refer to the [secure coding guidelines for Artificial Intelligence (AI) features](../secure_coding_guidelines.md#artificial-intelligence-ai-features).
## Help
- [Here's how to reach us!](https://handbook.gitlab.com/handbook/engineering/development/data-science/ai-powered/ai-framework/#-how-to-reach-us)
- View [guidelines](duo_chat.md) for working with GitLab Duo Chat.

View File

@ -0,0 +1,281 @@
---
stage: AI-powered
group: AI Framework
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
description: Documentation about GitLab Duo licensing options for local development
title: GitLab Duo licensing for local development
---
This document explains the different licensing options available for GitLab Duo features in local development environments.
> **Note:** When developing GitLab Duo features, it's important to test in both multi-tenant (GitLab.com) and single-tenant (Self-managed/Dedicated) environments where appropriate. There is no default or recommended approach - the setup you choose should be based on your specific testing requirements.
## Overview
GitLab Duo features require either Duo Pro or Duo Enterprise licensing. When developing locally, there are multiple approaches to set up licensing, each serving different development needs.
This guide helps you understand:
- Which licensing approach to use for your specific development needs
- How to set up each licensing option
- The trade-offs between different approaches
## Quick reference
You should choose a license setup based on your development needs. Each approach provides a different testing environment:
| Development Scenario | License Setup | Instructions |
|----------------------|---------------------------|-------------|
| Multi-tenant setup (GitLab.com) | Local license with Rake task | [Option A](#option-a-local-license-with-rake-task-multi-tenantgitlabcom-mode) |
| Single-tenant setup (Self-managed/Dedicated) | Local license with Rake task in self-managed mode | [Option B](#option-b-local-license-with-rake-task-in-self-managed-mode-single-tenant-setup) |
| Full dog-fooding experience | Cloud license via CustomersDot | [Option C](#option-c-cloud-license-via-customersdot) |
## Prerequisites for all options
### Install AI gateway
**Why:** Duo features (except for Duo Workflow) route LLM requests through the AI gateway.
**How:**
Follow [these instructions](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/gitlab_ai_gateway.md#install)
to install the AI gateway with GDK. We recommend this route for most users.
You can also install AI gateway by:
1. [Cloning the repository directly](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist).
1. [Running the server locally](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#how-to-run-the-server-locally).
We only recommend this for users who have a specific reason for *not* running
the AI gateway through GDK.
### Set up GitLab Team Member License
**Why:** GitLab Duo is available to Premium and Ultimate customers only. You
likely want an Ultimate license for your GDK. Ultimate gets you access to
all GitLab Duo features.
**How:**
Follow [the process to obtain an EE license](https://handbook.gitlab.com/handbook/engineering/developer-onboarding/#working-on-gitlab-ee-developer-licenses)
for your local instance and [upload the license](../../administration/license_file.md).
To verify that the license is applied, go to **Admin area** > **Subscription**
and check the subscription plan.
## Option A: Local license with Rake task (Multi-tenant/GitLab.com mode)
This approach configures your environment to behave like GitLab.com (multi-tenant) for Duo features development.
### When to use
- When you need to test in a multi-tenant environment (similar to GitLab.com)
- When developing features that are specific to or need validation in GitLab.com
- When testing integration points that behave differently in multi-tenant mode
### Setup steps
- Ensure that you have a [GitLab Team Member License](https://handbook.gitlab.com/handbook/engineering/developer-onboarding/#working-on-gitlab-ee-developer-licenses) and that it is [activated](../../administration/license_file.md).
- Run the Rake task to set up Duo features for a group:
```shell
GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup'
```
- Restart your GDK:
```shell
gdk restart
```
- This Rake task creates a Duo Enterprise add-on attached to your group and assigns a Duo add-on seat to the 'root' user.
> **Note:** This Rake task primarily creates database records to simulate licensing in your development environment. With `GITLAB_SIMULATE_SAAS=1`, the environment is configured to behave like GitLab.com and allows self-signing tokens automatically.
- If you need to set up a Duo Pro add-on instead, run this Rake task:
```shell
GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[pro]'
```
### Pros
- Quick setup for multi-tenant testing
- No need to connect to external services
- Simulates the GitLab.com environment
- No need to set `CLOUD_CONNECTOR_SELF_SIGN_TOKENS` as it's handled automatically
### Cons
- Not a true representation of how real cloud customers would experience the setup
- May mask issues that only appear in production environments with real cloud licensing
## Option B: Local license with Rake task in self-managed mode (Single-tenant setup)
This approach configures your environment to behave like a self-managed or Dedicated GitLab instance (single-tenant).
### When to use
- When you need to test in a single-tenant environment (similar to Self-managed or Dedicated)
- When developing features that are specific to or need validation in self-managed or Dedicated environments
- When testing integration points that behave differently in single-tenant mode
### Setup steps
- Ensure that you have a [GitLab Team Member License](https://handbook.gitlab.com/handbook/engineering/developer-onboarding/#working-on-gitlab-ee-developer-licenses) and that it is [activated](../../administration/license_file.md).
- Run the Rake task to set up Duo features for the instance in self-managed mode:
```shell
GITLAB_SIMULATE_SAAS=0 bundle exec 'rake gitlab:duo:setup'
```
- Restart your GDK:
```shell
gdk restart
```
- This Rake task creates a Duo Enterprise add-on attached to your instance and assigns a Duo add-on seat to the 'root' user.
> **Note:** This Rake task primarily creates database records to simulate licensing in your development environment.
- For self-managed mode, you need to configure environment variables for self-signing tokens:
```shell
# <GDK-root>/env.runit
export CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1
```
- Restart GDK again to apply the change.
> **Note:** In self-managed mode, `CLOUD_CONNECTOR_SELF_SIGN_TOKENS` is required to allow your local GitLab instance to issue tokens itself, without syncing with CustomersDot. With `GITLAB_SIMULATE_SAAS=1` (Option A), this variable is not needed but can be left in place when switching between modes as it doesn't interfere with multi-tenant operation.
### Pros
- Quick setup for single-tenant testing
- No need to connect to external services
- Simulates self-managed/Dedicated environments
### Cons
- Not a true representation of how real self-managed customers would experience the setup
- May mask issues that only appear in production environments with real cloud licensing
## Option C: Cloud license via CustomersDot
This approach uses a real cloud license through CustomersDot, providing the most authentic testing environment that matches what customers experience.
### When to use
- When testing or updating functionality related to cloud licensing
- When you need to use the staging AI Gateway or want to run CustomersDot and the AI gateway locally
- When you want to fully dog-food the customer experience
- When troubleshooting issues specific to license validation in production
### Setup steps
- Add a **Self-Managed Ultimate** subscription with a [Duo Pro or Duo Enterprise subscription add-on](../../subscriptions/subscription-add-ons.md) to your GDK instance.
- Sign in to the [staging Customers Portal](https://customers.staging.gitlab.com) by selecting the **Continue with GitLab.com account** button. If you do not have an existing account, you are prompted to create one.
- If you do not have an existing cloud activation code, visit the **Self-Managed Ultimate Subscription** page using the [buy subscription flow link](https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/8aa922840091ad5c5d96ada43d0065a1b6198841/doc/flows/buy_subscription.md).
- Purchase the subscription using [a test credit card](https://gitlab.com/gitlab-org/customers-gitlab-com/#testing-credit-card-information).
- Once you have a subscription, on the subscription card, select the ellipse menu **...** > **Buy Duo Pro add-on** (or Duo Enterprise if needed).
- Use the previously saved credit card information, and the same number of seats as in the subscription.
- Follow the activation instructions:
- Set environment variables:
```shell
export GITLAB_LICENSE_MODE=test
export CUSTOMER_PORTAL_URL=https://customers.staging.gitlab.com
```
- **Note on GDK and AI Gateway:** While GDK can include AI Gateway as part of its distribution, developers may run AI Gateway with different configurations or ports. Currently, GitLab instances need explicit configuration of the AI Gateway URL, even in development environments.
- If you need to connect to the staging AI Gateway, configure it through the Admin UI (this option is only available with Ultimate license and active Duo Enterprise add-on):
1. Go to **Admin Area** > **Settings** > **GitLab Duo** > **Self-hosted models**
1. Set the **AI Gateway URL** to `https://cloud.staging.gitlab.com/ai`
1. Click **Save changes**
- Alternatively, you can set the AI gateway URL in a Rails console (useful when you don't have access to the Admin UI):
```ruby
Ai::Setting.instance.update!(ai_gateway_url: 'https://cloud.staging.gitlab.com/ai')
```
- Restart your GDK.
- Go to `/admin/subscription`.
- Optional. Remove any active license.
- Add the new activation code.
- Inside your GDK, navigate to **Admin area** > **GitLab Duo Pro**, go to `/admin/code_suggestions`
- Filter users to find `root` and click the toggle to assign a GitLab Duo Pro add-on seat to the root user.
### Pros
- Provides the most authentic testing environment
- Required for testing with staging AI Gateway
- Tests the complete flow including cloud license validation
- Most closely mirrors customer experience
### Cons
- Significantly more complex to set up
- Requires interaction with external services
- Time-consuming to configure
### Future improvements
> **Note:** There are ongoing plans to streamline the configuration of AI Gateway in development environments to reduce manual setup steps. In the future, we aim to automate this process as part of the GDK setup. For now, please follow the manual configuration steps described above.
## Setting up Duo on your GitLab.com staging account
When working in staging environments, you may need to set up Duo add-ons for your staging account. This is different from setting up your local development environment.
> **Note:** This section contains the same information as the previous [Staging account setup](staging_accounts.md) document, which now redirects here.
### Duo Pro
1. Have your account ready at <https://staging.gitlab.com>.
1. [Create a new group](../../user/group/_index.md#create-a-group) or use an existing one as the namespace which will receive the Duo Pro access.
1. Go to **Settings > Billing**.
1. Initiate the purchase flow for the Ultimate plan by clicking on `Upgrade to Ultimate`.
1. After being redirected to <https://customers.staging.gitlab.com>, click on `Continue with your Gitlab.com account`.
1. Purchase the SaaS Ultimate subscription using [a test credit card](https://gitlab.com/gitlab-org/customers-gitlab-com#testing-credit-card-information).
1. Find the newly purchased subscription card, and select from the three dots menu the option `Buy GitLab Duo Pro`.
1. Purchase the GitLab Duo Pro add-on using the same test credit card from the above steps.
1. Go back to <https://staging.gitlab.com> and verify that your group has access to Duo Pro by navigating to `Settings > GitLab Duo` and managing seats.
### Duo Enterprise
**Internal use only:** Given that purchasing a license for Duo Enterprise is not self-serviceable, post a request in the `#g_provision` Slack channel to grant access to your customer staging account with a Duo Enterprise license.
## Testing your setup
In the Admin Area, you can run [a health check](../../user/gitlab_duo/setup.md#run-a-health-check-for-gitlab-duo)
to see if Duo is correctly set up.
## Troubleshooting
If you're having issues with your Duo license setup:
- Verify your license is active by checking the Admin Area
- Ensure your user has a Duo seat assigned
- Run the [Duo health check](../../user/gitlab_duo/setup.md#run-a-health-check-for-gitlab-duo) to identify specific issues
- Check logs for any authentication or license validation errors
- For cloud license issues, reach out to `#s_fulfillment_engineering` in Slack
- For AI Gateway connection issues, reach out to `#g_cloud_connector` in Slack
## Best Practices
- **Test in both environments**: For thorough testing, consider alternating between multi-tenant and single-tenant setups to ensure your feature works well in both environments.
- **Consult domain documentation**: Review specific feature documentation to understand if there are any environment-specific behaviors you need to consider.
- **Consider end-user context**: Remember that features should work well for both GitLab.com users and self-managed/dedicated customers.
## Additional resources
- [AI Features Documentation](../ai_features/_index.md)
- [Code Suggestions Development](../ai_features/code_suggestions.md)
- [License Management Guidelines for Code Suggestions](../ai_features/code_suggestions.md#setup-instructions-to-use-gdk-with-the-code-suggestions-add-on)
- [Duo Enterprise License Access Process for Staging Environment](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/duo/duo_license.md?ref_type=heads)

View File

@ -3,20 +3,7 @@ stage: AI-powered
group: AI Framework
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
title: Set up Duo on your GitLab.com staging account
redirect_to: '../ai_features/ai_development_license.md#setting-up-duo-on-your-gitlabcom-staging-account'
---
## Duo Pro
1. Have your account ready at <https://staging.gitlab.com>.
1. [Create a new group](../../user/group/_index.md#create-a-group) or use an existing one as the namespace which will receive the Duo Pro access.
1. Go to **Settings > Billing**.
1. Initiate the purchase flow for the Ultimate plan by clicking on `Upgrade to Ultimate`.
1. After being redirected to <https://customers.staging.gitlab.com>, click on `Continue with your Gitlab.com account`.
1. Purchase the SaaS Ultimate subscription using [a test credit card](https://gitlab.com/gitlab-org/customers-gitlab-com#testing-credit-card-information).
1. Find the newly purchased subscription card, and select from the three dots menu the option `Buy GitLab Duo Pro`.
1. Purchase the GitLab Duo Pro add-on using the same test credit card from the above steps.
1. Go back to <https://staging.gitlab.com> and verify that your group has access to Duo Pro by navigating to `Settings > GitLab Duo` and managing seats.
## Duo Enterprise
**Internal use only:** Given that purchasing a license for Duo Enterprise is not self-serviceable, post a request in the `#g_provision` Slack channel to grant access to your customer staging account with a Duo Enterprise license.
This document has been consolidated into [GitLab Duo licensing for local development](ai_development_license.md#setting-up-duo-on-your-gitlabcom-staging-account).

View File

@ -1,87 +0,0 @@
---
stage: AI-powered
group: Custom Models
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
title: Setting up local development
---
## Set up your local GitLab instance
1. [Configure GDK to set up Duo Features in the local environment](../ai_features/_index.md)
1. For AI gateway:
- Set `AIGW_CUSTOM_MODELS__ENABLED=True`
- Set `AIGW_AUTH__BYPASS_EXTERNAL=False` or `AIGW_GITLAB_URL=<your-gitlab-instance>`
1. Run `gitlab:duo:verify_self_hosted_setup` task to verify the setup
## Configure self-hosted models
1. Follow the [instructions](../../administration/gitlab_duo_self_hosted/configure_duo_features.md#configure-the-self-hosted-model) to configure self-hosted models
1. Follow the [instructions](../../administration/gitlab_duo_self_hosted/configure_duo_features.md#configure-gitlab-duo-features-to-use-self-hosted-models) to configure features to use the models
AI-powered features are now powered by self-hosted models.
## Configure features to use AI vendor models
After adding [support](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/164924) for configuring features to either use self-hosted models for AI Vendor, setting `CLOUD_CONNECTOR_SELF_SIGN_TOKENS` is no longer necessary for the customers. But it is harder for developers to configure the features to use AI vendored because we still want to send all requests to the local AI gateway instead of Cloud Connector.
Setting [`CLOUD_CONNECTOR_BASE_URL`](https://gitlab.com/gitlab-org/gitlab/-/blob/1452de8cde035bb5eba53ba2a2903c28fc237455/config/initializers/1_settings.rb#L1028) is not sufficient because we [add](https://gitlab.com/gitlab-org/gitlab/-/blob/1452de8cde035bb5eba53ba2a2903c28fc237455/ee/lib/gitlab/ai_gateway.rb#L14) `/ai` suffix to it.
Currently, there are the following workarounds:
1. Verify that `CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1`
1. Remove `ai_feature_settings` record responsible for the configuration to fallback to using the AI gateway URL (configured through `Ai::Setting.instance.ai_gateway_url`) as Cloud Connector URL:
```ruby
Ai::FeatureSetting.find_by(feature: :duo_chat).destroy!
```
## Testing
To comprehensively test that a feature using Custom Models works as expected, you must write `system` specs.
This is required because, unlike `unit` tests, `system` specs invoke all the components involved in the custom models stack. For example, the Puma, Workhorse, AI gateway + LLM Mock server.
To write a new `system` test and for it to run successfully, there are the following prerequisites:
- AI gateway must be running (usually on port `5052`), and you must configure the AI gateway URL through the `Ai::Setting.instance` record:
```shell
Ai::Setting.instance.update!(ai_gateway_url: 'http://localhost:5052')
```
- We use [LiteLLM proxy](https://www.litellm.ai/) to return mock responses. You must configure LiteLLM to return mock responses using a configuration file:
```yaml
# config.yaml
model_list:
- model_name: codestral
litellm_params:
model: ollama/codestral
mock_response: "Mock response from codestral"
```
- LiteLLM proxy must be running (usually on port `4000`), and the you must configure the environment variable `LITELLM_PROXY_URL`:
```shell
litellm --config config.yaml
export LITELLM_PROXY_URL="http://localhost:4000"
```
- You must tag the RSpec file with `requires_custom_models_setup`.
For an example, see [`ee/spec/features/custom_models/code_suggestions_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/244e37a201620f9d98503e186b60e4e572a05d6e/ee/spec/features/custom_models/code_suggestions_spec.rb). In this file, we test that the code completions feature uses a self-hosted `codestral` model.
### Testing On CI
On CI, AI gateway and LiteLLM proxy are already configured to run for all tests tagged with `requires_custom_models_setup`.
<!-- markdownlint-disable proper-names -->
<!-- vale gitlab_base.Substitutions = NO -->
However, you must also update the `config` for LiteLLM if you are testing features that use newer models in the specs that have not been used before.
The configuration for LiteLLM is in [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/2b14f212d48ca2c22904805600491baf8460427e/.gitlab/ci/global.gitlab-ci.yml#L332).
<!-- vale gitlab_base.Substitutions = YES -->
<!-- markdownlint-enable proper-names -->

View File

@ -7,12 +7,44 @@ title: Python Merge Requests Guidelines
GitLab standard [code review guidelines](../code_review.md#approval-guidelines) apply to Python projects as well.
## How to find a reviewer
## How to set up a Python code review process
This section explains how to integrate your project with [reviewer roulette](../code_review.md#reviewer-roulette)
and other resources to find reviewers with Python expertise.
There are two main approaches to set up a Python code review process at GitLab:
[Work item](https://gitlab.com/gitlab-org/gitlab/-/issues/514318).
1. **Established Projects:** Larger Python projects typically have their own dedicated pool of reviewers through reviewer-roulette. To set this up, please refer to [Setting Up Reviewer Roulette](#setting-up-reviewer-roulette).
1. **Smaller Projects:** For projects with fewer contributors, we maintain a shared pool of Python reviewers across GitLab.
### Setting Up Reviewer Roulette
This section explains how to integrate your project with [reviewer roulette](../code_review.md#reviewer-roulette) and other resources to connect project contributors with Python experts for code reviews.
For both large and small projects, Reviewer Roulette can automate the reviewer assignment process. To set up:
1. Add the Python project to the list of [GitLab projects](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/projects.yml?ref_type=heads).
1. Project maintainer(s) should add a group for the project in the [GitLab.org maintainers repository](https://gitlab.com/gitlab-org/maintainers)
1. Install and configure [Dangerfiles](https://gitlab.com/gitlab-org/ruby/gems/gitlab-dangerfiles) in your project, ensuring [CI is properly set up](https://gitlab.com/gitlab-org/ruby/gems/gitlab-dangerfiles#ci-configuration) to enable the Reviewer Roulette plugin.
Then, depending on your project size:
- **For large projects with sufficient contributors:**
- Eligible team members should add the Python project to the `projects` field in their individual entry in [team_members](https://gitlab.com/gitlab-com/www-gitlab-com/-/tree/master/data/team_members/person) or [team_database](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/doc/team_database.md), specifying appropriate roles such as reviewer or maintainer.
- Add the [individual roulette configuration](https://gitlab.com/gitlab-org/python/code-review-templates/-/tree/main/individual_roulette?ref_type=heads) to your project.
- **For smaller projects (e.g. fewer than 10 contributors):**
- Leverage the company wide pool of Python experts by adding the [shared pool configuration](https://gitlab.com/gitlab-org/python/code-review-templates/-/tree/main/shared_pull/danger?ref_type=heads) to your project.
- You can also encourage contributors or other non-domain reviewers to reach out in your team's Slack channel for domain expertise where needed.
When a merge request is created, Review Roulette will randomly select qualified reviewers based on your configuration.
### Additional recommendations
Please refer to [the documentation](../code_review.md#reviewer-roulette)
### Ask for help
If contributors have questions or need additional help with Python-specific reviews, direct them to the GitLab #python or #python_maintainers Slack channels for assistance.
## How to find a project to review

View File

@ -145,18 +145,14 @@ For use cases and best practices, follow the [GitLab Duo examples documentation]
## The context Code Suggestions is aware of
Code Suggestions is aware of and uses:
Code Suggestions uses the following information about your development environment as context to enrich suggestions:
- The file open in your IDE.
- The content before and after the cursor in that file.
- The file open in your IDE, including the content before and after the cursor in that file.
- The filename and extension.
Code Suggestions also uses files from your repository as context to make suggestions and
generate code:
- Code completion can use files in your repository that are written in the [languages enabled for Code Suggestions in your IDE](supported_extensions.md#supported-languages).
- Code generation can use files in your repository that are written in the following
languages:
- (Optional) Files open in tabs in your IDE. These files give GitLab Duo more information about the standards and practices in your code project. On by default. To manage tabs as context, see [Using open files as context](#using-open-files-as-context).
- (Optional) Files imported in the current opened file. These imported files give GitLab Duo more information about the classes and methods used in the current file. Off by default. To manage imported files as context, see [Using imported files as context](#using-imported-files-as-context).
- Code suggestion functionality uses content from the [supported languages](supported_extensions.md#supported-languages).
- Code generation functionality uses content from the following languages:
- Go
- Java
- JavaScript
@ -197,11 +193,7 @@ GitLab recently refactored the Open Tabs internal logic for Duo Code Suggestions
{{< /alert >}}
As well as using files from your repository, Code Suggestions can use the files
open in tabs in your IDE as context.
These files give GitLab Duo more information about the standards and practices
in your code project.
If tab as context is on, Code Suggestions uses the files open in tabs in your IDE as context. These files give GitLab Duo more information about the standards and practices in your code project.
#### Turn on open files as context

View File

@ -43,8 +43,7 @@ module API
Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new,
Gitlab::GrapeLogging::Loggers::ContextLogger.new,
Gitlab::GrapeLogging::Loggers::ContentLogger.new,
Gitlab::GrapeLogging::Loggers::UrgencyLogger.new,
Gitlab::GrapeLogging::Loggers::ResponseLogger.new
Gitlab::GrapeLogging::Loggers::UrgencyLogger.new
]
allow_access_with_scope :api

View File

@ -23,7 +23,7 @@ module Gitlab
SECUREFLAG_DATA = {
name: 'SecureFlag',
description: "Get remediation advice with example code and recommended hands-on labs in a fully
interactive virtualised environment.",
interactive virtualized environment.",
url: "https://knowledge-base-api.secureflag.com/gitlab"
}.freeze

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
module Gitlab
module GrapeLogging
module Loggers
class ResponseLogger < ::GrapeLogging::Loggers::Base
def parameters(_, response)
return {} unless Feature.enabled?(:log_response_length)
response_bytes = 0
case response
when String
response_bytes = response.bytesize
else
response.each { |resp| response_bytes += resp.to_s.bytesize }
end
{
response_bytes: response_bytes
}
end
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
LIMITED_ARRAY_SENTINEL = { key: 'truncated', value: '...' }.freeze
IGNORE_PARAMS = Set.new(%w[controller action format]).freeze
KNOWN_PAYLOAD_PARAMS = [:remote_ip, :user_id, :username, :ua, :queue_duration_s, :response_bytes,
KNOWN_PAYLOAD_PARAMS = [:remote_ip, :user_id, :username, :ua, :queue_duration_s,
:etag_route, :request_urgency, :target_duration_s] + CLOUDFLARE_CUSTOM_HEADERS.values
def self.call(event)
@ -36,10 +36,6 @@ module Gitlab
payload[:feature_flag_states] = Feature.logged_states.map { |key, state| "#{key}:#{state ? 1 : 0}" }
end
if Feature.disabled?(:log_response_length)
payload.delete(:response_bytes)
end
payload
end
end

View File

@ -63749,6 +63749,9 @@ msgstr ""
msgid "UserMapping|Placeholders"
msgstr ""
msgid "UserMapping|Please upload a valid CSV file."
msgstr ""
msgid "UserMapping|Reassign"
msgstr ""

View File

@ -56,8 +56,7 @@ module QA
show.delete_account(user.password)
end
expect { user.exists? }.to eventually_be_falsey.within(max_duration: 120, sleep_interval: 3),
"Expected user to be deleted, but it still exists"
expect(page).to have_text("Account scheduled for removal.")
end
it "allows to recreate deleted user with same credeintials",

View File

@ -543,23 +543,25 @@ function log_disk_usage() {
# all functions below are for customizing CI job exit code
function run_with_custom_exit_code() {
set +e # temporarily disable exit on error to prevent premature exit
set -o pipefail # Take the exit status of the rightmost command that failed
set +e # temporarily disable exit on error to prevent premature exit
# runs command passed in as argument, save standard error and standard output
output=$(set -e; "$@" 2>&1)
local trace_file="/tmp/stdout_stderr_log.out"
# Run the command and tee output to both the terminal and the file
"$@" 2>&1 | tee "$trace_file"
initial_exit_code=$?
echo "initial_exit_code: $initial_exit_code"
local trace_file="stdout_stderr_log.out"
echo "$output" | tee "$trace_file"
find_custom_exit_code "$initial_exit_code" "$trace_file"
new_exit_code=$?
echo "new_exit_code=$new_exit_code"
# Restore shell default behavior
set -e
set +o pipefail
exit "$new_exit_code"
}

View File

@ -561,28 +561,6 @@ RSpec.describe ApplicationController, feature_category: :shared do
expect(controller.last_payload[:target_duration_s]).to eq(0.25)
end
end
it 'logs response length' do
sign_in user
get :index
expect(controller.last_payload[:response_bytes]).to eq('authenticated'.bytesize)
end
context 'with log_response_length disabled' do
before do
stub_feature_flags(log_response_length: false)
end
it 'logs response length' do
sign_in user
get :index
expect(controller.last_payload).not_to include(:response_bytes)
end
end
end
describe '#access_denied' do

View File

@ -39,27 +39,5 @@ RSpec.describe Oauth::TokensController, feature_category: :user_management do
expect(controller.last_payload[:metadata]).to include(Gitlab::ApplicationContext.current)
end
it 'logs response length' do
sign_in user
post :create
expect(controller.last_payload[:response_bytes]).to eq('authenticated'.bytesize)
end
context 'with log_response_length disabled' do
before do
stub_feature_flags(log_response_length: false)
end
it 'logs response length' do
sign_in user
post :create
expect(controller.last_payload).not_to include(:response_bytes)
end
end
end
end

View File

@ -113,8 +113,8 @@ describe('TabView', () => {
});
});
it('passes projects to `ProjectsList` component', () => {
expect(findProjectsList().props('projects')).toEqual(formatProjects(expectedProjects));
it('passes items to `ProjectsList` component', () => {
expect(findProjectsList().props('items')).toEqual(formatProjects(expectedProjects));
});
it('passes `timestampType` prop to `ProjectsList` component', () => {
@ -292,8 +292,12 @@ describe('TabView', () => {
});
it('renders an empty state and passes title and description prop', () => {
expect(findEmptyState().props('title')).toBe(CONTRIBUTED_TAB.emptyState.title);
expect(findEmptyState().props('description')).toBe(CONTRIBUTED_TAB.emptyState.description);
expect(findEmptyState().props('title')).toBe(
CONTRIBUTED_TAB.emptyStateComponentProps.title,
);
expect(findEmptyState().props('description')).toBe(
CONTRIBUTED_TAB.emptyStateComponentProps.description,
);
});
});

View File

@ -64,6 +64,10 @@ const defaultProvide = {
programmingLanguages,
};
const defaultPropsData = {
tabs: PROJECT_DASHBOARD_TABS,
};
const searchTerm = 'foo bar';
const mockEndCursor = 'mockEndCursor';
const mockStartCursor = 'mockStartCursor';
@ -86,6 +90,7 @@ describe('TabsWithList', () => {
const createComponent = async ({
provide = {},
propsData = {},
projectsCountHandler = successHandler,
userPreferencesUpdateHandler = userPreferencesUpdateSuccessHandler,
route = defaultRoute,
@ -104,6 +109,7 @@ describe('TabsWithList', () => {
TabView: stubComponent(TabView),
},
provide: { ...defaultProvide, ...provide },
propsData: { ...defaultPropsData, ...propsData },
});
};

View File

@ -58,10 +58,6 @@ describe('CsvUploadModal', () => {
});
describe('CSV upload', () => {
beforeEach(() => {
jest.spyOn(FileReader.prototype, 'readAsText');
});
it('renders the upload dropzone', () => {
expect(findUploadDropzone().exists()).toBe(true);
});
@ -90,18 +86,23 @@ describe('CsvUploadModal', () => {
});
describe('submitting the data', () => {
const preventDefault = jest.fn();
const file = new File(['test'], 'file.csv');
beforeEach(() => {
jest.spyOn(axios, 'post');
findUploadDropzone().vm.$emit('change', file);
});
it('prevents modal from getting closed', () => {
findGlModal().vm.$emit('primary', { preventDefault });
expect(preventDefault).toHaveBeenCalled();
});
it('calls the endpoint with the correct data', async () => {
const uploadDropzone = findUploadDropzone();
const file = new File(['test'], 'file.csv');
uploadDropzone.vm.$emit('change', file);
await waitForPromises();
findGlModal().vm.$emit('primary');
findGlModal().vm.$emit('primary', { preventDefault });
await waitForPromises();
const expectedFormData = new FormData();
@ -118,7 +119,7 @@ describe('CsvUploadModal', () => {
.onPost(MOCK_REASSIGNMENT_CSV_PATH)
.reply(HTTP_STATUS_OK, { message: mockMessage });
findGlModal().vm.$emit('primary');
findGlModal().vm.$emit('primary', { preventDefault });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
@ -136,7 +137,7 @@ describe('CsvUploadModal', () => {
.onPost(MOCK_REASSIGNMENT_CSV_PATH)
.reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, { message: mockMessage });
findGlModal().vm.$emit('primary');
findGlModal().vm.$emit('primary', { preventDefault });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
@ -149,7 +150,7 @@ describe('CsvUploadModal', () => {
.onPost(MOCK_REASSIGNMENT_CSV_PATH)
.reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { error: new Error('error uploading CSV') });
findGlModal().vm.$emit('primary');
findGlModal().vm.$emit('primary', { preventDefault });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({

View File

@ -107,7 +107,7 @@ describe('GroupsView', () => {
const findGroupsList = () => wrapper.findComponent(GroupsList);
const findGroupsListByGroupId = (groupId) =>
findGroupsList()
.props('groups')
.props('items')
.find((group) => group.id === groupId);
afterEach(() => {
@ -189,7 +189,7 @@ describe('GroupsView', () => {
await waitForPromises();
expect(findGroupsList().props()).toMatchObject({
groups: formatGroups(nodes),
items: formatGroups(nodes),
showGroupIcon: true,
listItemClass: defaultPropsData.listItemClass,
timestampType: TIMESTAMP_TYPE_CREATED_AT,

View File

@ -164,7 +164,7 @@ describe('ProjectsView', () => {
await waitForPromises();
expect(findProjectsList().props()).toMatchObject({
projects: formatProjects(nodes),
items: formatProjects(nodes),
showProjectIcon: true,
listItemClass: defaultPropsData.listItemClass,
timestampType: TIMESTAMP_TYPE_CREATED_AT,

View File

@ -86,7 +86,7 @@ describe('OverviewTab', () => {
wrapper
.findByTestId('personal-projects-section')
.findComponent(ProjectsList)
.props('projects'),
.props('items'),
).toMatchObject(defaultPropsData.personalProjects);
});
});

View File

@ -1,5 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import YourWorkProjectsApp from '~/projects/your_work/components/app.vue';
import { PROJECT_DASHBOARD_TABS } from '~/projects/your_work/constants';
import TabsWithList from '~/groups_projects/components/tabs_with_list.vue';
describe('YourWorkProjectsApp', () => {
@ -13,7 +14,9 @@ describe('YourWorkProjectsApp', () => {
createComponent();
});
it('renders TabsWithList component', () => {
expect(wrapper.findComponent(TabsWithList).exists()).toBe(true);
it('renders TabsWithList component and passes correct props', () => {
expect(wrapper.findComponent(TabsWithList).props()).toEqual({
tabs: PROJECT_DASHBOARD_TABS,
});
});
});

View File

@ -8,7 +8,7 @@ import { stubComponent } from 'helpers/stub_component';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
import SearchTypeIndicator from '~/search/topbar/components/search_type_indicator.vue';
import GlSearchBoxByType from '~/search/topbar/components/search_box_by_type.vue';
import GlobalSearchInput from '~/search/topbar/components/global_search_input.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
import {
SYNTAX_OPTIONS_ADVANCED_DOCUMENT,
@ -54,7 +54,7 @@ describe('GlobalSearchTopbar', () => {
});
};
const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findGlSearchBox = () => wrapper.findComponent(GlobalSearchInput);
const findSyntaxOptionButton = () => wrapper.findComponent(GlButton);
const findSyntaxOptionDrawer = () => wrapper.findComponent(MarkdownDrawer);
const findSearchTypeIndicator = () => wrapper.findComponent(SearchTypeIndicator);
@ -208,7 +208,7 @@ describe('GlobalSearchTopbar', () => {
beforeEach(() => {
createComponent({
initialState: { query: { search }, searchType: 'zoekt' },
stubs: { GlSearchBoxByType },
stubs: { GlobalSearchInput },
});
});

View File

@ -6,16 +6,16 @@ import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlLoadingIcon } from '@gitlab/ui';
import ClearIcon from '~/search/topbar/components/clear_icon_button.vue';
import SearchBoxByType from '~/search/topbar/components/search_box_by_type.vue';
import GlobalSearchInput from '~/search/topbar/components/global_search_input.vue';
const modelEvent = SearchBoxByType.model.event;
const modelEvent = GlobalSearchInput.model.event;
const newValue = 'new value';
describe('search box by type component', () => {
let wrapper;
const createComponent = ({ listeners, ...propsData }, mountFn = shallowMount) => {
wrapper = mountFn(SearchBoxByType, { propsData, listeners });
wrapper = mountFn(GlobalSearchInput, { propsData, listeners });
};
const findClearIcon = () => wrapper.findComponent(ClearIcon);

View File

@ -8,7 +8,7 @@ describe('GroupsList', () => {
let wrapper;
const defaultPropsData = {
groups,
items: groups,
listItemClass: 'gl-px-5',
};
@ -27,7 +27,7 @@ describe('GroupsList', () => {
);
expect(expectedProps).toEqual(
defaultPropsData.groups.map((group) => ({
defaultPropsData.items.map((group) => ({
group,
showGroupIcon: false,
listItemClass: defaultPropsData.listItemClass,
@ -37,7 +37,7 @@ describe('GroupsList', () => {
});
describe('when `GroupsListItem` emits `delete` event', () => {
const [firstGroup] = defaultPropsData.groups;
const [firstGroup] = defaultPropsData.items;
beforeEach(() => {
createComponent();

View File

@ -9,7 +9,7 @@ describe('ProjectsList', () => {
let wrapper;
const defaultPropsData = {
projects: convertObjectPropsToCamelCase(projects, { deep: true }),
items: convertObjectPropsToCamelCase(projects, { deep: true }),
listItemClass: 'gl-px-5',
};
@ -28,7 +28,7 @@ describe('ProjectsList', () => {
);
expect(expectedProps).toEqual(
defaultPropsData.projects.map((project) => ({
defaultPropsData.items.map((project) => ({
project,
showProjectIcon: false,
listItemClass: defaultPropsData.listItemClass,

View File

@ -296,6 +296,52 @@ describe('Upload dropzone component', () => {
});
});
describe('file input change', () => {
// See note in the 'updates file input files value' test for more details
// on why this function exists.
const stubFileInputOnWrapper = (files = []) => {
Object.defineProperty(wrapper.vm.$refs.fileUpload, 'files', {
writable: true,
value: files,
});
};
const validFile = { type: 'image/jpg' };
const invalidFile = { type: 'audio/midi' };
describe('when all uploaded files are valid', () => {
it('emits change event with valid files', () => {
createComponent();
stubFileInputOnWrapper([validFile, validFile]);
findFileInput().trigger('change');
expect(wrapper.emitted('change')).toEqual([[[validFile, validFile]]]);
});
it('emits single file when singleFileSelection is true', () => {
createComponent({
props: { singleFileSelection: true },
});
stubFileInputOnWrapper([validFile]);
findFileInput().trigger('change');
expect(wrapper.emitted('change')).toEqual([[validFile]]);
});
});
describe('when some uploaded files are invalid', () => {
it('emits error event when some uploaded files are invalid', () => {
createComponent();
stubFileInputOnWrapper([validFile, invalidFile]);
findFileInput().trigger('change');
expect(wrapper.emitted('error')).toHaveLength(1);
});
});
});
describe('updates file input files value', () => {
// NOTE: the component assigns dropped files from the drop event to the
// input.files property. There's a restriction that nothing but a FileList

View File

@ -1,37 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GrapeLogging::Loggers::ResponseLogger do
let(:logger) { described_class.new }
describe '#parameters' do
let(:response1) { 'response1' }
let(:response) { [response1] }
subject { logger.parameters(nil, response) }
it { expect(subject).to eq({ response_bytes: response1.bytesize }) }
context 'with multiple response parts' do
let(:response2) { 'response2' }
let(:response) { [response1, response2] }
it { expect(subject).to eq({ response_bytes: response1.bytesize + response2.bytesize }) }
end
context 'with log_response_length disabled' do
before do
stub_feature_flags(log_response_length: false)
end
it { expect(subject).to eq({}) }
end
context 'when response is a String' do
let(:response) { response1 }
it { expect(subject).to eq({ response_bytes: response1.bytesize }) }
end
end
end

View File

@ -25,8 +25,7 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
remote_ip: '192.168.1.2',
ua: 'Nyxt',
queue_duration_s: 0.2,
etag_route: '/etag',
response_bytes: 1234
etag_route: '/etag'
}
end
@ -56,20 +55,6 @@ RSpec.describe Gitlab::Lograge::CustomOptions do
expect(subject[:user_id]).to eq('test')
end
it 'adds the response length' do
expect(subject[:response_bytes]).to eq(1234)
end
context 'with log_response_length disabled' do
before do
stub_feature_flags(log_response_length: false)
end
it 'does not add the response length' do
expect(subject).not_to include(:response_bytes)
end
end
it 'adds Cloudflare headers' do
expect(subject[:cf_ray]).to eq(event.payload[:cf_ray])
expect(subject[:cf_request_id]).to eq(event.payload[:cf_request_id])

View File

@ -1779,7 +1779,18 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
describe '#vscode_extension_marketplace' do
let(:invalid_custom) { { enabled: false, preset: "custom", custom_values: {} } }
let(:valid_open_vsx) { { enabled: true, preset: "open_vsx" } }
let(:invalid_custom_urls) do
{
enabled: true,
preset: "custom",
custom_values: {
item_url: "abc",
service_url: "def",
resource_url_template: "ghi"
}
}
end
let(:valid_custom) do
{
enabled: false,
@ -1792,15 +1803,20 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
}
end
let(:valid_open_vsx) { { enabled: true, preset: "open_vsx" } }
let(:valid_open_vsx_with_custom) { valid_custom.merge(valid_open_vsx) }
# valid json
it { is_expected.to allow_value({}).for(:vscode_extension_marketplace) }
it { is_expected.to allow_value({ enabled: true, preset: "open_vsx" }).for(:vscode_extension_marketplace) }
it { is_expected.to allow_value(valid_open_vsx).for(:vscode_extension_marketplace) }
it { is_expected.to allow_value(valid_open_vsx_with_custom).for(:vscode_extension_marketplace) }
it { is_expected.to allow_value(valid_custom).for(:vscode_extension_marketplace) }
# invalid json
it { is_expected.not_to allow_value({ enabled: false, preset: "foo" }).for(:vscode_extension_marketplace) }
it { is_expected.not_to allow_value({ enabled: true, preset: "custom" }).for(:vscode_extension_marketplace) }
it { is_expected.not_to allow_value(invalid_custom).for(:vscode_extension_marketplace) }
it { is_expected.not_to allow_value(invalid_custom_urls).for(:vscode_extension_marketplace) }
end
describe '#vscode_extension_marketplace_enabled' do

View File

@ -101,11 +101,9 @@
- :quarantine
- :query_analyzers
- :rd_fast
- :real_ai_request
- :redis
- :reestablished_active_record_base
- :request_store
- :requires_custom_models_setup
- :rerun_file_path
- :retry
- :retry_attempts

View File

@ -5052,7 +5052,6 @@
- './spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb'
- './spec/lib/gitlab/grape_logging/loggers/perf_logger_spec.rb'
- './spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb'
- './spec/lib/gitlab/grape_logging/loggers/response_logger_spec.rb'
- './spec/lib/gitlab/grape_logging/loggers/token_logger_spec.rb'
- './spec/lib/gitlab/grape_logging/loggers/urgency_logger_spec.rb'
- './spec/lib/gitlab/graphs/commits_spec.rb'