diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml index 024405aeb42..94c60ec1ba9 100644 --- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: # Helm chart ref used by test-on-cng pipeline GITLAB_HELM_CHART_REF: "6cdb0e1cd4ceb7c9fd01ffa2f62c4a7a4c77a23b" # Specific ref for cng-mirror project to trigger builds for - GITLAB_CNG_MIRROR_REF: "272b2069fc386348d2474eac26060e781f2a0dd2" + GITLAB_CNG_MIRROR_REF: "51637e0b7ab573aa0817945df517c7c0d0250d79" # Makes sure some of the common scripts from pipeline-common use bundler to execute commands RUN_WITH_BUNDLE: "true" # Makes sure reporting script defined in .gitlab-qa-report from pipeline-common is executed from correct folder diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index cce98a73a4a..99ef55e4b75 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -6093f9c42ad8d328c37423e3067f3c9e7cd1ddcf +c995db1d0baa5ffa66832d48adeac8c2eabe2811 diff --git a/Gemfile.checksum b/Gemfile.checksum index 92e279b3587..7524f1d94d2 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -364,7 +364,7 @@ {"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"}, {"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"}, {"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"}, -{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"}, +{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"}, {"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"}, {"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"}, {"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"}, diff --git a/Gemfile.lock b/Gemfile.lock index acee5a80809..814cf1a71a2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1105,7 +1105,7 @@ GEM language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - lefthook (1.11.14) + lefthook (1.11.16) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index 03aeb92be68..40be68656f9 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -364,7 +364,7 @@ {"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"}, {"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"}, {"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"}, -{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"}, +{"name":"lefthook","version":"1.11.16","platform":"ruby","checksum":"1a273f1963b4a899c56f7cf14ccea90b17af239bf7acf09cbc9e4ca6298ee685"}, {"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"}, {"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"}, {"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 7665b9909f2..c08a1c7d679 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -1099,7 +1099,7 @@ GEM language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - lefthook (1.11.14) + lefthook (1.11.16) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) diff --git a/app/assets/javascripts/admin/projects/index/components/app.vue b/app/assets/javascripts/admin/projects/index/components/app.vue index 98dc0393ebf..5bb99fd96fe 100644 --- a/app/assets/javascripts/admin/projects/index/components/app.vue +++ b/app/assets/javascripts/admin/projects/index/components/app.vue @@ -3,6 +3,7 @@ import TabsWithList from '~/groups_projects/components/tabs_with_list.vue'; import { FILTERED_SEARCH_TOKEN_LANGUAGE, FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, PAGINATION_TYPE_KEYSET, } from '~/groups_projects/constants'; import { @@ -31,6 +32,7 @@ export default { filteredSearchSupportedTokens: [ FILTERED_SEARCH_TOKEN_LANGUAGE, FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, ], timestampTypeMap: { [SORT_OPTION_CREATED.value]: TIMESTAMP_TYPE_CREATED_AT, diff --git a/app/assets/javascripts/admin/projects/index/graphql/queries/project_counts.query.graphql b/app/assets/javascripts/admin/projects/index/graphql/queries/project_counts.query.graphql index 07a2a72dd71..6ba319bcc8e 100644 --- a/app/assets/javascripts/admin/projects/index/graphql/queries/project_counts.query.graphql +++ b/app/assets/javascripts/admin/projects/index/graphql/queries/project_counts.query.graphql @@ -2,12 +2,14 @@ query getAdminProjectCounts( $search: String $minAccessLevel: AccessLevelEnum $programmingLanguageName: String + $visibilityLevel: VisibilityLevelsEnum ) { active: projects( active: true search: $search minAccessLevel: $minAccessLevel programmingLanguageName: $programmingLanguageName + visibilityLevel: $visibilityLevel ) { count } @@ -16,6 +18,7 @@ query getAdminProjectCounts( search: $search minAccessLevel: $minAccessLevel programmingLanguageName: $programmingLanguageName + visibilityLevel: $visibilityLevel ) { count } diff --git a/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql b/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql index e10f2ec17a0..41c11066603 100644 --- a/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql +++ b/app/assets/javascripts/admin/projects/index/graphql/queries/projects.query.graphql @@ -10,6 +10,7 @@ query getAdminProjects( $sort: String $search: String $minAccessLevel: AccessLevelEnum + $visibilityLevel: VisibilityLevelsEnum $programmingLanguageName: String ) { projects( @@ -22,6 +23,7 @@ query getAdminProjects( search: $search minAccessLevel: $minAccessLevel programmingLanguageName: $programmingLanguageName + visibilityLevel: $visibilityLevel ) { count nodes { diff --git a/app/assets/javascripts/groups_projects/components/tabs_with_list.vue b/app/assets/javascripts/groups_projects/components/tabs_with_list.vue index 59bbac30128..b09a2ee27f3 100644 --- a/app/assets/javascripts/groups_projects/components/tabs_with_list.vue +++ b/app/assets/javascripts/groups_projects/components/tabs_with_list.vue @@ -13,6 +13,12 @@ import { ACCESS_LEVEL_OWNER_INTEGER, ACCESS_LEVELS_INTEGER_TO_STRING, } from '~/access_level/constants'; +import { + VISIBILITY_LEVEL_LABELS, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { InternalEvents } from '~/tracking'; import { @@ -23,6 +29,7 @@ import { PAGINATION_TYPE_KEYSET, PAGINATION_TYPE_OFFSET, QUERY_PARAM_PAGE, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, } from '../constants'; import userPreferencesUpdateMutation from '../graphql/mutations/user_preferences_update.mutation.graphql'; import TabView from './tab_view.vue'; @@ -186,6 +193,22 @@ export default { }, ], }, + { + type: FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, + icon: 'eye', + title: __('Visibility'), + token: GlFilteredSearchToken, + unique: true, + operators: OPERATORS_IS, + options: [ + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + ].map((visibilityLevelString) => ({ + value: visibilityLevelString, + title: VISIBILITY_LEVEL_LABELS[visibilityLevelString], + })), + }, ].filter((filteredSearchToken) => this.filteredSearchSupportedTokens.includes(filteredSearchToken.type), ); @@ -236,9 +259,8 @@ export default { }, filters() { const filters = pick(this.routeQueryWithoutPagination, [ - FILTERED_SEARCH_TOKEN_LANGUAGE, - FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, this.filteredSearchTermKey, + ...this.filteredSearchSupportedTokens, ]); // Normalize the property to Number since Vue Router 4 will @@ -265,10 +287,15 @@ export default { this.programmingLanguages.find(({ id }) => id === parseInt(programmingLanguageId, 10))?.name ); }, + visibilityLevel() { + const visibilityLevel = this.filters[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]; + return Array.isArray(visibilityLevel) ? visibilityLevel[0] : visibilityLevel; + }, filtersAsQueryVariables() { return { programmingLanguageName: this.programmingLanguageName, minAccessLevel: this.minAccessLevel, + visibilityLevel: this.visibilityLevel, }; }, timestampType() { @@ -509,8 +536,9 @@ export default { size="sm" class="gl-tab-counter-badge" data-testid="tab-counter-badge" - >{{ tabCount(tab) }} + {{ tabCount(tab) }} + diff --git a/app/assets/javascripts/groups_projects/constants.js b/app/assets/javascripts/groups_projects/constants.js index e6a6994b63f..8874427ac82 100644 --- a/app/assets/javascripts/groups_projects/constants.js +++ b/app/assets/javascripts/groups_projects/constants.js @@ -8,6 +8,7 @@ export const SORT_LABEL_STORAGE_SIZE = __('Storage size'); export const FILTERED_SEARCH_TOKEN_LANGUAGE = 'language'; export const FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL = 'min_access_level'; +export const FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL = 'visibility_level'; export const SORT_DIRECTION_ASC = 'asc'; export const SORT_DIRECTION_DESC = 'desc'; diff --git a/app/assets/javascripts/members/placeholders/components/placeholders_table.vue b/app/assets/javascripts/members/placeholders/components/placeholders_table.vue index 69a0fa6c6e6..bb02dba7446 100644 --- a/app/assets/javascripts/members/placeholders/components/placeholders_table.vue +++ b/app/assets/javascripts/members/placeholders/components/placeholders_table.vue @@ -2,7 +2,6 @@ import { GlAvatarLabeled, GlBadge, - GlEmptyState, GlIcon, GlKeysetPagination, GlLoadingIcon, @@ -18,6 +17,7 @@ import { fetchPolicies } from '~/lib/graphql'; import { DEFAULT_PAGE_SIZE } from '~/members/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import EmptyResult from '~/vue_shared/components/empty_result.vue'; import { PLACEHOLDER_STATUS_KEPT_AS_PLACEHOLDER, PLACEHOLDER_STATUS_COMPLETED, @@ -34,7 +34,6 @@ export default { components: { GlAvatarLabeled, GlBadge, - GlEmptyState, GlIcon, GlKeysetPagination, GlLoadingIcon, @@ -43,6 +42,7 @@ export default { GlLink, PlaceholderActions, HelpPopover, + EmptyResult, }, directives: { GlTooltip: GlTooltipDirective, @@ -150,11 +150,6 @@ export default { isSearchQueryTooShort() { return this.querySearch && this.querySearch.trim().length < MINIMUM_QUERY_LENGTH; }, - emptyText() { - return this.isSearchQueryTooShort - ? __('Enter at least three characters to search.') - : __('Edit your search and try again'); - }, }, methods: { @@ -217,7 +212,9 @@ export default { - + + diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index bd2d4915414..c24312e6abd 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -11,7 +11,6 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import initSourcegraph from '~/sourcegraph'; import ZenMode from '~/zen_mode'; import initAwardsApp from '~/emoji/awards_app'; -import { initMrExperienceSurvey } from '~/surveys/merge_request_experience'; import toast from '~/vue_shared/plugins/global_toast'; import getStateQuery from './queries/get_state.query.graphql'; import initCheckoutModal from './init_checkout_modal'; @@ -23,7 +22,6 @@ export default function initMergeRequestShow(store, pinia) { initSourcegraph(); initIssuableSidebar(); initAwardsApp(document.getElementById('js-vue-awards-block')); - initMrExperienceSurvey(); initCheckoutModal(); const el = document.querySelector('.js-mr-header'); diff --git a/app/assets/javascripts/surveys/components/satisfaction_rate.vue b/app/assets/javascripts/surveys/components/satisfaction_rate.vue deleted file mode 100644 index 62feb9d372a..00000000000 --- a/app/assets/javascripts/surveys/components/satisfaction_rate.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - {{ $options.i18n.unhappy }} - {{ $options.i18n.delighted }} - - - diff --git a/app/assets/javascripts/surveys/merge_request_experience/app.js b/app/assets/javascripts/surveys/merge_request_experience/app.js deleted file mode 100644 index 50b1c2c3f39..00000000000 --- a/app/assets/javascripts/surveys/merge_request_experience/app.js +++ /dev/null @@ -1,60 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue'; -import createDefaultClient from '~/lib/graphql'; -import Translate from '~/vue_shared/translate'; - -Vue.use(Translate); -Vue.use(VueApollo); - -export const startMrSurveyApp = () => { - const mountEl = document.querySelector('#js-mr-experience-survey'); - if (!mountEl) return; - - let channel = null; - - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); - - const { accountAge } = mountEl.dataset; - - const app = new Vue({ - apolloProvider, - data() { - return { - hidden: false, - }; - }, - render(h) { - if (this.hidden) return null; - return h(MergeRequestExperienceSurveyApp, { - props: { - accountAge: Number(accountAge), - }, - on: { - close: () => { - channel?.postMessage('close'); - app.hidden = true; - }, - rate: () => { - channel?.postMessage('close'); - }, - }, - }); - }, - }); - - app.$mount(mountEl); - - if (window.BroadcastChannel) { - channel = new BroadcastChannel('mr_survey'); - channel.addEventListener('message', ({ data }) => { - if (data === 'close') { - app.hidden = true; - channel.close(); - channel = null; - } - }); - } -}; diff --git a/app/assets/javascripts/surveys/merge_request_experience/app.vue b/app/assets/javascripts/surveys/merge_request_experience/app.vue deleted file mode 100644 index 14b9c708bbb..00000000000 --- a/app/assets/javascripts/surveys/merge_request_experience/app.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - diff --git a/app/assets/javascripts/surveys/merge_request_experience/index.js b/app/assets/javascripts/surveys/merge_request_experience/index.js deleted file mode 100644 index 8c8ed0b1d5f..00000000000 --- a/app/assets/javascripts/surveys/merge_request_experience/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Tracker } from '~/tracking/tracker'; - -const MR_SURVEY_WAIT_DURATION = 10000; - -const broadcastNotificationVisible = () => { - // We don't want to clutter up the UI by displaying the survey when broadcast message(s) - // are visible as well. - return Boolean(document.querySelector('.js-broadcast-notification-message')); -}; - -export const initMrExperienceSurvey = () => { - if (!gon.features?.mrExperienceSurvey) return; - if (!gon.current_user_id) return; - if (!Tracker.enabled()) return; - if (broadcastNotificationVisible()) return; - - setTimeout(() => { - // eslint-disable-next-line promise/catch-or-return - import('./app').then(({ startMrSurveyApp }) => { - startMrSurveyApp(); - }); - }, MR_SURVEY_WAIT_DURATION); -}; diff --git a/app/assets/javascripts/wikis/wiki_notes/components/note_body.vue b/app/assets/javascripts/wikis/wiki_notes/components/note_body.vue index 0916a864896..477fe2edb13 100644 --- a/app/assets/javascripts/wikis/wiki_notes/components/note_body.vue +++ b/app/assets/javascripts/wikis/wiki_notes/components/note_body.vue @@ -44,11 +44,6 @@ export default { }; }, computed: { - edited() { - const { createdAt, lastEditedAt } = this.updatedNote; - - return new Date(createdAt).getTime() !== new Date(lastEditedAt).getTime(); - }, editedText() { return __('Edited'); }, @@ -111,7 +106,7 @@ export default { > { instance_level? || group_level? } validates :group_id, presence: true, unless: -> { instance_level? || project_level? } - validates :project_id, :group, absence: true, if: -> { instance_level? } - validates :organization_id, presence: true, if: -> { instance_level? } + validates :project_id, :group_id, absence: true, if: -> { instance_level? } validates :type, presence: true, exclusion: BASE_CLASSES validates :type, uniqueness: { scope: :instance }, if: :instance_level? validates :type, uniqueness: { scope: :project_id }, if: :project_level? validates :type, uniqueness: { scope: :group_id }, if: :group_level? - validate :validate_belongs_to_one_of_project_group_or_organization + validate :validate_belongs_to_project_or_group scope :external_issue_trackers, -> { where(category: 'issue_tracker').active } scope :third_party_wikis, -> { where(category: 'third_party_wiki').active } @@ -813,10 +811,10 @@ module Integrations private - def validate_belongs_to_one_of_project_group_or_organization - return if [group_id, project_id, organization_id].compact.one? + def validate_belongs_to_project_or_group + return unless project_level? && group_level? - errors.add(:base, 'The integration must belong to one organization, group, or project.') + errors.add(:project_id, 'The integration cannot belong to both a project and a group') end def validate_recipients? diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 83001fafd94..c2c7f6ff4f1 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -56,7 +56,7 @@ module Users user_reached_limit_free_plan_alert: 51, # EE-only submit_license_usage_data_banner: 52, # EE-only personal_project_limitations_banner: 53, # EE-only - mr_experience_survey: 54, + # 54 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195384 # 55 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920 namespace_storage_limit_alert_warning_threshold: 56, # EE-only namespace_storage_limit_alert_alert_threshold: 57, # EE-only diff --git a/app/services/integrations/slack_installation/instance_service.rb b/app/services/integrations/slack_installation/instance_service.rb index 2d53547cd3c..6b6e2950ca2 100644 --- a/app/services/integrations/slack_installation/instance_service.rb +++ b/app/services/integrations/slack_installation/instance_service.rb @@ -18,9 +18,7 @@ module Integrations end def find_or_create_integration! - GitlabSlackApplication - .for_instance - .first_or_create!(organization_id: params[:organization_id]) # rubocop:disable CodeReuse/ActiveRecord: -- legacy use + GitlabSlackApplication.for_instance.first_or_create! end end end diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml index c002c4119aa..c1f368a33f9 100644 --- a/app/views/projects/merge_requests/_page.html.haml +++ b/app/views/projects/merge_requests/_page.html.haml @@ -111,7 +111,4 @@ #js-reviewer-drawer-portal -- if current_user && Feature.enabled?(:mr_experience_survey, current_user) - #js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } } - = render 'shared/web_ide_path' diff --git a/config/feature_flags/development/mr_experience_survey.yml b/config/feature_flags/development/mr_experience_survey.yml deleted file mode 100644 index fbf71eab8b5..00000000000 --- a/config/feature_flags/development/mr_experience_survey.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: mr_experience_survey -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90036 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366561 -milestone: '15.2' -type: development -group: group::code review -default_enabled: false diff --git a/config/feature_flags/gitlab_com_derisk/log_database_sticking_operations.yml b/config/feature_flags/gitlab_com_derisk/log_database_sticking_operations.yml new file mode 100644 index 00000000000..e4bd8429e7d --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/log_database_sticking_operations.yml @@ -0,0 +1,9 @@ +--- +name: log_database_sticking_operations +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548158 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193945 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/548413 +milestone: '18.2' +group: group::database frameworks +type: gitlab_com_derisk +default_enabled: false diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml index b8a3a3a19af..c72be317d62 100644 --- a/config/gitlab_loose_foreign_keys.yml +++ b/config/gitlab_loose_foreign_keys.yml @@ -245,6 +245,10 @@ country_access_logs: - table: users column: user_id on_delete: async_delete +dast_pre_scan_verification_steps: + - table: projects + column: project_id + on_delete: async_delete dast_pre_scan_verifications: - table: p_ci_pipelines column: ci_pipeline_id diff --git a/db/docs/batched_background_migrations/backfill_packages_debian_file_metadata_project_id.yml b/db/docs/batched_background_migrations/backfill_packages_debian_file_metadata_project_id.yml index 44e28d2990f..358d7909e74 100644 --- a/db/docs/batched_background_migrations/backfill_packages_debian_file_metadata_project_id.yml +++ b/db/docs/batched_background_migrations/backfill_packages_debian_file_metadata_project_id.yml @@ -5,4 +5,4 @@ feature_category: package_registry introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185223 milestone: '17.11' queued_migration_version: 20250320085933 -finalized_by: # version of the migration that finalized this BBM +finalized_by: '20250616235836' diff --git a/db/docs/batched_background_migrations/encrypt_missed_ci_runner_tokens.yml b/db/docs/batched_background_migrations/encrypt_missed_ci_runner_tokens.yml new file mode 100644 index 00000000000..60fe79f09a4 --- /dev/null +++ b/db/docs/batched_background_migrations/encrypt_missed_ci_runner_tokens.yml @@ -0,0 +1,11 @@ +--- +migration_job_name: EncryptMissedCiRunnerTokens +description: > + We've encrypted plain tokens and migrated to token_encrypted (in milestone 11.6), so we need to remove the runner token column + and make encryption required (currently optional). However, plain tokens remain unencrypted in gitlab_com and likely in self-managed instances. + We must encrypt these remaining tokens and nullify them before removing the column completely. +feature_category: fleet_visibility +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196265 +milestone: '18.2' +queued_migration_version: 20250630163722 +finalized_by: # to be updated diff --git a/db/docs/dast_pre_scan_verification_steps.yml b/db/docs/dast_pre_scan_verification_steps.yml index 6581200a8bb..8b8c39a85c9 100644 --- a/db/docs/dast_pre_scan_verification_steps.yml +++ b/db/docs/dast_pre_scan_verification_steps.yml @@ -8,14 +8,6 @@ description: Verification step status for DAST Profiles introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105702 milestone: '15.7' gitlab_schema: gitlab_sec -desired_sharding_key: - project_id: - references: projects - backfill_via: - parent: - foreign_key: dast_pre_scan_verification_id - table: dast_pre_scan_verifications - sharding_key: project_id - belongs_to: dast_pre_scan_verification +sharding_key: + project_id: projects table_size: small -desired_sharding_key_migration_job_name: BackfillDastPreScanVerificationStepsProjectId diff --git a/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb b/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb index 170706e1477..3baf67b78d4 100644 --- a/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb +++ b/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb @@ -6,10 +6,10 @@ class AddMultiNotNullConstraintToIntegrations < Gitlab::Database::Migration[2.3] disable_ddl_transaction! def up - add_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id, validate: false) + # No-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126 end def down - remove_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id) + # no-op to fix https://gitlab.com/gitlab-com/gl-infra/production/-/issues/20126 end end diff --git a/db/migrate/20250703161014_add_duo_settings_index_to_namespace_settings.rb b/db/migrate/20250703161014_add_duo_settings_index_to_namespace_settings.rb new file mode 100644 index 00000000000..ddca383fb76 --- /dev/null +++ b/db/migrate/20250703161014_add_duo_settings_index_to_namespace_settings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddDuoSettingsIndexToNamespaceSettings < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.2' + + TABLE = :namespace_settings + COLUMNS = [:duo_features_enabled, :lock_duo_features_enabled] + INDEX = "index_namespace_settings_on_duo_features" + + def up + add_concurrent_index TABLE, COLUMNS, + where: 'duo_features_enabled IS NOT NULL', include: :namespace_id, + name: INDEX + end + + def down + remove_concurrent_index_by_name TABLE, INDEX + end +end diff --git a/db/migrate/20250707183141_remove_not_null_integrations_constraint.rb b/db/migrate/20250707183141_remove_not_null_integrations_constraint.rb new file mode 100644 index 00000000000..33ee29718a8 --- /dev/null +++ b/db/migrate/20250707183141_remove_not_null_integrations_constraint.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveNotNullIntegrationsConstraint < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + + milestone '18.2' + + def up + remove_check_constraint :integrations, 'check_2aae034509' + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20250616235836_finalize_backfill_packages_debian_file_metadata_project_id.rb b/db/post_migrate/20250616235836_finalize_backfill_packages_debian_file_metadata_project_id.rb new file mode 100644 index 00000000000..e6660ff82e4 --- /dev/null +++ b/db/post_migrate/20250616235836_finalize_backfill_packages_debian_file_metadata_project_id.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class FinalizeBackfillPackagesDebianFileMetadataProjectId < Gitlab::Database::Migration[2.3] + milestone '18.2' + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main_cell + + def up + ensure_batched_background_migration_is_finished( + job_class_name: 'BackfillPackagesDebianFileMetadataProjectId', + table_name: :packages_debian_file_metadata, + column_name: :package_file_id, + job_arguments: [:project_id, :packages_package_files, :project_id, :package_file_id], + finalize: true + ) + end + + def down; end +end diff --git a/db/post_migrate/20250627213600_add_dast_pre_scan_verification_steps_project_id_not_null.rb b/db/post_migrate/20250627213600_add_dast_pre_scan_verification_steps_project_id_not_null.rb new file mode 100644 index 00000000000..9a7d66599db --- /dev/null +++ b/db/post_migrate/20250627213600_add_dast_pre_scan_verification_steps_project_id_not_null.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AddDastPreScanVerificationStepsProjectIdNotNull < Gitlab::Database::Migration[2.3] + milestone '18.2' + disable_ddl_transaction! + + def up + add_not_null_constraint :dast_pre_scan_verification_steps, :project_id + end + + def down + remove_not_null_constraint :dast_pre_scan_verification_steps, :project_id + end +end diff --git a/db/post_migrate/20250630163722_queue_encrypt_missed_ci_runner_tokens.rb b/db/post_migrate/20250630163722_queue_encrypt_missed_ci_runner_tokens.rb new file mode 100644 index 00000000000..b6bfc4f3934 --- /dev/null +++ b/db/post_migrate/20250630163722_queue_encrypt_missed_ci_runner_tokens.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class QueueEncryptMissedCiRunnerTokens < Gitlab::Database::Migration[2.3] + milestone '18.2' + + restrict_gitlab_migration gitlab_schema: :gitlab_ci + + MIGRATION = "EncryptMissedCiRunnerTokens" + + def up + queue_batched_background_migration( + MIGRATION, + :ci_runners, + :id + ) + end + + def down + delete_batched_background_migration(MIGRATION, :ci_runners, :id, []) + end +end diff --git a/db/schema_migrations/20250616235836 b/db/schema_migrations/20250616235836 new file mode 100644 index 00000000000..ef4dc5af2f5 --- /dev/null +++ b/db/schema_migrations/20250616235836 @@ -0,0 +1 @@ +f5e13cacb2c1c5c16064567b512406b2a2eec8fb30ed8ba9420da247af2700d3 \ No newline at end of file diff --git a/db/schema_migrations/20250627213600 b/db/schema_migrations/20250627213600 new file mode 100644 index 00000000000..d6179259903 --- /dev/null +++ b/db/schema_migrations/20250627213600 @@ -0,0 +1 @@ +3726cd79a816694256fa6d449855d4515971b99e4fa0d59bdea410f49b712dfb \ No newline at end of file diff --git a/db/schema_migrations/20250630163722 b/db/schema_migrations/20250630163722 new file mode 100644 index 00000000000..09851f27479 --- /dev/null +++ b/db/schema_migrations/20250630163722 @@ -0,0 +1 @@ +b53bc6e953754f56cd7e7e4814fe569b3986330bebd04d17fc329d642e76a7f4 \ No newline at end of file diff --git a/db/schema_migrations/20250703161014 b/db/schema_migrations/20250703161014 new file mode 100644 index 00000000000..1bbdb5b5df9 --- /dev/null +++ b/db/schema_migrations/20250703161014 @@ -0,0 +1 @@ +debcbf90aec0669ce4914fe31b2f960584380520f4966df8dab09c039ae00745 \ No newline at end of file diff --git a/db/schema_migrations/20250707183141 b/db/schema_migrations/20250707183141 new file mode 100644 index 00000000000..c79cde5700b --- /dev/null +++ b/db/schema_migrations/20250707183141 @@ -0,0 +1 @@ +06b024ebde4a483d5bf01cc65ebb9f0c4e5f36ca5fbc55675d5ac84511e15c28 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 017a4f901d0..2d59cc88524 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13352,6 +13352,7 @@ CREATE TABLE dast_pre_scan_verification_steps ( verification_errors text[] DEFAULT '{}'::text[] NOT NULL, check_type smallint DEFAULT 0 NOT NULL, project_id bigint, + CONSTRAINT check_2cf67eeb54 CHECK ((project_id IS NOT NULL)), CONSTRAINT check_cd216b95e4 CHECK ((char_length(name) <= 255)) ); @@ -29777,9 +29778,6 @@ ALTER TABLE ONLY chat_teams ALTER TABLE workspaces ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID; -ALTER TABLE integrations - ADD CONSTRAINT check_2aae034509 CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID; - ALTER TABLE security_scans ADD CONSTRAINT check_2d56d882f6 CHECK ((project_id IS NOT NULL)) NOT VALID; @@ -36683,6 +36681,8 @@ CREATE UNIQUE INDEX index_namespace_import_users_on_user_id ON namespace_import_ CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id); +CREATE INDEX index_namespace_settings_on_duo_features ON namespace_settings USING btree (duo_features_enabled, lock_duo_features_enabled) INCLUDE (namespace_id) WHERE (duo_features_enabled IS NOT NULL); + CREATE INDEX index_namespace_settings_on_namespace_id_where_archived_true ON namespace_settings USING btree (namespace_id) WHERE (archived = true); CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id); diff --git a/doc/administration/dedicated/create_instance/_index.md b/doc/administration/dedicated/create_instance/_index.md index e5b4fb3aa68..1b5f5d57cd0 100644 --- a/doc/administration/dedicated/create_instance/_index.md +++ b/doc/administration/dedicated/create_instance/_index.md @@ -13,7 +13,7 @@ title: Create your GitLab Dedicated instance {{< /details >}} -The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/saas-platforms/switchboard/), the GitLab Dedicated portal. +The instructions on this page guide you through the onboarding and initial setup of your GitLab Dedicated instance using [Switchboard](https://about.gitlab.com/direction/platforms/switchboard/), the GitLab Dedicated portal. ## Step 1: Get access to Switchboard diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index c7668e36c54..ad95cc1f752 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -8849,6 +8849,30 @@ Input type: `NamespaceSettingsUpdateInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +### `Mutation.namespacesRegenerateNewWorkItemEmailAddress` + +{{< details >}} +**Introduced** in GitLab 18.2. +**Status**: Experiment. +{{< /details >}} + +Input type: `NamespacesRegenerateNewWorkItemEmailAddressInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `fullPath` | [`ID!`](#id) | Full path of the namespace to regenerate the new work item email address for. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +| `namespace` | [`Namespace`](#namespace) | Namespace after regenerating the new work item email address. | + ### `Mutation.noteConvertToThread` Convert a standard comment to a resolvable thread. @@ -47551,7 +47575,6 @@ Name of the feature that the callout is for. | `GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. | | `JOINING_A_PROJECT_ALERT` | Callout feature name for joining_a_project_alert. | | `MERGE_REQUEST_DASHBOARD_DISPLAY_PREFERENCES_POPOVER` | Callout feature name for merge_request_dashboard_display_preferences_popover. | -| `MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. | | `NAMESPACE_OVER_STORAGE_USERS_COMBINED_ALERT` | Callout feature name for namespace_over_storage_users_combined_alert. | | `NAMESPACE_STORAGE_LIMIT_ALERT_ALERT_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_alert_threshold. | | `NAMESPACE_STORAGE_LIMIT_ALERT_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_alert_error_threshold. | diff --git a/doc/subscriptions/gitlab_dedicated/_index.md b/doc/subscriptions/gitlab_dedicated/_index.md index cd1cc2a6b21..354107436a7 100644 --- a/doc/subscriptions/gitlab_dedicated/_index.md +++ b/doc/subscriptions/gitlab_dedicated/_index.md @@ -218,7 +218,7 @@ The following GitLab application features are not available: - Service Desk - Some GitLab Duo AI capabilities - View the [list of supported AI features](../../user/ai_features.md) - - For more information, see the [Supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/platforms/dedicated/#supporting-ai-features-on-gitlab-dedicated) + - For more information, see [supporting AI Features on GitLab Dedicated](https://about.gitlab.com/direction/gitlab_dedicated/#supporting-ai-features-on-gitlab-dedicated) - Features other than [available features](#available-features) that must be configured outside of the GitLab user interface - Any functionality or feature behind a feature flag that is turned `off` by default - [Sigstore for keyless signing and verification](../../ci/yaml/signing_examples.md) diff --git a/doc/user/application_security/policies/vulnerability_management_policy.md b/doc/user/application_security/policies/vulnerability_management_policy.md index e11b3a6264b..4b4aa5cbe50 100644 --- a/doc/user/application_security/policies/vulnerability_management_policy.md +++ b/doc/user/application_security/policies/vulnerability_management_policy.md @@ -50,6 +50,10 @@ detected are marked **Resolved**. - You can assign a maximum of five rules to each policy. - You can assign a maximum of five vulnerability management policies to each security policy project. +- When a secret detection scan finds that a previously detected secret key is no longer detected, + the vulnerability is not auto-resolved. Instead, it remains in **Needs Triage** because the removed + secret key has already been exposed. The vulnerability status should be manually resolved only after + the secret key is revoked or rotated. ## Create a vulnerability management policy diff --git a/doc/user/application_security/secret_detection/secret_push_protection/_index.md b/doc/user/application_security/secret_detection/secret_push_protection/_index.md index e62ae117205..150d45a5ad6 100644 --- a/doc/user/application_security/secret_detection/secret_push_protection/_index.md +++ b/doc/user/application_security/secret_detection/secret_push_protection/_index.md @@ -69,14 +69,14 @@ push protection, to minimize the delay when pushing your commits and minimize th alerts. For example, personal access tokens that use a custom prefix are not detected by secret push protection. You can [exclude](../exclusions.md) selected secrets from detection by secret push protection. -## Enable secret push protection +## Getting started On GitLab Dedicated and GitLab Self-Managed instances, you must: 1. [Allow secret push protection on the entire instance](#allow-the-use-of-secret-push-protection-in-your-gitlab-instance). 1. Enable secret push protection. You can either: - [Enable secret push protection in a specific project](#enable-secret-push-protection-in-a-project). - - Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting). Ultimate only. + - Use the API to [enable secret push protection for all projects in group](../../../../api/group_security_settings.md#update-secret_push_protection_enabled-setting). ### Allow the use of secret push protection in your GitLab instance @@ -149,6 +149,103 @@ Secret push protection does not check a file in a commit when: Secret push protection scans only the diffs of commits pushed over HTTP(S) and SSH. If a secret is already present in a file and not part of the changes, it is not detected. +## Understanding the results + +Secret push protection can identify various categories of secrets: + +- **API keys and tokens**: Service-specific authentication credentials +- **Database connection strings**: URLs containing embedded credentials +- **Private keys**: Cryptographic keys for authentication or encryption +- **Generic high-entropy strings**: Patterns that appear to be randomly generated secrets + +When a push is blocked, secret push protection provides detailed information to help you locate and address the detected secrets: + +- **Commit ID**: The specific commit containing the secret. Useful for tracking changes in your Git history. +- **File path and line number**: The exact location of the detected pattern for quick navigation. +- **Secret type**: The classification of the detected pattern. For example, `GitLab Personal Access Token` or `AWS Access Key`. + +### Common detection categories + +Not all detections require immediate action. Consider the following when evaluating results: + +- **True positives**: Legitimate secrets that should be rotated and removed. For example: + - [Valid](../../vulnerabilities/validity_check.md) API keys or tokens + - Production database credentials + - Private cryptographic keys + - Any credentials that could grant unauthorized access + +- **False positives**: Detected patterns that aren't actual secrets. For example: + - Test data that resembles secrets but has no real-world value + - Placeholder values in configuration templates + - Example credentials in documentation + - Hash values or checksums that match secret patterns + +Document common false positive patterns in your organization to streamline future evaluations. + +## Optimization + +Before deploying secret push protection widely, optimize the configuration to reduce false positives and improve accuracy for your specific environment. + +### Reduce false positives + +False positives can significantly impact developer productivity and lead to security fatigue. + +To reduce false positives: + +- Configure [exclusions](../exclusions.md) strategically: + - Create path-based exclusions for test directories, documentation, and third party dependencies. + - Use pattern-based exclusions for known false positive patterns specific to your codebase. + - Document your exclusion rules and review them regularly. +- Create standards for placeholder values and test credentials. +- Monitor false positive rates and continue to adjust exclusions accordingly. + +### Optimize performance + +Large repositories or frequent pushes can have performance impacts. + +To optimize the performance of secret push protection: + +- Monitor push times and establish baseline metrics before deployment. +- Use diff scanning to reduce the amount of content scanned on each push. +- Consider file size limits for repositories with large binary assets. +- Implement exclusions for directories that are unlikely to contain secrets. + +### Integration with existing workflows + +Ensure secret push protection complements your existing development practices: + +- Configure pipeline secret detection and secret push protection to be sure you have defense in depth. +- Update developer documentation to include secret push protection procedures. +- Align with security training to educate developers on secure coding practices to minimize leaked secrets. + +## Roll out + +Successfully deploying secret push protection at scale requires careful planning and a phased implementation: + +1. Choose two or three non-critical projects with active development to test the feature and understand its impact on developer workflows. +1. Turn on secret push protection for your selected test projects and monitor developer feedback. +1. Document processes for handling blocked pushes and train your development teams on the new workflows. +1. Track the number of secrets detected, false positive rates, and developer experience feedback during the pilot phase. + +You should run the pilot phase for two to four weeks to gather sufficient data and identify any workflow adjustments needed before broader deployment. + +Once you have completed the pilot, consider the next three phases for a scaled rollout: + +1. Early adopters (weeks 3-6) + - Enable on 10-20% of active projects, prioritizing security-sensitive repositories. + - Focus on teams with strong security awareness and buy-in. + - Monitor performance impacts and developer experience. + - Refine processes based on real-world usage. +1. Broad deployment (weeks 7-12) + - Gradually enable across remaining projects in batches. + - Provide ongoing support and training to development teams. + - Monitor system performance and scale infrastructure if needed. + - Continue optimizing exclusion rules based on usage patterns. +1. Full coverage (weeks 13-16) + - Enable secret push protection on all remaining projects. + - Establish ongoing maintenance and review processes. + - Implement regular audits of exclusion rules and detected patterns. + ## Resolve a blocked push When secret push protection blocks a push, you can either: diff --git a/doc/user/project/merge_requests/approvals/_index.md b/doc/user/project/merge_requests/approvals/_index.md index 0c8aea3b2f2..1145ba749d0 100644 --- a/doc/user/project/merge_requests/approvals/_index.md +++ b/doc/user/project/merge_requests/approvals/_index.md @@ -51,7 +51,7 @@ To configure approval rules: You can also configure: -- Additional [merge request approval settings](settings.md) for more control of the +- More [merge request approval settings](settings.md) for more control of the level of oversight and security your project needs. - Merge request approval rules with the [Merge request approvals API](../../../../api/merge_request_approvals.md). @@ -86,8 +86,8 @@ Use cases include: {{< /history >}} -You can see the approval status of a merge request in two places. On the [merge request itself](#for-a-single-merge-request) -and in the [list of merge requests](#in-the-list-of-merge-requests) for your project or group. +To view the approval status of a merge request, check the merge request itself, or the list of +merge requests for your project or group. ### For a single merge request @@ -135,7 +135,7 @@ To see the review and approval status for each reviewer: 1. Open the merge request. 1. Check the right sidebar. -Each reviewer's status is shown next to the their name. +Each reviewer's status is shown next to their name. - {{< icon name="dash-circle" >}} Awaiting review - {{< icon name="status_running" >}} Review in progress diff --git a/doc/user/project/merge_requests/homepage.md b/doc/user/project/merge_requests/homepage.md index cef74cc3dca..3012281ad9b 100644 --- a/doc/user/project/merge_requests/homepage.md +++ b/doc/user/project/merge_requests/homepage.md @@ -72,7 +72,8 @@ This user has: - 8 active merge requests ({{< icon name="merge-request-open" >}}) - 29 to-do items ({{< icon name="todo-done" >}}) -Your merge request homepage shows more information about these merge requests. To see it, use any of these methods: +Your merge request homepage shows more information about these merge requests. To see it, +use any of these methods: - Use the Shift + m [keyboard shortcut](../../shortcuts.md). - On the left sidebar, select **Merge requests** ({{< icon name="merge-request-open">}}). @@ -106,12 +107,13 @@ into three tabs: ### Set your display preferences -On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}}) -to sort your merge requests by: +On the top right of your merge request homepage, select **Display preferences** ({{< icon name="preferences">}}): -- **Workflow**. This view groups merge requests by their status. GitLab shows the merge requests needing your attention - first, regardless of whether you are the author or the reviewer. -- **Role**. This view groups merge requests by whether you are the reviewer or the author. +- Toggle **Show labels** to show or hide labels for each merge request. +- Sorting preferences: **Workflow** or **Role**. + - **Workflow** groups merge requests by their status. GitLab shows the merge requests + needing your attention first, regardless of whether you are the author or the reviewer. + - **Role** groups merge requests by whether you are the reviewer or the author. Active merge requests count toward the total shown on the left sidebar. GitLab excludes **Inactive** merge requests from your review count. @@ -124,12 +126,13 @@ These merge requests need your attention. They count toward the total shown on t Statuses: - **Draft**: The merge request is a draft. - **Reviewers needed**: The merge request is not a draft, but has no reviewers. -- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally, approve or request changes. - Statuses: - - **Changes requested**: A reviewer has requested changes. +- **Review requested**: You're a reviewer. Review the merge request. Provide feedback. Optionally, + approve or request changes. Statuses: + - **Changes requested**: A reviewer has requested changes. The change request blocks the merge request, + but [can be bypassed](reviews/_index.md#bypass-a-request-for-changes). - **Reviewer commented**: A reviewer has left comments but not requested changes. -- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments, and implement suggested changes. - Statuses: +- **Returned to you**: Reviewers have provided feedback, or requested changes. Address reviewer comments, + and apply suggested changes. Statuses: - **Changes requested**: A reviewer has requested changes. - **Reviewer commented**: A reviewer has left comments but not requested changes. @@ -137,12 +140,12 @@ These merge requests need your attention. They count toward the total shown on t GitLab excludes these merge requests from the active count, because no action is required from you right now: -- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're the reviewer, you've requested changes. - Statuses: +- **Waiting for assignee**: If you're the author, the merge request is awaiting review. If you're + the reviewer, you've requested changes. Statuses: - **You requested changes**: You've completed your review and requested changes. - **You commented**: You've commented, but have not completed your review. -- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews you have requested changes for. - Statuses: +- **Waiting for approvals**: Your assigned merge requests that are waiting for approvals, and reviews + you have requested changes for. Statuses: - **Approvals required**: Number of required approvals remaining. - **Approved**: Either you have approved, or all required approvals are satisfied. - **Waiting for approvals**. @@ -150,8 +153,8 @@ GitLab excludes these merge requests from the active count, because no action is Statuses: - **Approved**: You've approved, and required approvals are satisfied. - **Approval required**: You've approved, but not all required approvals are satisfied. -- **Approved by others**: Merge requests that have received approvals from other team members. Potentially ready to merge, if all requirements are met. - Statuses: +- **Approved by others**: Merge requests that have received approvals from other team members. + Potentially ready to merge, if all requirements are met. Statuses: - **Approved**: Your merge request has received the necessary approvals. ### Role view diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 65279fd1fbb..a668a9fee5d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -268,7 +268,7 @@ module API def find_namespace(id) if INTEGER_ID_REGEX.match?(id.to_s) # We need to stick to an up-to-date replica or primary db here in order to properly observe the namespace - # recently created by GitlabSubscriptions::Trials::CreateService#create_group_flow. + # recently created by GitlabSubscriptions::Trials::UltimateCreateService. # See https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/9808 ::Namespace.sticking.find_caught_up_replica(:namespace, id) diff --git a/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens.rb b/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens.rb new file mode 100644 index 00000000000..190c36fcf28 --- /dev/null +++ b/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class EncryptMissedCiRunnerTokens < BatchedMigrationJob + operation_name :encrypt_missed_ci_runner_tokens + feature_category :fleet_visibility + + def perform + each_sub_batch( + batching_scope: ->(relation) { relation.where.not(token: nil).where(token_encrypted: nil) } + ) do |runners_to_encrypt| + token_encrypted_cases = [] + runner_ids = [] + + runners_to_encrypt.pluck(:id, :token).each do |id, token| + encrypted_token = encode(token) + + token_encrypted_cases << + "WHEN id = #{connection.quote(id)} THEN #{connection.quote(encrypted_token)}" + runner_ids << connection.quote(id) + end + + next if token_encrypted_cases.empty? + + connection.execute( + <<~SQL + UPDATE ci_runners + SET token_encrypted = CASE + #{token_encrypted_cases.join(' ')} + END, + token = NULL + WHERE id IN (#{runner_ids.join(',')}) + SQL + ) + end + end + + private + + def encode(token) + Authn::TokenField::EncryptionHelper.encrypt_token(token) + end + end + end +end diff --git a/lib/gitlab/database/load_balancing/sticking.rb b/lib/gitlab/database/load_balancing/sticking.rb index 744c215d16e..8260893f0dd 100644 --- a/lib/gitlab/database/load_balancing/sticking.rb +++ b/lib/gitlab/database/load_balancing/sticking.rb @@ -47,6 +47,7 @@ module Gitlab def stick(namespace, id) with_primary_write_location do |location| set_write_location_for(namespace, id, location) + capture_stick_logs(namespace, id, location) end use_primary! end @@ -56,13 +57,31 @@ module Gitlab ids.each do |id| set_write_location_for(namespace, id, location) end + capture_stick_logs(namespace, ids, location) end - use_primary! end private + def log_database_sticking_operations_enabled? + Feature.enabled?(:log_database_sticking_operations, Feature.current_pod) + end + + def capture_stick_logs(namespace, ids, location) + return unless log_database_sticking_operations_enabled? + return unless namespace.to_sym == :user + + id = Array(ids).first # Only log the first ID to reduce log volume for bulk operations + + ::Gitlab::Database::LoadBalancing::Logger.info( + event: :load_balancer_stick_logging, + client_id: "#{namespace}/#{id}", + stick_id: id, + stick_type: namespace, + current_lsn: location) + end + def with_primary_write_location # When only using the primary, there's no point in getting write # locations, as the primary is always in sync with itself. diff --git a/lib/gitlab/email/common.rb b/lib/gitlab/email/common.rb index 01316995c4d..bf5300ca037 100644 --- a/lib/gitlab/email/common.rb +++ b/lib/gitlab/email/common.rb @@ -31,6 +31,8 @@ module Gitlab enabled? && supports_wildcard? end + alias_method :supports_work_item_creation?, :supports_issue_creation? + def reply_address(key) incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, key) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ca6cee54475..280975af654 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3530,9 +3530,6 @@ msgstr "" msgid "Activate Service Desk" msgstr "" -msgid "Activate my trial" -msgstr "" - msgid "Activated" msgstr "" @@ -26623,6 +26620,9 @@ msgstr "" msgid "Failed to refresh compliance requirement status. Error: %{error_message}" msgstr "" +msgid "Failed to regenerate new work item email address" +msgstr "" + msgid "Failed to regenerate unique domain" msgstr "" @@ -40391,21 +40391,6 @@ msgstr "" msgid "MrReports|Reports" msgstr "" -msgid "MrSurvey|By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the %{linkStart}GitLab Privacy Policy%{linkEnd}." -msgstr "" - -msgid "MrSurvey|How satisfied are you with %{strongStart}speed/performance%{strongEnd} of merge requests?" -msgstr "" - -msgid "MrSurvey|Merge request experience survey" -msgstr "" - -msgid "MrSurvey|Overall, how satisfied are you with merge requests?" -msgstr "" - -msgid "MrSurvey|Thank you for your feedback!" -msgstr "" - msgid "Multi-project" msgstr "" @@ -61448,21 +61433,6 @@ msgstr "" msgid "Support page URL" msgstr "" -msgid "Surveys|Delighted" -msgstr "" - -msgid "Surveys|Happy" -msgstr "" - -msgid "Surveys|Neutral" -msgstr "" - -msgid "Surveys|Sad" -msgstr "" - -msgid "Surveys|Unhappy" -msgstr "" - msgid "Switch Branches" msgstr "" @@ -65613,24 +65583,12 @@ msgstr "" msgid "TrialWidget|Your trial of Ultimate with GitLab Duo Enterprise has ended" msgstr "" -msgid "Trials|Create a new group and start your free trial." -msgstr "" - -msgid "Trials|You can apply your free trial to a group." -msgstr "" - msgid "Trial|Activate my trial" msgstr "" msgid "Trial|Allowed characters: +, 0-9, -, and spaces." msgstr "" -msgid "Trial|Apply your trial to a new group" -msgstr "" - -msgid "Trial|Apply your trial to a new or existing group" -msgstr "" - msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}" msgstr "" @@ -65661,9 +65619,6 @@ msgstr "" msgid "Trial|Start your free trial" msgstr "" -msgid "Trial|Start your free trial on %{group_name}" -msgstr "" - msgid "Trial|State or province" msgstr "" @@ -70306,6 +70261,12 @@ msgstr "" msgid "Work item" msgstr "" +msgid "Work item creation via email is not supported" +msgstr "" + +msgid "Work item creation via email is only supported for projects" +msgstr "" + msgid "Work item not supported" msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index 54ab3af0902..d182f125872 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -27,7 +27,6 @@ gem 'zeitwerk', '~> 2.7', '>= 2.7.3' gem 'influxdb-client', '~> 3.2' gem 'terminal-table', '~> 4.0.0', require: false gem 'slack-notifier', '~> 2.4', require: false -gem 'googleauth', '~> 1.9.0', require: false # see: https://gitlab.com/gitlab-org/gitlab/-/issues/449019 gem 'fog-google', '~> 1.25', require: false gem "warning", "~> 1.5" diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 6fa9be63f7c..38249fba31e 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -175,11 +175,14 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-storage_v1 (0.40.0) google-apis-core (>= 0.15.0, < 2.a) - google-cloud-env (2.1.1) + google-cloud-env (2.3.1) + base64 (~> 0.2) faraday (>= 1.0, < 3.a) - googleauth (1.9.2) + google-logging-utils (0.2.0) + googleauth (1.14.0) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -386,7 +389,6 @@ DEPENDENCIES gitlab-qa (~> 15, >= 15.5.0) gitlab-utils! gitlab_quality-test_tooling (~> 2.15.3) - googleauth (~> 1.9.0) influxdb-client (~> 3.2) junit_merge (~> 0.1.2) knapsack (~> 4.0) diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index dd94fa204bd..e5553dc46bb 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -97,11 +97,11 @@ module QA click_element 'expand-replies-button' end - def has_comment?(comment_text) + def has_comment?(comment_text, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) has_element?( 'noteable-note-container', text: comment_text, - wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME + wait: wait ) end @@ -167,13 +167,13 @@ module QA def start_review_with_comment(text) fill_editor_element('comment-field', text) click_element('start-review-button') - has_comment?(text) + has_comment?(text, 3) end def add_comment_to_review(text) fill_editor_element('comment-field', text) click_element('add-to-review-button') - has_comment?(text) + has_comment?(text, 3) end def toggle_comments(position) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index aa0b6b508ef..d558bf58514 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -180,8 +180,14 @@ module QA end end - all_elements('review-drawer-toggle', minimum: 1).first.click - click_element('submit-review-button') + retry_until(sleep_interval: 2, max_attempts: 5, message: "Retry closing review drawer") do + if has_element?('review-drawer-toggle', wait: 2) + all_elements('review-drawer-toggle', minimum: 1).first.click + end + + click_element('submit-review-button') + has_no_element?('review-drawer-toggle', wait: 2) + end # After clicking the button, wait for the review bar to disappear # before moving on to the next part of the test diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index c7005fd1237..177cc643ba0 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -15,6 +15,7 @@ module QA HTTP_STATUS_NOT_FOUND = 404 HTTP_STATUS_TOO_MANY_REQUESTS = 429 HTTP_STATUS_SERVER_ERROR = 500 + HTTP_STATUS_SERVICE_UNAVAILABLE = 503 def post(url, payload, args = {}) request_args = { @@ -197,7 +198,10 @@ module QA end def fatal_response?(response_code) - [HTTP_STATUS_UNAUTHORIZED, HTTP_STATUS_SERVER_ERROR, HTTP_STATUS_BAD_REQUEST].include?(response_code) + [HTTP_STATUS_UNAUTHORIZED, + HTTP_STATUS_SERVER_ERROR, + HTTP_STATUS_BAD_REQUEST, + HTTP_STATUS_SERVICE_UNAVAILABLE].include?(response_code) end end end diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb index 1faa61cc0cc..5b8ee305acd 100644 --- a/spec/controllers/admin/integrations_controller_spec.rb +++ b/spec/controllers/admin/integrations_controller_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Admin::IntegrationsController, :with_current_organization, featur end it_behaves_like Integrations::Actions do - let(:integration_attributes) { { instance: true, project: nil, organization: organization } } + let(:integration_attributes) { { instance: true, project: nil } } let(:routing_params) do { id: integration.to_param } diff --git a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb index de613bfefcb..212862654e0 100644 --- a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb +++ b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb @@ -23,7 +23,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", end context 'when the integration is active for the instance', :enable_admin_mode do - let(:instance_integration) { create(:beyond_identity_integration, :instance) } + let(:instance_integration) { create :beyond_identity_integration } before do ::Integrations::PropagateService.new(instance_integration).execute @@ -126,7 +126,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", end context 'and the integration is activated for the instance' do - let(:instance_integration) { create(:beyond_identity_integration, :instance) } + let(:instance_integration) { create :beyond_identity_integration } before do ::Integrations::PropagateService.new(instance_integration).execute @@ -144,7 +144,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", it { expect(project.reload.beyond_identity_integration).not_to be_activated } context 'and the integration is activated for the instance' do - let(:instance_integration) { create(:beyond_identity_integration, :instance) } + let(:instance_integration) { create :beyond_identity_integration } before do ::Integrations::PropagateService.new(instance_integration).execute diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb index ef067ea6818..b9c61938b85 100644 --- a/spec/features/markdown/sandboxed_mermaid_spec.rb +++ b/spec/features/markdown/sandboxed_mermaid_spec.rb @@ -75,12 +75,18 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js, feature_category: :markdown d project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) end - it 'includes mermaid frame correctly', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/553529' do + it 'includes mermaid frame correctly' do visit(project_path(project)) - wait_for_requests + wait_for_all_requests - expect(page.html).to include(expected) + page.within '.js-wiki-content' do + # the find is needed to ensure the lazy container is loaded, otherwise + # it can be a flaky test, similar to + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408 + # + expect(page.html).to include(expected) + end end end diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index b217bcced12..a36400d7f7c 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -80,7 +80,7 @@ RSpec.describe 'User Settings > GPG keys', feature_category: :user_profile do end context 'when external verification is required' do - let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) } + let!(:beyond_identity_integration) { create(:beyond_identity_integration) } let!(:gpg_key) do create :gpg_key, externally_verified: externally_verified, user: user, key: GpgHelpers::User2.public_key end diff --git a/spec/frontend/admin/projects/index/components/app_spec.js b/spec/frontend/admin/projects/index/components/app_spec.js index 0495c52be47..dd7cb91b42b 100644 --- a/spec/frontend/admin/projects/index/components/app_spec.js +++ b/spec/frontend/admin/projects/index/components/app_spec.js @@ -5,6 +5,7 @@ import { programmingLanguages } from 'jest/groups_projects/components/mock_data' import { FILTERED_SEARCH_TOKEN_LANGUAGE, FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, PAGINATION_TYPE_KEYSET, } from '~/groups_projects/constants'; import { @@ -45,6 +46,7 @@ describe('AdminProjectsApp', () => { filteredSearchSupportedTokens: [ FILTERED_SEARCH_TOKEN_LANGUAGE, FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, ], filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE, diff --git a/spec/frontend/groups_projects/components/tabs_with_list_spec.js b/spec/frontend/groups_projects/components/tabs_with_list_spec.js index a36781b9101..cacf67c2f7c 100644 --- a/spec/frontend/groups_projects/components/tabs_with_list_spec.js +++ b/spec/frontend/groups_projects/components/tabs_with_list_spec.js @@ -27,6 +27,7 @@ import { SORT_DIRECTION_ASC, PAGINATION_TYPE_KEYSET, PAGINATION_TYPE_OFFSET, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, } from '~/groups_projects/constants'; import { RECENT_SEARCHES_STORAGE_KEY_PROJECTS } from '~/filtered_search/recent_searches_storage_keys'; import { @@ -50,6 +51,7 @@ import { import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; +import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants'; import { programmingLanguages } from './mock_data'; jest.mock('~/alert'); @@ -67,6 +69,7 @@ const defaultPropsData = { filteredSearchSupportedTokens: [ FILTERED_SEARCH_TOKEN_LANGUAGE, FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL, + FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL, ], filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, filteredSearchNamespace: FILTERED_SEARCH_NAMESPACE, @@ -271,6 +274,7 @@ describe('TabsWithList', () => { [defaultPropsData.filteredSearchTermKey]: searchTerm, [FILTERED_SEARCH_TOKEN_LANGUAGE]: ['5'], [FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ['50'], + [FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: [VISIBILITY_LEVEL_PRIVATE_STRING], }); await waitForPromises(); }); @@ -279,6 +283,7 @@ describe('TabsWithList', () => { expect(successHandler).toHaveBeenCalledWith({ minAccessLevel: 'OWNER', programmingLanguageName: 'CSS', + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING, search: searchTerm, skipContributed: false, skipStarred: true, @@ -553,6 +558,7 @@ describe('TabsWithList', () => { [defaultPropsData.filteredSearchTermKey]: 'foo', [FILTERED_SEARCH_TOKEN_LANGUAGE]: '8', [FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: ACCESS_LEVEL_OWNER_INTEGER, + [FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_STRING, [QUERY_PARAM_END_CURSOR]: mockEndCursor, [QUERY_PARAM_START_CURSOR]: mockStartCursor, }; @@ -578,6 +584,7 @@ describe('TabsWithList', () => { [defaultPropsData.filteredSearchTermKey]: query[defaultPropsData.filteredSearchTermKey], [FILTERED_SEARCH_TOKEN_LANGUAGE]: query[FILTERED_SEARCH_TOKEN_LANGUAGE], [FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL]: query[FILTERED_SEARCH_TOKEN_MIN_ACCESS_LEVEL], + [FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL]: query[FILTERED_SEARCH_TOKEN_VISIBILITY_LEVEL], }, filtersAsQueryVariables: { programmingLanguageName: 'CoffeeScript', diff --git a/spec/frontend/members/placeholders/components/placeholders_table_spec.js b/spec/frontend/members/placeholders/components/placeholders_table_spec.js index eb502f8f1f9..9005d63e9b5 100644 --- a/spec/frontend/members/placeholders/components/placeholders_table_spec.js +++ b/spec/frontend/members/placeholders/components/placeholders_table_spec.js @@ -3,14 +3,7 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import { mount, shallowMount } from '@vue/test-utils'; -import { - GlAvatarLabeled, - GlBadge, - GlEmptyState, - GlKeysetPagination, - GlLoadingIcon, - GlTable, -} from '@gitlab/ui'; +import { GlAvatarLabeled, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTable } from '@gitlab/ui'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import createMockApollo from 'helpers/mock_apollo_helper'; import { stubComponent } from 'helpers/stub_component'; @@ -30,6 +23,7 @@ import { } from '~/import_entities/import_groups/constants'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import EmptyResult from '~/vue_shared/components/empty_result.vue'; import importSourceUsersQuery from '~/members/placeholders/graphql/queries/import_source_users.query.graphql'; @@ -93,7 +87,7 @@ describe('PlaceholdersTable', () => { }); }; - const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findEmptyState = () => wrapper.findComponent(EmptyResult); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPagination = () => wrapper.findComponent(GlKeysetPagination); const findTable = () => wrapper.findComponent(GlTable); @@ -363,9 +357,8 @@ describe('PlaceholdersTable', () => { }); it('renders empty state with short query description', () => { - expect(findEmptyState().props('description')).toBe( - 'Enter at least three characters to search.', - ); + expect(findEmptyState().exists()).toBe(true); + expect(findEmptyState().props('searchMinimumLength')).toBe(3); }); }); @@ -382,7 +375,8 @@ describe('PlaceholdersTable', () => { }); it('renders empty state with no results message', () => { - expect(findEmptyState().props('description')).toBe('Edit your search and try again'); + expect(findEmptyState().exists()).toBe(true); + expect(findEmptyState().props('searchMinimumLength')).toBe(null); }); }); diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js deleted file mode 100644 index d03451c71a8..00000000000 --- a/spec/frontend/surveys/merge_request_performance/app_spec.js +++ /dev/null @@ -1,212 +0,0 @@ -import { nextTick } from 'vue'; -import { GlButton, GlSprintf } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { mockTracking } from 'helpers/tracking_helper'; -import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; -import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue'; -import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue'; - -const createRenderTrackedArguments = () => [ - undefined, - 'survey:mr_experience', - { - label: 'render', - extra: { - accountAge: 0, - }, - }, -]; - -describe('MergeRequestExperienceSurveyApp', () => { - let trackingSpy; - let wrapper; - let dismiss; - let dismisserComponent; - - const findCloseButton = () => - wrapper - .findAllComponents(GlButton) - .filter((button) => button.attributes('aria-label') === 'Close') - .at(0); - - const createWrapper = ({ shouldShowCallout = true } = {}) => { - dismiss = jest.fn(); - dismisserComponent = makeMockUserCalloutDismisser({ - dismiss, - shouldShowCallout, - }); - trackingSpy = mockTracking(undefined, undefined, jest.spyOn); - wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, { - propsData: { - accountAge: 0, - }, - stubs: { - UserCalloutDismisser: dismisserComponent, - GlSprintf, - }, - }); - }; - - beforeEach(() => { - localStorage.clear(); - }); - - describe('when user callout is visible', () => { - beforeEach(() => { - createWrapper(); - }); - - it('shows survey', () => { - expect(wrapper.html()).toContain('Overall, how satisfied are you with merge requests?'); - expect(wrapper.findComponent(SatisfactionRate).exists()).toBe(true); - expect(wrapper.emitted().close).toBe(undefined); - }); - - it('tracks render once', () => { - expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments()); - }); - - it("doesn't track subsequent renders", () => { - createWrapper(); - expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments()); - expect(trackingSpy).toHaveBeenCalledTimes(1); - }); - - describe('when close button clicked', () => { - beforeEach(() => { - findCloseButton().vm.$emit('click'); - }); - - it('triggers user callout on close', () => { - expect(dismiss).toHaveBeenCalledTimes(1); - }); - - it('emits close event on close button click', () => { - expect(wrapper.emitted()).toMatchObject({ close: [[]] }); - }); - - it('tracks dismissal', () => { - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { - label: 'dismiss', - extra: { - accountAge: 0, - }, - }); - }); - - it('tracks subsequent renders', () => { - createWrapper(); - expect(trackingSpy.mock.calls).toEqual([ - createRenderTrackedArguments(), - expect.anything(), - createRenderTrackedArguments(), - ]); - }); - }); - - it('applies correct feature name for user callout', () => { - expect(wrapper.findComponent(dismisserComponent).props('featureName')).toBe( - 'mr_experience_survey', - ); - }); - - it('dismisses user callout on survey rate', () => { - const rate = wrapper.findComponent(SatisfactionRate); - expect(dismiss).not.toHaveBeenCalled(); - rate.vm.$emit('rate', 5); - expect(dismiss).toHaveBeenCalledTimes(1); - }); - - it('steps through survey steps', async () => { - const rate = wrapper.findComponent(SatisfactionRate); - rate.vm.$emit('rate', 5); - await nextTick(); - expect(wrapper.text()).toContain( - 'How satisfied are you with speed/performance of merge requests?', - ); - }); - - it('tracks survey rates', () => { - const rate = wrapper.findComponent(SatisfactionRate); - rate.vm.$emit('rate', 5); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { - value: 5, - label: 'overall', - extra: { - accountAge: 0, - }, - }); - rate.vm.$emit('rate', 4); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { - value: 4, - label: 'performance', - extra: { - accountAge: 0, - }, - }); - }); - - it('shows legal note', () => { - expect(wrapper.text()).toContain( - 'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.', - ); - }); - - it('hides legal note after first step', async () => { - const rate = wrapper.findComponent(SatisfactionRate); - rate.vm.$emit('rate', 5); - await nextTick(); - expect(wrapper.text()).not.toContain( - 'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.', - ); - }); - - it('shows disappearing thanks message', async () => { - const rate = wrapper.findComponent(SatisfactionRate); - rate.vm.$emit('rate', 5); - await nextTick(); - rate.vm.$emit('rate', 5); - await nextTick(); - expect(wrapper.text()).toContain('Thank you for your feedback!'); - expect(wrapper.emitted()).toMatchObject({}); - jest.runOnlyPendingTimers(); - expect(wrapper.emitted()).toMatchObject({ close: [[]] }); - }); - }); - - describe('when user callout is hidden', () => { - beforeEach(() => { - createWrapper({ shouldShowCallout: false }); - }); - - it('emits close event', () => { - expect(wrapper.emitted()).toMatchObject({ close: [[]] }); - }); - - it("doesn't track anything", () => { - expect(trackingSpy).toHaveBeenCalledTimes(0); - }); - }); - - describe('when Escape key is pressed', () => { - beforeEach(() => { - createWrapper(); - const event = new KeyboardEvent('keyup', { key: 'Escape' }); - document.dispatchEvent(event); - }); - - it('emits close event', () => { - expect(wrapper.emitted()).toMatchObject({ close: [[]] }); - expect(dismiss).toHaveBeenCalledTimes(1); - }); - - it('tracks dismissal', () => { - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { - label: 'dismiss', - extra: { - accountAge: 0, - }, - }); - }); - }); -}); diff --git a/spec/frontend/wikis/notes/components/note_body_spec.js b/spec/frontend/wikis/notes/components/note_body_spec.js index d7220f78d71..5e877c0cf41 100644 --- a/spec/frontend/wikis/notes/components/note_body_spec.js +++ b/spec/frontend/wikis/notes/components/note_body_spec.js @@ -42,16 +42,16 @@ describe('NoteBody', () => { expect(renderGFM).toHaveBeenCalled(); }); - it('should not render "Edited" text when lastEdited is the same as createdAt', () => { + it('should not render "Edited" text when lastEditedBy is not present', () => { const editedComponent = wrapper.findComponent(NoteEditedText); expect(editedComponent.exists()).toBe(false); }); - it('should render "Edited" text when lastEditedAt is not the same as createdAt', () => { + it('should render "Edited" text when lastEditedBy is present', () => { // remounting to trigger mounted function wrapper = createWrapper({ - note: { ...note, lastEditedAt: '2024-11-11T08:11:34Z' }, + note: { ...note, lastEditedBy: { name: 'user', webPath: '/user' } }, noteableId, }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 371e96bdb64..65167f2be5f 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1917,7 +1917,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do it { is_expected.to be_falsey } context 'when beyond identity is disabled for a project' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) } + let_it_be(:integration) { create(:beyond_identity_integration, active: false) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1927,7 +1927,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when a GPG key failed external validation and one GPC key is externally validated' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:integration) { create(:beyond_identity_integration) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1939,7 +1939,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when there are no GPG keys externally validated' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:integration) { create(:beyond_identity_integration) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1951,7 +1951,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when GPG keys are missing' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:integration) { create(:beyond_identity_integration) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) diff --git a/spec/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens_spec.rb b/spec/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens_spec.rb new file mode 100644 index 00000000000..34e71db7844 --- /dev/null +++ b/spec/lib/gitlab/background_migration/encrypt_missed_ci_runner_tokens_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::EncryptMissedCiRunnerTokens, + migration: :gitlab_ci, feature_category: :fleet_visibility do + let(:connection) { Ci::ApplicationRecord.connection } + + describe '#perform' do + let(:runners) { table(:ci_runners, primary_key: :id) } + + let(:args) do + min, max = runners.pick('MIN(id)', 'MAX(id)') + { + start_id: min, + end_id: max, + batch_table: 'ci_runners', + batch_column: 'id', + sub_batch_size: 100, + pause_ms: 0, + connection: connection + } + end + + let!(:runner_with_plain_token) { runners.create!(runner_type: 1, token: 'plain_token') } + let!(:runner_without_token) { runners.create!(runner_type: 1, token: nil, token_encrypted: nil) } + let!(:runner_with_encrypted_token) do + runners.create!(runner_type: 1, token: nil, + token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt(SecureRandom.hex(32))) + end + + let!(:runner_with_plain_and_encrypted_token) do + runners.create!(runner_type: 1, token: 'plain_token2', + token_encrypted: Gitlab::CryptoHelper.aes256_gcm_encrypt('encrypted_token2')) + end + + subject(:perform_migration) { described_class.new(**args).perform } + + it 'encrypts plain tokens', :aggregate_failures do + expect { perform_migration } + .to change { runner_with_plain_token.reload.token_encrypted } + .from(nil).to(Authn::TokenField::EncryptionHelper.encrypt_token('plain_token')) + .and change { runner_with_plain_token.reload.token }.to(nil) + .and not_change { runner_with_encrypted_token.reload.token } + .and not_change { runner_with_encrypted_token.reload.token_encrypted } + .and not_change { runner_with_plain_and_encrypted_token.reload.token } + .and not_change { runner_with_plain_and_encrypted_token.reload.token_encrypted } + .and not_change { runner_without_token.reload.token } + .and not_change { runner_without_token.reload.token_encrypted } + end + end +end diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb index 47c73707042..e9e9e9189e6 100644 --- a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb +++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb @@ -3,17 +3,10 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do - let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') } - - let(:namespaces_table) { table(:namespaces) } - let(:group1) { namespaces_table.create!(name: 'group1', path: 'group1', organization_id: organization.id) } - let(:group2) { namespaces_table.create!(name: 'group2', path: 'group2', organization_id: organization.id) } - let(:group3) { namespaces_table.create!(name: 'group3', path: 'group3', organization_id: organization.id) } - let(:integrations_table) { table(:integrations) } - let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService', group_id: group1.id) } - let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService', group_id: group2.id) } - let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService', group_id: group3.id) } + let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') } + let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') } + let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') } let(:table_name) { :jira_tracker_data } let(:batch_column) { :id } diff --git a/spec/lib/gitlab/beyond_identity/client_spec.rb b/spec/lib/gitlab/beyond_identity/client_spec.rb index e4d1b281657..f0a3e11ed2a 100644 --- a/spec/lib/gitlab/beyond_identity/client_spec.rb +++ b/spec/lib/gitlab/beyond_identity/client_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Gitlab::BeyondIdentity::Client, feature_category: :source_code_management do - let_it_be_with_reload(:integration) { create(:beyond_identity_integration, :instance) } + let_it_be_with_reload(:integration) { create(:beyond_identity_integration) } let(:stubbed_response) do { 'authorized' => true }.to_json diff --git a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb index 3fd8795e966..e2b72d87c56 100644 --- a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb +++ b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_category: :source_code_management do include_context 'changes access checks context' let(:integration_check) { Gitlab::Checks::IntegrationsCheck.new(changes_access) } - let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) } + let!(:beyond_identity_integration) { create(:beyond_identity_integration) } subject(:check) { described_class.new(integration_check) } @@ -65,7 +65,7 @@ RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_catego context 'when service accounts are excluded' do let!(:beyond_identity_integration) do - create(:beyond_identity_integration, :instance, exclude_service_accounts: true) + create(:beyond_identity_integration, exclude_service_accounts: true) end it 'does not raise an error' do diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb index e9782f214c7..5462778fd62 100644 --- a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb @@ -2,10 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do +RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis, feature_category: :database do let(:load_balancer) { ActiveRecord::Base.load_balancer } let(:primary_write_location) { 'the-primary-lsn' } let(:last_write_location) { 'the-last-write-lsn' } + let(:namespace) { 'user' } + let(:location) { '0/1234ABED' } let(:sticking) do described_class.new(load_balancer) @@ -141,4 +143,88 @@ RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do subject { sticking.bulk_stick(:user, ids) } end end + + describe '#log_database_sticking_operations_enabled?' do + it 'returns true when the feature flag is enabled' do + stub_feature_flags(log_database_sticking_operations: true) + expect(sticking.send(:log_database_sticking_operations_enabled?)).to be true + end + + it 'returns false when the feature flag is disabled' do + stub_feature_flags(log_database_sticking_operations: false) + expect(sticking.send(:log_database_sticking_operations_enabled?)).to be false + end + end + + describe '#capture_stick_logs' do + let(:id) { 123 } + let(:ids) { [124, 125, 16] } + + context 'when logging is enabled and namespace is user' do + before do + stub_feature_flags(log_database_sticking_operations: true) + + allow(sticking).to receive(:with_primary_write_location).and_yield(location) + allow(sticking).to receive(:set_write_location_for) + allow(sticking).to receive(:use_primary!) + end + + it 'logs the sticking operation with correct parameters' do + expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with( + event: :load_balancer_stick_logging, + client_id: "#{namespace}/#{id}", + stick_id: id, + stick_type: namespace, + current_lsn: location + ) + sticking.stick(namespace, id) + end + + it 'logs only the first ID for bulk sticking operations with correct parameters' do + expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:info).with( + event: :load_balancer_stick_logging, + client_id: "#{namespace}/#{ids.first}", + stick_id: ids.first, + stick_type: namespace, + current_lsn: location + ) + + sticking.bulk_stick(namespace, ids) + end + end + + context 'when logging is disabled' do + before do + stub_feature_flags(log_database_sticking_operations: false) + + allow(sticking).to receive(:with_primary_write_location).and_yield(location) + allow(sticking).to receive(:set_write_location_for) + allow(sticking).to receive(:use_primary!) + end + + it 'does not log anything' do + expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info) + + sticking.stick(namespace, id) + end + end + + context 'when logging is enabled and namespace is not user' do + let(:namespace) { 'project' } + + before do + stub_feature_flags(log_database_sticking_operations: true) + + allow(sticking).to receive(:with_primary_write_location).and_yield(location) + allow(sticking).to receive(:set_write_location_for) + allow(sticking).to receive(:use_primary!) + end + + it 'does not log anything' do + expect(::Gitlab::Database::LoadBalancing::Logger).not_to receive(:info) + + sticking.bulk_stick(namespace, ids) + end + end + end end diff --git a/spec/migrations/20250630163722_queue_encrypt_missed_ci_runner_tokens_spec.rb b/spec/migrations/20250630163722_queue_encrypt_missed_ci_runner_tokens_spec.rb new file mode 100644 index 00000000000..58d0f6ae385 --- /dev/null +++ b/spec/migrations/20250630163722_queue_encrypt_missed_ci_runner_tokens_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueEncryptMissedCiRunnerTokens, migration: :gitlab_ci, feature_category: :fleet_visibility do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + gitlab_schema: :gitlab_ci, + table_name: :ci_runners, + column_name: :id + ) + } + end + end +end diff --git a/spec/migrations/cleanup_untethered_integrations_spec.rb b/spec/migrations/cleanup_untethered_integrations_spec.rb new file mode 100644 index 00000000000..d5124c9c975 --- /dev/null +++ b/spec/migrations/cleanup_untethered_integrations_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe CleanupUntetheredIntegrations, feature_category: :integrations do + let(:organizations) { table(:organizations) } + let(:integrations) { table(:integrations) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + let!(:default_organization) { organizations.create!(id: 1, name: 'Default', path: 'default') } + let!(:other_organization) { organizations.create!(id: 2, name: 'Other', path: 'other') } + let!(:group) { namespaces.create!(name: 'bar', path: 'bar', type: 'Group', organization_id: 1) } + + let!(:project) do + projects.create!( + name: 'project', + path: 'project', + namespace_id: group.id, + organization_id: 1, + project_namespace_id: group.id + ) + end + + let!(:untethered_integration) do + integrations.create!( + instance: false, + group_id: nil, + project_id: nil, + type_new: 'Integrations::MockMonitoring' + ) + end + + let!(:another_untethered_integration) do + integrations.create!( + instance: false, + group_id: nil, + project_id: nil, + type_new: 'Integrations::MockCi' + ) + end + + let!(:tethered_integration) do + integrations.create!( + instance: false, + group_id: group.id, + type_new: 'Integrations::MockCi' + ) + end + + let!(:another_tethered_integration) do + integrations.create!( + instance: false, + project_id: project.id, + group_id: nil, + type_new: 'Integrations::Asana' + ) + end + + describe "#up" do + it 'removes untethered integrations' do + expect(integrations.count).to eq(4) + expect(untethered_integration).to be_present + expect(another_untethered_integration).to be_present + + migrate! + + expect(integrations.count).to eq(2) + expect(tethered_integration.reload).to be_present + expect(another_tethered_integration.reload).to be_present + end + end +end diff --git a/spec/migrations/remove_not_null_integrations_constraint_spec.rb b/spec/migrations/remove_not_null_integrations_constraint_spec.rb new file mode 100644 index 00000000000..6f519c5ad38 --- /dev/null +++ b/spec/migrations/remove_not_null_integrations_constraint_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveNotNullIntegrationsConstraint, feature_category: :integrations do + include Database::TableSchemaHelpers + + describe '#up' do + before do + ApplicationRecord + .connection + .execute( + 'ALTER TABLE integrations ADD CONSTRAINT check_2aae034509 ' \ + 'CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID;' + ) + end + + it 'removes the check constraint' do + expect(check_constraint_definition(:integrations, 'check_2aae034509')) + .to eq('CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID') + + migrate! + + expect(check_constraint_definition(:integrations, 'check_2aae034509')).to be_nil + end + end +end diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index af035b303dc..fbd50acb049 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -53,28 +53,21 @@ RSpec.describe Integration, feature_category: :integrations do it { is_expected.to validate_presence_of(:type) } it { is_expected.to validate_exclusion_of(:type).in_array(described_class::BASE_CLASSES) } - where(:project_id, :group_id, :instance, :organization_id, :valid) do - 1 | nil | false | nil | true - nil | 1 | false | nil | true - nil | nil | true | 1 | true - nil | nil | false | nil | false - 1 | 1 | false | 1 | false - 1 | nil | true | nil | false - nil | 1 | false | nil | true - nil | 1 | true | 1 | false + where(:project_id, :group_id, :instance, :valid) do + 1 | nil | false | true + nil | 1 | false | true + nil | nil | true | true + nil | nil | false | false + 1 | 1 | false | false + 1 | nil | false | true + 1 | nil | true | false + nil | 1 | false | true + nil | 1 | true | false end with_them do it 'validates the integration' do - expect( - build( - :integration, - project_id: project_id, - group_id: group_id, - organization_id: organization_id, - instance: instance - ).valid? - ).to eq(valid) + expect(build(:integration, project_id: project_id, group_id: group_id, instance: instance).valid?).to eq(valid) end end @@ -363,95 +356,57 @@ RSpec.describe Integration, feature_category: :integrations do end describe '.find_or_initialize_all_non_project_specific' do - shared_examples 'integration instances' do |include_instance_specific| - it 'returns the available integration instances' do - integrations = described_class.find_or_initialize_all_non_project_specific( - described_class.for_instance, include_instance_specific: include_instance_specific - ).map(&:to_param) + shared_examples 'integration instances' do + [false, true].each do |include_instance_specific| + context "with include_instance_specific value equal to #{include_instance_specific}" do + it 'returns the available integration instances' do + integrations = described_class.find_or_initialize_all_non_project_specific( + described_class.for_instance, include_instance_specific: include_instance_specific + ).map(&:to_param) - expect(integrations).to match_array( - described_class.available_integration_names( - include_project_specific: false, - include_instance_specific: include_instance_specific - ) - ) - end + expect(integrations).to match_array( + described_class.available_integration_names( + include_project_specific: false, + include_instance_specific: include_instance_specific) + ) + end - it 'does not create integration instances' do - expect do - described_class.find_or_initialize_all_non_project_specific( - described_class.for_instance, - include_instance_specific: include_instance_specific - ) - end.not_to change { described_class.count } + it 'does not create integration instances' do + expect do + described_class.find_or_initialize_all_non_project_specific( + described_class.for_instance, + include_instance_specific: include_instance_specific + ) + end.not_to change { described_class.count } + end + end end end - context 'without existing instance integrations' do - it_behaves_like 'integration instances', false - it_behaves_like 'integration instances', true - end + it_behaves_like 'integration instances' - context 'with existing instances' do + context 'with all existing instances' do def integration_hash(type) - integration_hash = Integration.new(type: type).to_database_hash - integration_hash[:organization_id] = create(:organization).id - integration_hash[:instance] = true - integration_hash + Integration.new(instance: true, type: type).to_database_hash end - context 'when all instance integrations are present' do - context 'when include_instance_specific is true' do - before do - attrs = described_class - .available_integration_types( - include_project_specific: false, - include_instance_specific: true - ) - .map do |integration_type| - integration_hash(integration_type) - end - - described_class.insert_all(attrs) - end - - it_behaves_like 'integration instances', true - - context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do - before do - described_class.insert(integration_hash(:mock_ci)) - described_class.delete_by(**integration_hash(:asana)) - end - - it_behaves_like 'integration instances', true - end + before do + attrs = described_class.available_integration_types(include_project_specific: false).map do |integration_type| + integration_hash(integration_type) end - context 'when include_instance_specific is false' do - before do - attrs = described_class - .available_integration_types( - include_project_specific: false, - include_instance_specific: false - ) - .map do |integration_type| - integration_hash(integration_type) - end + described_class.insert_all(attrs) + end - described_class.insert_all(attrs) - end + it_behaves_like 'integration instances' - it_behaves_like 'integration instances', false - - context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do - before do - described_class.insert(integration_hash(:mock_ci)) - described_class.delete_by(**integration_hash(:asana)) - end - - it_behaves_like 'integration instances', false - end + context 'with a previous existing integration (:mock_ci) and a new integration (:asana)' do + before do + described_class.insert(integration_hash(:mock_ci)) + described_class.delete_by(**integration_hash(:asana)) end + + it_behaves_like 'integration instances' end end @@ -460,8 +415,7 @@ RSpec.describe Integration, feature_category: :integrations do create(:jira_integration, :instance) end - it_behaves_like 'integration instances', true - it_behaves_like 'integration instances', false + it_behaves_like 'integration instances' end end @@ -648,8 +602,8 @@ RSpec.describe Integration, feature_category: :integrations do end describe '.create_from_default_integrations' do - let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/', organization: create(:organization)) } - let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:prometheus_integration, :instance, api_url: 'https://prometheus.instance.com/') } + let!(:instance_level_instance_specific_integration) { create(:beyond_identity_integration) } it 'creates integrations from default integrations' do expect(described_class).to receive(:create_from_active_default_integrations) @@ -783,7 +737,7 @@ RSpec.describe Integration, feature_category: :integrations do end context 'when the integration is instance specific' do - let!(:instance_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:beyond_identity_integration) } it 'does not create an integration from the instance level instance specific integration' do described_class.create_from_active_default_integrations(project, :project_id) @@ -796,7 +750,7 @@ RSpec.describe Integration, feature_category: :integrations do describe '.create_from_default_instance_specific_integrations' do context 'with an active instance-level integration' do - let!(:instance_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:beyond_identity_integration) } it 'creates an integration from the instance-level integration' do described_class.create_from_default_instance_specific_integrations(project, :project_id) @@ -1514,7 +1468,7 @@ RSpec.describe Integration, feature_category: :integrations do it 'saves correctly using insert_all' do hash = record.to_database_hash - hash[:project_id] = project.id + hash[:project_id] = project expect do described_class.insert_all([hash]) @@ -1522,9 +1476,6 @@ RSpec.describe Integration, feature_category: :integrations do expect(described_class.last).not_to eq record expect(described_class.last).to have_attributes(properties: db_props) - expect(described_class.last.project_id).to eq(project.id) - expect(described_class.last.group_id).to be_nil - expect(described_class.last.organization_id).to be_nil end end end diff --git a/spec/models/integrations/beyond_identity_spec.rb b/spec/models/integrations/beyond_identity_spec.rb index 7c0b2ea70aa..3b645afb8b7 100644 --- a/spec/models/integrations/beyond_identity_spec.rb +++ b/spec/models/integrations/beyond_identity_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do - subject(:integration) { create(:beyond_identity_integration, :instance) } + subject(:integration) { create(:beyond_identity_integration) } describe 'validations' do context 'when inactive' do @@ -65,28 +65,22 @@ RSpec.describe Integrations::BeyondIdentity, feature_category: :integrations do end describe '.activated_for_instance?' do - let!(:integration) do - create(:beyond_identity_integration, instance: instance, active: active, group: group, organization: organization) - end - + let!(:integration) { create(:beyond_identity_integration, instance: instance, active: active, group: group) } let_it_be(:group_for_integration) { create(:group) } - let_it_be(:organization_for_integration) { create(:organization) } - subject(:activated) { described_class.activated_for_instance? } + subject { described_class.activated_for_instance? } using RSpec::Parameterized::TableSyntax - where(:instance, :group, :active, :expected, :organization) do - true | nil | true | true | lazy { organization_for_integration } - false | lazy { group_for_integration } | true | false | nil - true | nil | false | false | lazy { organization_for_integration } - false | lazy { group_for_integration } | false | false | nil + where(:instance, :group, :active, :expected) do + true | nil | true | true + false | lazy { group_for_integration } | true | false + true | nil | false | false + false | lazy { group_for_integration } | false | false end with_them do - it 'returns true if integration is activated' do - expect(activated).to eq(expected) - end + it { is_expected.to eq(expected) } end end end diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb index 38ada8eac8f..ab01d4641e4 100644 --- a/spec/models/integrations/drone_ci_spec.rb +++ b/spec/models/integrations/drone_ci_spec.rb @@ -133,7 +133,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching, fea it 'does not create a hook if project is not present' do integration.project = nil integration.instance = true - integration.organization = create(:organization) expect { integration.save! }.not_to change(ServiceHook, :count) end diff --git a/spec/requests/admin/slacks_controller_spec.rb b/spec/requests/admin/slacks_controller_spec.rb index db8d11576da..81ca6c9dbd4 100644 --- a/spec/requests/admin/slacks_controller_spec.rb +++ b/spec/requests/admin/slacks_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Admin::SlacksController, :enable_admin_mode, :with_current_organization, feature_category: :integrations do +RSpec.describe Admin::SlacksController, :enable_admin_mode, feature_category: :integrations do let_it_be(:user) { create(:admin) } before do diff --git a/spec/requests/api/graphql/integrations/exclusions_spec.rb b/spec/requests/api/graphql/integrations/exclusions_spec.rb index 9ef7bbae087..b99873bfea9 100644 --- a/spec/requests/api/graphql/integrations/exclusions_spec.rb +++ b/spec/requests/api/graphql/integrations/exclusions_spec.rb @@ -22,7 +22,7 @@ RSpec.describe 'Querying for integration exclusions', feature_category: :integra end context 'when the user is authorized' do - let!(:instance_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:beyond_identity_integration) } let!(:integration_exclusion) do create(:beyond_identity_integration, active: false, instance: false, project: project2, inherit_from_id: nil) end diff --git a/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb b/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb index f3698ec736e..407083306e6 100644 --- a/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb +++ b/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb @@ -60,7 +60,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Create, feature_category: :i end context 'when integrations exist for the projects' do - let!(:instance_exclusion) { create(:beyond_identity_integration, :instance) } + let!(:instance_exclusion) { create(:beyond_identity_integration) } let!(:existing_exclusion) do create(:beyond_identity_integration, project: project2, active: false, inherit_from_id: instance_exclusion.id, instance: false) diff --git a/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb index 1ed81c82c6d..f84daa3ac25 100644 --- a/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Mutations::Integrations::Exclusions::Delete, feature_category: :i end context 'and the integration is active for the instance' do - let!(:instance_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:beyond_identity_integration) } context 'and there is a group exclusion', :sidekiq_inline do let!(:group_exclusion) do diff --git a/spec/requests/api/graphql/mutations/namespaces/regenerate_new_work_item_email_address_spec.rb b/spec/requests/api/graphql/mutations/namespaces/regenerate_new_work_item_email_address_spec.rb new file mode 100644 index 00000000000..175e76b1025 --- /dev/null +++ b/spec/requests/api/graphql/mutations/namespaces/regenerate_new_work_item_email_address_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'NamespacesRegenerateNewWorkItemEmailAddress', feature_category: :team_planning do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, group: group) } + + let(:mutation) do + graphql_mutation( + :namespaces_regenerate_new_work_item_email_address, + { full_path: namespace.full_path }, + <<~FIELDS + namespace { + id + fullPath + } + errors + FIELDS + ) + end + + shared_examples 'successful regeneration' do + it 'regenerates the new work item email address' do + expect { post_graphql_mutation(mutation, current_user: user) } + .to change { user.reload.incoming_email_token } + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']['fullPath']) + .to eq(namespace.full_path) + + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']).to be_empty + end + end + + shared_examples 'permission denied' do + it 'returns an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).to include( + a_hash_including( + 'message' => 'The resource that you are attempting to access does not exist or you don\'t have ' \ + 'permission to perform this action' + ) + ) + end + end + + describe 'for group namespace' do + let(:namespace) { group } + + context 'when work item creation via email is supported' do + before do + stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com') + user.ensure_incoming_email_token! + end + + context 'when user has access to namespace' do + before_all do + group.add_developer(user) + end + + it 'returns an error about work item creation only being supported for projects' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']) + .to include('Work item creation via email is only supported for projects') + end + end + + context 'when user does not have access to namespace' do + it_behaves_like 'permission denied' + end + end + end + + describe 'for project namespace' do + let(:namespace) { project.project_namespace } + + context 'when work item creation via email is supported' do + before do + stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com') + user.ensure_incoming_email_token! + end + + context 'when user has access to namespace' do + before_all do + project.add_developer(user) + end + + it_behaves_like 'successful regeneration' + end + + context 'when user does not have access to namespace' do + it_behaves_like 'permission denied' + end + end + + context 'when work item creation via email is not supported' do + before do + stub_incoming_email_setting(enabled: false) + end + + before_all do + project.add_developer(user) + end + + it 'returns an error about work item creation not being supported' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']) + .to include('Work item creation via email is not supported') + end + end + + context 'when token reset fails' do + before do + stub_incoming_email_setting(enabled: true, address: 'incoming+%{key}@localhost.com') + user.ensure_incoming_email_token! + allow_next_found_instance_of(User) do |instance| + allow(instance).to receive(:reset_incoming_email_token!).and_return(false) + end + end + + before_all do + project.add_developer(user) + end + + it 'returns an error about failed regeneration' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['namespace']).to be_nil + expect(graphql_mutation_response(:namespaces_regenerate_new_work_item_email_address)['errors']) + .to include('Failed to regenerate new work item email address') + end + end + end +end diff --git a/spec/services/gpg_keys/validate_integrations_service_spec.rb b/spec/services/gpg_keys/validate_integrations_service_spec.rb index e128b2f1dc3..42b84473eb0 100644 --- a/spec/services/gpg_keys/validate_integrations_service_spec.rb +++ b/spec/services/gpg_keys/validate_integrations_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c end context 'when BeyondIdentity integration is not activated' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) } + let_it_be(:integration) { create(:beyond_identity_integration, active: false) } it 'return false' do expect(::Gitlab::BeyondIdentity::Client).not_to receive(:new) @@ -28,7 +28,7 @@ RSpec.describe GpgKeys::ValidateIntegrationsService, feature_category: :source_c end context 'when BeyondIdentity integration is activated' do - let_it_be(:integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:integration) { create(:beyond_identity_integration) } it 'returns true on successful check' do expect_next_instance_of(::Gitlab::BeyondIdentity::Client) do |instance| diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 57844798724..4ffff03ee0b 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -321,7 +321,7 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_ end context 'when an instance-level instance specific integration' do - let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) } it 'creates integration inheriting from the instance level integration' do expect(created_group.integrations.count).to eq(1) diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 85d856211eb..364adfa005e 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -366,7 +366,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline, feature_category: :grou end context 'with instance specific integration' do - let_it_be(:instance_specific_integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:instance_specific_integration) { create(:beyond_identity_integration) } let_it_be(:group_instance_specific_integration) do create( :beyond_identity_integration, diff --git a/spec/services/integrations/exclusions/create_service_spec.rb b/spec/services/integrations/exclusions/create_service_spec.rb index f9631e0103e..618668dfbd2 100644 --- a/spec/services/integrations/exclusions/create_service_spec.rb +++ b/spec/services/integrations/exclusions/create_service_spec.rb @@ -72,7 +72,7 @@ RSpec.describe Integrations::Exclusions::CreateService, feature_category: :sourc end context 'when there are existing custom settings' do - let_it_be(:instance_level_integration) { create(:beyond_identity_integration, :instance) } + let_it_be(:instance_level_integration) { create(:beyond_identity_integration) } let_it_be(:project_level_integration) do create( :beyond_identity_integration, diff --git a/spec/services/integrations/exclusions/destroy_service_spec.rb b/spec/services/integrations/exclusions/destroy_service_spec.rb index ee7a6f4b648..93718013aee 100644 --- a/spec/services/integrations/exclusions/destroy_service_spec.rb +++ b/spec/services/integrations/exclusions/destroy_service_spec.rb @@ -67,7 +67,7 @@ RSpec.describe Integrations::Exclusions::DestroyService, feature_category: :sour end context 'and the integration is active for the instance' do - let!(:instance_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_integration) { create(:beyond_identity_integration) } it 'updates the exclusion integration to be active' do expect { execute }.to change { exclusion.reload.active }.from(false).to(true) diff --git a/spec/services/integrations/slack_installation/instance_service_spec.rb b/spec/services/integrations/slack_installation/instance_service_spec.rb index 9b932272ff6..b4dee6e88d8 100644 --- a/spec/services/integrations/slack_installation/instance_service_spec.rb +++ b/spec/services/integrations/slack_installation/instance_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, :with_current_organization, feature_category: :integrations do +RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_mode, feature_category: :integrations do let_it_be(:user) { create(:admin) } let(:params) { {} } @@ -13,10 +13,9 @@ RSpec.describe Integrations::SlackInstallation::InstanceService, :enable_admin_m let(:integration) { Integrations::GitlabSlackApplication.for_instance.first } let(:redirect_url) { Gitlab::Routing.url_helpers.slack_auth_admin_application_settings_slack_url } let(:enqueues_propagation_worker) { true } - let(:params) { { code: oauth_code, organization_id: current_organization.id } } def create_gitlab_slack_application_integration! - Integrations::GitlabSlackApplication.create!(instance: true, organization: current_organization) + Integrations::GitlabSlackApplication.create!(instance: true) end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 24a29dcd7b9..b2c2dd40619 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -935,7 +935,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an subject(:project) { create_project(user, opts) } context 'when an instance-level instance specific integration' do - let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_specific_integration) { create(:beyond_identity_integration) } it 'creates integration inheriting from the instance level integration' do expect(project.integrations.count).to eq(1) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 7e161e0c1f9..a0e8827ee0e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -284,7 +284,7 @@ RSpec.describe Projects::TransferService, feature_category: :groups_and_projects end context 'when the new default integration is instance specific and deactivated' do - let!(:instance_specific_integration) { create(:beyond_identity_integration, :instance) } + let!(:instance_specific_integration) { create(:beyond_identity_integration) } let!(:project_instance_specific_integration) do create( :beyond_identity_integration, diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1075e20e5ef..0aae0985334 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -288,9 +288,6 @@ RSpec.configure do |config| # (ie. ApplicationSetting#auto_devops_enabled) stub_feature_flags(force_autodevops_on_by_default: false) - # The survey popover can block the diffs causing specs to fail - stub_feature_flags(mr_experience_survey: false) - # Using FortiAuthenticator as OTP provider is disabled by default in # tests, until we introduce it in user settings stub_feature_flags(forti_authenticator: false) diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb index cf1edc28746..1d02f768dbe 100644 --- a/spec/workers/projects/post_creation_worker_spec.rb +++ b/spec/workers/projects/post_creation_worker_spec.rb @@ -75,13 +75,7 @@ RSpec.describe Projects::PostCreationWorker, feature_category: :source_code_mana end it 'cleans invalid record and logs warning', :aggregate_failures do - invalid_integration_record = build( - :prometheus_integration, - properties: { api_url: nil, manual_configuration: true }, - project: build(:project), - group: build(:group) - ) - + invalid_integration_record = build(:prometheus_integration, properties: { api_url: nil, manual_configuration: true }) allow(::Integrations::Prometheus).to receive(:new).and_return(invalid_integration_record) expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice