diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 94d91326842..92f84f69b44 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1701,9 +1701,6 @@
# Run tests automatically for MRs that touch QA files
- <<: *if-merge-request
changes: *qa-patterns
- # Run tests automatically for MRS that touch GITALY_SERVER_VERSION
- - <<: *if-merge-request
- changes: *gitaly-patterns
# Otherwise, only run tests after the MR is approved
- <<: *if-merge-request-not-approved
when: never
@@ -2366,6 +2363,9 @@
changes: *code-backstage-qa-patterns
- <<: *if-merge-request
changes: *workhorse-patterns
+ - <<: *if-merge-request
+ changes: *rubocop-patterns
+
.rails:rules:detect-previous-failed-tests:
rules:
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 1ac09bfa157..9d1a620b048 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -86,22 +86,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/types/notes/update_diff_image_position_input_type.rb'
- 'app/graphql/types/packages/cleanup/policy_type.rb'
- 'app/graphql/types/packages/file_metadata_type.rb'
- - 'app/graphql/types/work_items/widgets/hierarchy_type.rb'
- - 'app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb'
- - 'app/graphql/types/work_items/widgets/labels_update_input_type.rb'
- - 'app/graphql/types/work_items/widgets/milestone_input_type.rb'
- - 'app/graphql/types/work_items/widgets/milestone_type.rb'
- - 'app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb'
- - 'app/graphql/types/x509_certificate_type.rb'
- - 'app/graphql/types/x509_issuer_type.rb'
- - 'app/services/lfs/lock_file_service.rb'
- - 'app/services/markdown_content_rewriter_service.rb'
- - 'app/services/members/base_service.rb'
- - 'app/services/ml/experiment_tracking/experiment_repository.rb'
- - 'app/services/post_receive_service.rb'
- - 'app/services/preview_markdown_service.rb'
- - 'app/services/protected_branches/api_service.rb'
- - 'app/services/protected_branches/legacy_api_create_service.rb'
- 'ee/app/graphql/ee/mutations/issues/create.rb'
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/mutations/work_items/create.rb'
diff --git a/.rubocop_todo/lint/or_assignment_to_constant.yml b/.rubocop_todo/lint/or_assignment_to_constant.yml
deleted file mode 100644
index 64ebe9e49b9..00000000000
--- a/.rubocop_todo/lint/or_assignment_to_constant.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-# Cop supports --autocorrect.
-Lint/OrAssignmentToConstant:
- Exclude:
- - 'lib/gitlab/email/handler/base_handler.rb'
- - 'tooling/danger/project_helper.rb'
diff --git a/.rubocop_todo/lint/redundant_string_coercion.yml b/.rubocop_todo/lint/redundant_string_coercion.yml
deleted file mode 100644
index 2663e0420b3..00000000000
--- a/.rubocop_todo/lint/redundant_string_coercion.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-# Cop supports --autocorrect.
-Lint/RedundantStringCoercion:
- Exclude:
- - 'ee/bin/geo_log_cursor'
- - 'ee/db/fixtures/development/31_devops_adoption.rb'
diff --git a/.rubocop_todo/migration/ensure_factory_for_table.yml b/.rubocop_todo/migration/ensure_factory_for_table.yml
index f5a8c65853c..96f9b79d1e8 100644
--- a/.rubocop_todo/migration/ensure_factory_for_table.yml
+++ b/.rubocop_todo/migration/ensure_factory_for_table.yml
@@ -56,3 +56,4 @@ Migration/EnsureFactoryForTable:
- 'db/migrate/20240304184128_create_ci_build_names_table.rb'
- 'db/migrate/20240306121653_create_relation_import_tracker.rb'
- 'db/migrate/20240404192955_create_early_access_program_tracking_events.rb'
+ - 'db/migrate/20240423064716_create_ci_build_execution_config.rb'
diff --git a/.rubocop_todo/rspec/env_mocking.yml b/.rubocop_todo/rspec/env_mocking.yml
deleted file mode 100644
index c9e0dad986b..00000000000
--- a/.rubocop_todo/rspec/env_mocking.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-# Cop supports --autocorrect.
-RSpec/EnvMocking:
- Exclude:
- - 'ee/spec/lib/gitlab/elastic/client_spec.rb'
- - 'spec/initializers/net_http_patch_spec.rb'
- - 'spec/lib/gitlab/database/reflection_spec.rb'
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index 497413ab452..6f20a424eac 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -134,7 +134,7 @@ export default {
if (!this.editor.isActive('diagram')) return;
- this.diagramSource = this.$refs.nodeViewContent.$el.textContent;
+ this.diagramSource = this.$refs.nodeViewContent?.$el.textContent || '';
if (this.node.attrs.language !== 'mermaid') {
this.diagramUrl = await this.contentEditor.renderDiagram(
diff --git a/app/assets/javascripts/content_editor/extensions/code_suggestion.js b/app/assets/javascripts/content_editor/extensions/code_suggestion.js
index 45090d57559..33e0dd13289 100644
--- a/app/assets/javascripts/content_editor/extensions/code_suggestion.js
+++ b/app/assets/javascripts/content_editor/extensions/code_suggestion.js
@@ -67,7 +67,7 @@ export default CodeBlockHighlight.extend({
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
- tag: 'pre[lang="suggestion"]',
+ tag: 'pre[data-canonical-lang="suggestion"]',
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index 7c4a56468eb..60c337bc572 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -39,7 +39,7 @@ export default CodeBlockHighlight.extend({
return [
{
priority: PARSE_HTML_PRIORITY_HIGHEST,
- tag: 'pre[lang="mermaid"]',
+ tag: 'pre[data-canonical-lang="mermaid"]',
getAttrs: () => ({ language: 'mermaid' }),
},
{
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index 673462073f0..af25b2261a5 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -7,6 +7,9 @@ Vue.use(VueApollo);
export default () => {
const el = document.getElementById('js-deploy-keys');
+
+ if (!el) return false;
+
return new Vue({
el,
apolloProvider: createApolloProvider({
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 958916fd9b0..57fbc661237 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -2,7 +2,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -65,38 +64,15 @@ export default {
data() {
return {
enableJiraIssues: this.initialEnableJiraIssues,
- projectKey: this.initialProjectKey,
projectKeys: this.initialProjectKeys,
};
},
computed: {
...mapGetters(['isInheriting']),
- multipleProjectKeys() {
- return this.glFeatures.jiraMultipleProjectKeys;
- },
-
checkboxDisabled() {
return !this.showJiraIssuesIntegration || this.isInheriting;
},
-
- validProjectKey() {
- // Allow saving the form without project_key when feature flag is enabled.
- // This will be improved in https://gitlab.com/gitlab-org/gitlab/-/issues/452161.
- if (this.multipleProjectKeys) {
- return true;
- }
-
- return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated;
- },
- },
- i18n: {
- enableCheckboxHelp: s__(
- 'JiraService|Warning: All users with access to this GitLab project can view all issues from the Jira project you specify.',
- ),
- projectKeyLabel: s__('JiraService|Jira project key'),
- projectKeyPlaceholder: s__('JiraService|AB'),
- requiredFieldFeedback: __('This field is required.'),
},
};
@@ -112,14 +88,18 @@ export default {
>
{{ s__('JiraService|View Jira issues') }}
- {{ $options.i18n.enableCheckboxHelp }}
+ {{
+ s__(
+ 'JiraService|Warning: All users with access to this GitLab project can view all issues from the Jira project you specify.',
+ )
+ }}
-
-
-
-
-
-
-
-
0) {
+ this.buildDropdowns();
+ this.bindEvents();
+ }
+
this.selectedItems = [];
}
@@ -35,6 +39,7 @@ export default class ProtectedTagCreate {
// Allowed to Create dropdown
const createTagSelector = 'js-allowed-to-create';
const [dropdownEl] = this.$form.find(`.${createTagSelector}`);
+
this.protectedTagAccessDropdown = initAccessDropdown(dropdownEl, {
toggleClass: createTagSelector,
hasLicense: this.hasLicense,
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
index fad15a5d89e..fdc82279a01 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -9,17 +9,14 @@ export default class ProtectedTagEditList {
}
initEditForm() {
- document
- .querySelector('.protected-tags-list')
- .querySelectorAll('.js-protected-tag-edit-form')
- ?.forEach((el) => {
- const accessDropdownEl = el.querySelector('.js-allowed-to-create');
- this.initAccessDropdown(accessDropdownEl, {
- url: el.dataset.url,
- hasLicense: this.hasLicense,
- accessLevelsData: gon.create_access_levels.roles,
- });
+ document.querySelectorAll('.protected-tags-list .js-protected-tag-edit-form')?.forEach((el) => {
+ const accessDropdownEl = el.querySelector('.js-allowed-to-create');
+ this.initAccessDropdown(accessDropdownEl, {
+ url: el.dataset.url,
+ hasLicense: this.hasLicense,
+ accessLevelsData: gon.create_access_levels.roles,
});
+ });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
index 52784fb9b84..5780f6691cf 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -183,7 +183,6 @@ export default {
ref="issuableFormWrapper"
:class="{ focus: isInputFocused }"
class="add-issuable-form-input-wrapper form-control gl-field-error-outline gl-h-auto gl-px-3 gl-pt-2 gl-pb-0"
- role="button"
@click="onIssuableFormWrapperClick"
>
diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js
index e99470cb79c..ae70b902709 100644
--- a/app/assets/javascripts/security_configuration/constants.js
+++ b/app/assets/javascripts/security_configuration/constants.js
@@ -52,6 +52,8 @@ export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning'
export const PRE_RECEIVE_SECRET_DETECTION = 'pre_receive_secret_detection';
+export const PRE_RECEIVE_SECRET_DETECTION_NAME = __('Pre-receive Secret Detection');
+
export const SCANNER_NAMES_MAP = {
SAST: SAST_SHORT_NAME,
SAST_IAC: SAST_IAC_NAME,
@@ -63,6 +65,7 @@ export const SCANNER_NAMES_MAP = {
DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
BREACH_AND_ATTACK_SIMULATION: BAS_NAME,
CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
+ PRE_RECEIVE_SECRET_DETECTION: PRE_RECEIVE_SECRET_DETECTION_NAME,
GENERIC: s__('ciReport|Manually added'),
};
diff --git a/app/controllers/groups/settings/integrations_controller.rb b/app/controllers/groups/settings/integrations_controller.rb
index ab41e17412c..59b24e8103d 100644
--- a/app/controllers/groups/settings/integrations_controller.rb
+++ b/app/controllers/groups/settings/integrations_controller.rb
@@ -6,9 +6,6 @@ module Groups
include ::Integrations::Actions
before_action :authorize_admin_group!
- before_action only: [:edit, :update] do
- push_frontend_feature_flag(:jira_multiple_project_keys, group)
- end
feature_category :integrations
diff --git a/app/controllers/import/source_users_controller.rb b/app/controllers/import/source_users_controller.rb
new file mode 100644
index 00000000000..430d1700134
--- /dev/null
+++ b/app/controllers/import/source_users_controller.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Import
+ class SourceUsersController < ApplicationController
+ prepend_before_action :check_feature_flag!
+
+ before_action :source_user
+ before_action :check_current_user_matches_invite!
+
+ respond_to :html
+ feature_category :importers
+
+ def accept
+ if source_user.accept
+ # TODO: This is where we enqueue the job to assign the contributions.
+
+ redirect_to(root_path, notice: format(mapping_decision_notice('approved'), invite_details))
+ else
+ redirect_to(root_path, alert: _('The invitation could not be accepted.'))
+ end
+ end
+
+ def decline
+ if source_user.reject
+ redirect_to(root_path, notice: format(mapping_decision_notice('rejected'), invite_details))
+ else
+ redirect_to(root_path, alert: _('The invitation could not be declined.'))
+ end
+ end
+
+ private
+
+ def check_current_user_matches_invite!
+ not_found unless current_user_matches_invite?
+ end
+
+ def current_user_matches_invite?
+ current_user.id == source_user.reassign_to_user_id
+ end
+
+ def source_user
+ Import::SourceUser.find(params[:id])
+ end
+ strong_memoize_attr :source_user
+
+ def invite_details
+ {
+ source_username: source_user.source_username,
+ source_hostname: source_user.source_hostname,
+ destination_group: source_user.namespace.name
+ }
+ end
+
+ def check_feature_flag!
+ not_found unless Feature.enabled?(:bulk_import_user_mapping, current_user)
+ end
+
+ # TODO: This is a placeholder for the proper UI to be provided
+ # in a follow-up MR.
+ def mapping_decision_notice(decision)
+ "You have #{decision} the reassignment of contributions from " \
+ "%{source_username} on %{source_hostname} " \
+ "to yourself on %{destination_group}."
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 8d5f2007a9e..ad8ddf182b7 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -12,9 +12,6 @@ module Projects
before_action :default_integration, only: [:edit, :update]
before_action :web_hook_logs, only: [:edit, :update]
before_action -> { check_test_rate_limit! }, only: :test
- before_action only: [:edit, :update] do
- push_frontend_feature_flag(:jira_multiple_project_keys, project.group)
- end
respond_to :html
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 2d7a3d698f0..8dc30ed5f68 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -4,7 +4,8 @@ module Projects
module Settings
class RepositoryController < Projects::ApplicationController
layout 'project_settings'
- before_action :authorize_admin_project!
+ before_action :authorize_admin_project!, except: [:show, :update]
+ before_action :authorize_admin_push_rules!, only: [:show, :update]
before_action :define_variables, only: [:create_deploy_token]
before_action do
diff --git a/app/graphql/mutations/integrations/exclusions/create.rb b/app/graphql/mutations/integrations/exclusions/create.rb
new file mode 100644
index 00000000000..7949b2a267b
--- /dev/null
+++ b/app/graphql/mutations/integrations/exclusions/create.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Integrations
+ module Exclusions
+ class Create < BaseMutation
+ graphql_name 'IntegrationExclusionCreate'
+ include ResolvesIds
+
+ field :exclusions, [::Types::Integrations::ExclusionType],
+ null: true,
+ description: 'Integration exclusions created by the mutation.'
+
+ argument :integration_name,
+ ::Types::Integrations::IntegrationTypeEnum,
+ required: true,
+ description: 'Type of integration to exclude.'
+
+ argument :project_ids,
+ [::Types::GlobalIDType[::Project]],
+ required: true,
+ description: 'Ids of projects to exclude.'
+
+ authorize :admin_all_resources
+
+ def resolve(integration_name:, project_ids:)
+ authorize!(:global)
+
+ projects = Project.id_in(resolve_ids(project_ids))
+
+ result = ::Integrations::Exclusions::CreateService.new(
+ current_user: current_user,
+ projects: projects,
+ integration_name: integration_name
+ ).execute
+
+ {
+ exclusions: result.payload,
+ errors: result.errors
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/integrations/exclusions/delete.rb b/app/graphql/mutations/integrations/exclusions/delete.rb
new file mode 100644
index 00000000000..d01d8c6776e
--- /dev/null
+++ b/app/graphql/mutations/integrations/exclusions/delete.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Integrations
+ module Exclusions
+ class Delete < BaseMutation
+ graphql_name 'IntegrationExclusionDelete'
+ include ResolvesIds
+
+ field :exclusions, [::Types::Integrations::ExclusionType],
+ null: true,
+ description: 'Project no longer excluded due to the mutation.'
+
+ argument :integration_name,
+ ::Types::Integrations::IntegrationTypeEnum,
+ required: true,
+ description: 'Type of integration.'
+
+ argument :project_ids,
+ [::Types::GlobalIDType[::Project]],
+ required: true,
+ description: 'Id of excluded project.'
+
+ authorize :admin_all_resources
+
+ def resolve(integration_name:, project_ids:)
+ authorize!(:global)
+
+ projects = Project.id_in(resolve_ids(project_ids))
+
+ result = ::Integrations::Exclusions::DestroyService.new(
+ current_user: current_user,
+ projects: projects,
+ integration_name: integration_name
+ ).execute
+
+ {
+ exclusions: result.payload,
+ errors: result.errors
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/integrations/exclusions_resolver.rb b/app/graphql/resolvers/integrations/exclusions_resolver.rb
new file mode 100644
index 00000000000..f5d405b578a
--- /dev/null
+++ b/app/graphql/resolvers/integrations/exclusions_resolver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Integrations
+ class ExclusionsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ type Types::Integrations::ExclusionType.connection_type, null: true
+
+ argument :integration_name, Types::Integrations::IntegrationTypeEnum,
+ required: true,
+ description: 'Type of integration.'
+
+ def resolve(integration_name:)
+ authorize!
+ Integration.integration_name_to_model(integration_name).with_custom_settings.by_active_flag(false)
+ end
+
+ def authorize!
+ raise_resource_not_available_error! unless context[:current_user]&.can_admin_all_resources?
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/integrations/exclusion_type.rb b/app/graphql/types/integrations/exclusion_type.rb
new file mode 100644
index 00000000000..0cd07f74238
--- /dev/null
+++ b/app/graphql/types/integrations/exclusion_type.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module Integrations
+ class ExclusionType < BaseObject
+ graphql_name 'IntegrationExclusion'
+ description 'An integration to override the level settings of instance specific integrations.'
+ authorize :admin_all_resources
+
+ field :project, ::Types::ProjectType,
+ description: 'Project that has been excluded from the instance specific integration.'
+ end
+ end
+end
diff --git a/app/graphql/types/integrations/integration_type_enum.rb b/app/graphql/types/integrations/integration_type_enum.rb
new file mode 100644
index 00000000000..4e6b8c8005c
--- /dev/null
+++ b/app/graphql/types/integrations/integration_type_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module Integrations
+ class IntegrationTypeEnum < BaseEnum
+ graphql_name 'IntegrationType'
+ description 'Integration Names'
+
+ value 'BEYOND_IDENTITY', description: 'Beyond Identity.', value: 'beyond_identity'
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index b4a28f640aa..0ea7d1a0b5f 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -63,6 +63,8 @@ module Types
mount_mutation Mutations::IncidentManagement::TimelineEvent::Update
mount_mutation Mutations::IncidentManagement::TimelineEvent::Destroy
mount_mutation Mutations::IncidentManagement::TimelineEventTag::Create
+ mount_mutation Mutations::Integrations::Exclusions::Create, alpha: { milestone: '17.0' }
+ mount_mutation Mutations::Integrations::Exclusions::Delete, alpha: { milestone: '17.0' }
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetCrmContacts
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 1091ca2a788..76b5f7a6d05 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -222,6 +222,11 @@ module Types
description: 'Find machine learning models.',
resolver: Resolvers::Ml::ModelDetailResolver
+ field :integration_exclusions, Types::Integrations::ExclusionType.connection_type,
+ null: true,
+ alpha: { milestone: '17.0' },
+ resolver: Resolvers::Integrations::ExclusionsResolver
+
field :work_items_by_reference,
null: true,
alpha: { milestone: '16.7' },
diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb
index 41c5af2ce63..c30a886ab15 100644
--- a/app/graphql/types/work_items/widgets/hierarchy_type.rb
+++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb
@@ -27,7 +27,7 @@ module Types
resolver: Resolvers::WorkItems::AncestorsResolver
field :has_children, GraphQL::Types::Boolean,
- null: false, description: 'Indicates if the work item has children.'
+ null: false, description: 'Indicates if the work item has children.'
# rubocop: disable CodeReuse/ActiveRecord
def has_children?
diff --git a/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb b/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb
index 297b06a8fab..2e0d6d12012 100644
--- a/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb
+++ b/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb
@@ -7,26 +7,26 @@ module Types
graphql_name 'WorkItemWidgetHierarchyUpdateInput'
argument :adjacent_work_item_id,
- ::Types::GlobalIDType[::WorkItem],
- required: false,
- loads: ::Types::WorkItemType,
- description: 'ID of the work item to be switched with.'
+ ::Types::GlobalIDType[::WorkItem],
+ required: false,
+ loads: ::Types::WorkItemType,
+ description: 'ID of the work item to be switched with.'
argument :children_ids, [::Types::GlobalIDType[::WorkItem]],
- required: false,
- description: 'Global IDs of children work items.',
- loads: ::Types::WorkItemType,
- as: :children
+ required: false,
+ description: 'Global IDs of children work items.',
+ loads: ::Types::WorkItemType,
+ as: :children
argument :parent_id, ::Types::GlobalIDType[::WorkItem],
- required: false,
- loads: ::Types::WorkItemType,
- description: 'Global ID of the parent work item. Use `null` to remove the association.'
+ required: false,
+ loads: ::Types::WorkItemType,
+ description: 'Global ID of the parent work item. Use `null` to remove the association.'
argument :relative_position,
- Types::RelativePositionTypeEnum,
- required: false,
- description: 'Type of switch. Valid values are `BEFORE` or `AFTER`.'
+ Types::RelativePositionTypeEnum,
+ required: false,
+ description: 'Type of switch. Valid values are `BEFORE` or `AFTER`.'
end
end
end
diff --git a/app/graphql/types/work_items/widgets/labels_update_input_type.rb b/app/graphql/types/work_items/widgets/labels_update_input_type.rb
index d38b8cefa63..dafdff1f884 100644
--- a/app/graphql/types/work_items/widgets/labels_update_input_type.rb
+++ b/app/graphql/types/work_items/widgets/labels_update_input_type.rb
@@ -7,13 +7,13 @@ module Types
graphql_name 'WorkItemWidgetLabelsUpdateInput'
argument :add_label_ids, [Types::GlobalIDType[::Label]],
- required: false,
- description: 'Global IDs of labels to be added to the work item.',
- prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
+ required: false,
+ description: 'Global IDs of labels to be added to the work item.',
+ prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
argument :remove_label_ids, [Types::GlobalIDType[::Label]],
- required: false,
- description: 'Global IDs of labels to be removed from the work item.',
- prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
+ required: false,
+ description: 'Global IDs of labels to be removed from the work item.',
+ prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
end
end
end
diff --git a/app/graphql/types/work_items/widgets/milestone_input_type.rb b/app/graphql/types/work_items/widgets/milestone_input_type.rb
index 996c782373f..b4cbf34e35a 100644
--- a/app/graphql/types/work_items/widgets/milestone_input_type.rb
+++ b/app/graphql/types/work_items/widgets/milestone_input_type.rb
@@ -7,10 +7,10 @@ module Types
graphql_name 'WorkItemWidgetMilestoneInput'
argument :milestone_id,
- Types::GlobalIDType[::Milestone],
- required: :nullable,
- prepare: ->(id, _) { id.model_id unless id.nil? },
- description: 'Milestone to assign to the work item.'
+ Types::GlobalIDType[::Milestone],
+ required: :nullable,
+ prepare: ->(id, _) { id.model_id unless id.nil? },
+ description: 'Milestone to assign to the work item.'
end
end
end
diff --git a/app/graphql/types/work_items/widgets/milestone_type.rb b/app/graphql/types/work_items/widgets/milestone_type.rb
index 73318e58a00..96edda000c3 100644
--- a/app/graphql/types/work_items/widgets/milestone_type.rb
+++ b/app/graphql/types/work_items/widgets/milestone_type.rb
@@ -13,9 +13,9 @@ module Types
implements Types::WorkItems::WidgetInterface
field :milestone,
- ::Types::MilestoneType,
- null: true,
- description: 'Milestone of the work item.'
+ ::Types::MilestoneType,
+ null: true,
+ description: 'Milestone of the work item.'
end
# rubocop:enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb b/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb
index bccd4afe8f3..f9e85d823ab 100644
--- a/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb
+++ b/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb
@@ -7,11 +7,11 @@ module Types
graphql_name 'WorkItemWidgetStartAndDueDateUpdateInput'
argument :due_date, Types::DateType,
- required: false,
- description: 'Due date for the work item.'
+ required: false,
+ description: 'Due date for the work item.'
argument :start_date, Types::DateType,
- required: false,
- description: 'Start date for the work item.'
+ required: false,
+ description: 'Start date for the work item.'
end
end
end
diff --git a/app/graphql/types/x509_certificate_type.rb b/app/graphql/types/x509_certificate_type.rb
index 806aa441af7..5f053b25094 100644
--- a/app/graphql/types/x509_certificate_type.rb
+++ b/app/graphql/types/x509_certificate_type.rb
@@ -8,31 +8,31 @@ module Types
description 'Represents an X.509 certificate.'
field :certificate_status, GraphQL::Types::String,
- null: false,
- description: 'Indicates if the certificate is good or revoked.'
+ null: false,
+ description: 'Indicates if the certificate is good or revoked.'
field :created_at, Types::TimeType, null: false,
- description: 'Timestamp of when the certificate was saved.'
+ description: 'Timestamp of when the certificate was saved.'
field :email, GraphQL::Types::String, null: false,
- description: 'Email associated with the cerificate.'
+ description: 'Email associated with the cerificate.'
field :id, GraphQL::Types::ID, null: false, description: 'ID of the certificate.'
field :serial_number, GraphQL::Types::String, null: false,
- description: 'Serial number of the certificate.'
+ description: 'Serial number of the certificate.'
field :subject, GraphQL::Types::String, null: false, description: 'Subject of the certificate.'
field :subject_key_identifier, GraphQL::Types::String,
- null: false,
- description: 'Subject key identifier of the certificate.'
+ null: false,
+ description: 'Subject key identifier of the certificate.'
field :updated_at, Types::TimeType, null: false,
- description: 'Timestamp of when the certificate was last updated.'
+ description: 'Timestamp of when the certificate was last updated.'
field :x509_issuer, Types::X509IssuerType, null: false,
- description: 'Issuer of the certificate.'
+ description: 'Issuer of the certificate.'
end
end
diff --git a/app/graphql/types/x509_issuer_type.rb b/app/graphql/types/x509_issuer_type.rb
index a5759e48ee0..343c5934fcd 100644
--- a/app/graphql/types/x509_issuer_type.rb
+++ b/app/graphql/types/x509_issuer_type.rb
@@ -8,21 +8,21 @@ module Types
description 'Issuer of an X.509 certificate.'
field :created_at, Types::TimeType, null: true,
- description: 'Timestamp of when the issuer was created.'
+ description: 'Timestamp of when the issuer was created.'
field :crl_url, GraphQL::Types::String, null: true,
- description: 'Certificate revokation list of the issuer.'
+ description: 'Certificate revokation list of the issuer.'
field :id, GraphQL::Types::ID, null: true, description: 'ID of the issuer.'
field :subject, GraphQL::Types::String, null: true, description: 'Subject of the issuer.'
field :subject_key_identifier, GraphQL::Types::String,
- null: true,
- description: 'Subject key identifier of the issuer.'
+ null: true,
+ description: 'Subject key identifier of the issuer.'
field :updated_at, Types::TimeType, null: true,
- description: 'Timestamp of when the issuer was last updated.'
+ description: 'Timestamp of when the issuer was last updated.'
end
end
diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
index 53f960e64a7..3fbee8fc3ff 100644
--- a/app/mailers/previews/devise_mailer_preview.rb
+++ b/app/mailers/previews/devise_mailer_preview.rb
@@ -12,6 +12,13 @@ class DeviseMailerPreview < ActionMailer::Preview
DeviseMailer.confirmation_instructions(user, 'faketoken', {})
end
+ def confirmation_instructions_for_secondary_email
+ user = User.last
+ secondary_email = user.emails.build(email: 'unconfirmed@example.com')
+
+ DeviseMailer.confirmation_instructions(secondary_email, 'faketoken', {})
+ end
+
def reset_password_instructions
DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 915226ed772..51f912b5e4e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -46,6 +46,8 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
PACKAGE_REGISTRY_SETTINGS = [:nuget_skip_metadata_url_validation].freeze
+ USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS = 3
+
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 03f10c1f817..cc0e24003d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -28,6 +28,13 @@ module Ci
inverse_of: :builds
belongs_to :project_mirror, primary_key: :project_id, foreign_key: :project_id, inverse_of: :builds
+ belongs_to :execution_config,
+ ->(build) { in_partition(build) },
+ class_name: 'Ci::BuildExecutionConfig',
+ foreign_key: :execution_config_id,
+ partition_foreign_key: :partition_id,
+ inverse_of: :builds
+
RUNNER_FEATURES = {
upload_multiple_artifacts: ->(build) { build.publishes_artifacts_reports? },
refspecs: ->(build) { build.merge_request_ref? },
diff --git a/app/models/ci/build_execution_config.rb b/app/models/ci/build_execution_config.rb
new file mode 100644
index 00000000000..f9488bfd4fe
--- /dev/null
+++ b/app/models/ci/build_execution_config.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildExecutionConfig < Ci::ApplicationRecord
+ include Ci::Partitionable
+
+ self.table_name = :p_ci_builds_execution_configs
+ self.primary_key = :id
+
+ partitionable scope: :pipeline, partitioned: true
+
+ query_constraints :id, :partition_id
+
+ belongs_to :pipeline,
+ ->(execution_config) { in_partition(execution_config) },
+ class_name: 'Ci::Pipeline',
+ partition_foreign_key: :partition_id,
+ inverse_of: :build_execution_configs
+
+ belongs_to :project
+
+ has_many :builds,
+ ->(execution_config) { in_partition(execution_config) },
+ class_name: 'Ci::Build',
+ foreign_key: :execution_config_id,
+ inverse_of: :execution_config,
+ partition_foreign_key: :partition_id
+
+ validates :run_steps, json_schema: { filename: 'run_steps' }, presence: true
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5a0ea42e46b..f56d15098e3 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -86,6 +86,7 @@ module Ci
inverse_of: :pipeline, partition_foreign_key: :partition_id
has_many :bridges, ->(pipeline) { in_partition(pipeline) }, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline, partition_foreign_key: :partition_id
has_many :builds, ->(pipeline) { in_partition(pipeline) }, foreign_key: :commit_id, inverse_of: :pipeline, partition_foreign_key: :partition_id
+ has_many :build_execution_configs, ->(pipeline) { in_partition(pipeline) }, class_name: 'Ci::BuildExecutionConfig', inverse_of: :pipeline, partition_foreign_key: :partition_id
has_many :generic_commit_statuses, ->(pipeline) { in_partition(pipeline) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'GenericCommitStatus', partition_foreign_key: :partition_id
#
# NEW:
diff --git a/app/models/concerns/ci/partitionable/testing.rb b/app/models/concerns/ci/partitionable/testing.rb
index 62fc7cff53d..7203d71a2ca 100644
--- a/app/models/concerns/ci/partitionable/testing.rb
+++ b/app/models/concerns/ci/partitionable/testing.rb
@@ -7,6 +7,7 @@ module Ci
PARTITIONABLE_MODELS = %w[
CommitStatus
+ Ci::BuildExecutionConfig
Ci::BuildMetadata
Ci::BuildName
Ci::BuildNeed
diff --git a/app/models/concerns/import/has_import_source.rb b/app/models/concerns/import/has_import_source.rb
index f617c7accc3..cdede6ddc77 100644
--- a/app/models/concerns/import/has_import_source.rb
+++ b/app/models/concerns/import/has_import_source.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
module Import
+ SOURCE_DIRECT_TRANSFER = :gitlab_migration # aka BulkImports
+
module HasImportSource
extend ActiveSupport::Concern
IMPORT_SOURCES = {
none: 0, # not imported
- gitlab_migration: 1, # aka direct transfer & bulk_import
+ SOURCE_DIRECT_TRANSFER => 1,
gitlab_project: 2, # aka gitlab import/export
github: 3,
bitbucket: 4, # aka bitbucket cloud
diff --git a/app/models/email.rb b/app/models/email.rb
index 5fca57520b8..2fc70369673 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -11,6 +11,8 @@ class Email < MainClusterwide::ApplicationRecord
validate :unique_email, if: ->(email) { email.email_changed? }
scope :confirmed, -> { where.not(confirmed_at: nil) }
+ scope :unconfirmed, -> { where(confirmed_at: nil) }
+ scope :unconfirmed_and_created_before, ->(created_cut_off) { unconfirmed.where('created_at < ?', created_cut_off) }
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
diff --git a/app/models/group.rb b/app/models/group.rb
index 06860b8801b..7f266ce1012 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -171,7 +171,6 @@ class Group < Namespace
format_with_prefix: :runners_token_prefix,
require_prefix_for_validation: true
- before_save :ensure_runner_registration_token_disabled_on_com
after_create :post_create_hook
after_create -> { create_or_load_association(:group_feature) }
after_update :path_changed_hook, if: :saved_change_to_path?
@@ -601,15 +600,6 @@ class Group < Namespace
system_hook_service.execute_hooks_for(self, :destroy)
end
- def ensure_runner_registration_token_disabled_on_com
- return unless parent.nil?
- return if namespace_settings
- return if ::Gitlab::CurrentSettings.gitlab_dedicated_instance?
- return unless ::Gitlab.com? # rubocop: disable Gitlab/AvoidGitlabInstanceChecks -- this is not based on a feature, but indeed on the location of the code
-
- self.namespace_settings = NamespaceSetting.new(namespace_id: id, allow_runner_registration_token: false)
- end
-
# rubocop: disable CodeReuse/ServiceClass
def system_hook_service
SystemHooksService.new
diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb
index d87886a9228..3c51c87acee 100644
--- a/app/models/import/source_user.rb
+++ b/app/models/import/source_user.rb
@@ -19,6 +19,22 @@ module Import
state :rejected, value: 2
state :failed, value: 3
state :completed, value: 4
+
+ event :cancel_assignment do
+ transition [:awaiting_approval, :rejected] => :pending_assignment
+ end
+
+ event :accept do
+ transition awaiting_approval: :completed
+ end
+
+ event :reject do
+ transition awaiting_approval: :rejected
+ end
+
+ after_transition any => [:pending_assignment, :rejected] do |status|
+ status.update!(reassign_to_user: nil)
+ end
end
def self.find_source_user(source_user_identifier:, namespace:, source_hostname:, import_type:)
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index db7666ad4c3..272f2b5d467 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -61,10 +61,6 @@ module Integrations
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
- before_save :copy_project_key_to_project_keys,
- if: -> {
- Feature.disabled?(:jira_multiple_project_keys, group || project&.group)
- }
before_save :format_project_keys, if: :project_keys_changed?
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
@@ -260,28 +256,20 @@ module Integrations
# Currently, Jira issues are only configurable at the project and group levels.
unless instance_level?
- issues_title = if Feature.enabled?(:jira_multiple_project_keys, group || project&.group)
- s_('JiraService|Jira issues (optional)')
- else
- _('Issues')
- end
-
sections.push({
type: SECTION_TYPE_JIRA_ISSUES,
- title: issues_title,
+ title: s_('JiraService|Jira issues (optional)'),
description: jira_issues_section_description,
plan: 'premium'
})
- if Feature.enabled?(:jira_multiple_project_keys, group || project&.group)
- sections.push({
- type: SECTION_TYPE_JIRA_ISSUE_CREATION,
- title: s_('JiraService|Jira issues for vulnerabilities (optional)'),
- description: s_('JiraService|Create Jira issues from GitLab to track any action taken ' \
- 'to resolve or mitigate vulnerabilities.'),
- plan: 'ultimate'
- })
- end
+ sections.push({
+ type: SECTION_TYPE_JIRA_ISSUE_CREATION,
+ title: s_('JiraService|Jira issues for vulnerabilities (optional)'),
+ description: s_('JiraService|Create Jira issues from GitLab to track any action taken ' \
+ 'to resolve or mitigate vulnerabilities.'),
+ plan: 'ultimate'
+ })
end
sections
@@ -456,11 +444,7 @@ module Integrations
end
def issue_key_allowed?(issue_key)
- if Feature.disabled?(:jira_multiple_project_keys)
- parse_project_from_issue_key(issue_key) == project_key
- else
- project_keys.blank? || project_keys.include?(parse_project_from_issue_key(issue_key))
- end
+ project_keys.blank? || project_keys.include?(parse_project_from_issue_key(issue_key))
end
def branch_name(commit)
@@ -733,10 +717,6 @@ module Integrations
end
end
- def copy_project_key_to_project_keys
- data_fields.project_keys = [project_key]
- end
-
def format_project_keys
data_fields.project_keys = project_keys.compact_blank.map(&:strip).uniq
end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 32232c93d11..2187eac36d0 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -50,7 +50,7 @@ module WorkItems
private
def validate_max_children
- return unless work_item_parent
+ return unless work_item_parent && work_item_parent_id_changed?
max = persisted? ? MAX_CHILDREN : MAX_CHILDREN - 1
if work_item_parent.child_links.count > max
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 0b51cc1a28a..7ce72a8f731 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -604,6 +604,7 @@ class ProjectPolicy < BasePolicy
enable :stop_environment
enable :read_import_error
enable :admin_cicd_variables
+ enable :admin_push_rules
end
rule { can?(:admin_build) }.enable :manage_trigger
diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb
index 54f193c86e6..da4ef6e68ed 100644
--- a/app/services/lfs/lock_file_service.rb
+++ b/app/services/lfs/lock_file_service.rb
@@ -26,7 +26,7 @@ module Lfs
def create_lock!
lock = project.lfs_file_locks.create!(user: current_user,
- path: params[:path])
+ path: params[:path])
success(http_status: 201, lock: lock)
end
diff --git a/app/services/markdown_content_rewriter_service.rb b/app/services/markdown_content_rewriter_service.rb
index 4d8f523fa77..6d47b89f260 100644
--- a/app/services/markdown_content_rewriter_service.rb
+++ b/app/services/markdown_content_rewriter_service.rb
@@ -67,6 +67,6 @@ class MarkdownContentRewriterService
end
attr_reader :current_user, :content, :source_parent,
- :target_parent, :rewriters, :content_html,
- :field, :html_field, :object, :result
+ :target_parent, :rewriters, :content_html,
+ :field, :html_field, :object, :result
end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index 80fba33b20e..8d5996fda2b 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -76,6 +76,6 @@ module Members
end
alias_method :cannot_revoke_owner_responsibilities_from_member_in_project?,
- :cannot_assign_owner_responsibilities_to_member_in_project?
+ :cannot_assign_owner_responsibilities_to_member_in_project?
end
end
diff --git a/app/services/ml/experiment_tracking/experiment_repository.rb b/app/services/ml/experiment_tracking/experiment_repository.rb
index a2674235137..767eda13934 100644
--- a/app/services/ml/experiment_tracking/experiment_repository.rb
+++ b/app/services/ml/experiment_tracking/experiment_repository.rb
@@ -22,8 +22,8 @@ module Ml
def create!(name, tags = nil)
experiment = ::Ml::Experiment.create!(name: name,
- user: user,
- project: project)
+ user: user,
+ project: project)
add_tags(experiment, tags)
diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb
index 804bb2e1fd7..16a09e3d616 100644
--- a/app/services/post_receive_service.rb
+++ b/app/services/post_receive_service.rb
@@ -27,7 +27,7 @@ class PostReceiveService
repository&.expire_branches_cache if mr_options&.fetch(:create, false)
PostReceive.perform_async(params[:gl_repository], params[:identifier],
- params[:changes], push_options.as_json)
+ params[:changes], push_options.as_json)
if mr_options.present?
message = process_mr_push_options(mr_options, params[:changes])
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 3c2f5dcbbc5..a8247fdb908 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -37,14 +37,14 @@ class PreviewMarkdownService < BaseContainerService
return [] unless preview_sugestions?
position = Gitlab::Diff::Position.new(new_path: params[:file_path],
- new_line: params[:line].to_i,
- base_sha: params[:base_sha],
- head_sha: params[:head_sha],
- start_sha: params[:start_sha])
+ new_line: params[:line].to_i,
+ base_sha: params[:base_sha],
+ head_sha: params[:head_sha],
+ start_sha: params[:start_sha])
Gitlab::Diff::SuggestionsParser.parse(text, position: position,
- project: project,
- supports_suggestion: params[:preview_suggestions])
+ project: project,
+ supports_suggestion: params[:preview_suggestions])
end
def preview_sugestions?
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index 0a7777c7fed..51cef412a88 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -8,7 +8,7 @@ module ProtectedBranches
def update(protected_branch)
::ProtectedBranches::UpdateService.new(project_or_group, @current_user,
-protected_branch_params(with_defaults: false)).execute(protected_branch)
+ protected_branch_params(with_defaults: false)).execute(protected_branch)
end
private
diff --git a/app/services/protected_branches/legacy_api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb
index f662d9d1bf0..b4cf5da251b 100644
--- a/app/services/protected_branches/legacy_api_create_service.rb
+++ b/app/services/protected_branches/legacy_api_create_service.rb
@@ -22,7 +22,7 @@ module ProtectedBranches
end
@params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
- merge_access_levels_attributes: [{ access_level: merge_access_level }])
+ merge_access_levels_attributes: [{ access_level: merge_access_level }])
service = ProtectedBranches::CreateService.new(project_or_group, @current_user, @params)
service.execute
diff --git a/app/validators/json_schemas/run_steps.json b/app/validators/json_schemas/run_steps.json
new file mode 100644
index 00000000000..1afcda06b73
--- /dev/null
+++ b/app/validators/json_schemas/run_steps.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://gitlab.com/gitlab-org/step-runner/schema/v1/steps",
+ "items": {
+ "oneOf": [
+ {
+ "required": [
+ "step"
+ ],
+ "title": "step"
+ },
+ {
+ "required": [
+ "script"
+ ],
+ "title": "script"
+ }
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name is a unique identifier for this step."
+ },
+ "step": {
+ "type": "string",
+ "description": "Step is a reference to the step to invoke."
+ },
+ "env": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Env is a map of environment variable names to string values."
+ },
+ "inputs": {
+ "type": "object",
+ "description": "Inputs is a map of step input names to structured values."
+ },
+ "script": {
+ "type": "string",
+ "description": "Script is a shell script to evaluate."
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "description": "Step is a single step invocation."
+ },
+ "type": "array"
+}
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
index 97fdf0249da..1576bdf4d0e 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml
@@ -6,3 +6,5 @@
%p
= _('If this email was added in error, you can remove it here:')
= link_to _("Emails"), profile_emails_url
+ %p
+ = format(_('Confirm this email address within %{cut_off_days} days, otherwise the email address is removed.'), cut_off_days: ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS)
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
index 32e88047a9c..b6f409e2919 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
@@ -5,3 +5,5 @@
<%= confirmation_url(@resource, confirmation_token: @token) %>
<%= _("If this email was added in error, you can remove it here: %{profile_emails_url}") % { profile_emails_url: profile_emails_url } %>
+
+<%= format(_('Confirm this email address within %{cut_off_days} days, otherwise the email address is removed.'), cut_off_days: ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS) %>
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index a47072c0e26..f0ca2a18e5b 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -3,18 +3,23 @@
- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to packages, your repository, and registry images.')
- @force_desktop_expanded_sidebar = true
-= render "projects/branch_defaults/show"
-= render "projects/branch_rules/show"
-= render_if_exists "projects/push_rules/index"
-= render "projects/mirrors/mirror_repos"
+- if can?(current_user, :admin_project, @project)
+ = render "projects/branch_defaults/show"
+ = render "projects/branch_rules/show"
--# Protected branches & tags use a lot of nested partials.
--# The shared parts of the views can be found in the `shared` directory.
--# Those are used throughout the actual views. These `shared` views are then
--# reused in EE.
-= render "projects/settings/repository/protected_branches", protected_branch_entity: @project
-= render "shared/deploy_tokens/index", group_or_project: @project, description: deploy_token_description
-= render 'shared/deploy_keys/index'
-= render "projects/maintenance/show"
+- if can?(current_user, :admin_push_rules, @project)
+ = render_if_exists "projects/push_rules/index"
-= render_if_exists 'shared/promotions/promote_repository_features'
+- if can?(current_user, :admin_project, @project)
+ = render "projects/mirrors/mirror_repos"
+
+ -# Protected branches & tags use a lot of nested partials.
+ -# The shared parts of the views can be found in the `shared` directory.
+ -# Those are used throughout the actual views. These `shared` views are then
+ -# reused in EE.
+ = render "projects/settings/repository/protected_branches", protected_branch_entity: @project
+ = render "shared/deploy_tokens/index", group_or_project: @project, description: deploy_token_description
+ = render 'shared/deploy_keys/index'
+ = render "projects/maintenance/show"
+
+ = render_if_exists 'shared/promotions/promote_repository_features'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index c8591e6b80a..027ebc85130 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -975,6 +975,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:users_unconfirmed_secondary_emails_deletion_cron
+ :worker_name: Users::UnconfirmedSecondaryEmailsDeletionCronWorker
+ :feature_category: :user_management
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:x509_issuer_crl_check
:worker_name: X509IssuerCrlCheckWorker
:feature_category: :source_code_management
diff --git a/app/workers/users/unconfirmed_secondary_emails_deletion_cron_worker.rb b/app/workers/users/unconfirmed_secondary_emails_deletion_cron_worker.rb
new file mode 100644
index 00000000000..205233fae71
--- /dev/null
+++ b/app/workers/users/unconfirmed_secondary_emails_deletion_cron_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Users
+ class UnconfirmedSecondaryEmailsDeletionCronWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext -- This worker does not perform work scoped to a context
+
+ deduplicate :until_executed
+ idempotent!
+ data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency -- This is a cron job
+ feature_category :user_management
+
+ BATCH_SIZE = 1000
+
+ def perform
+ loop do
+ records_deleted = Email.unconfirmed_and_created_before(created_cut_off).limit(BATCH_SIZE).delete_all
+
+ break if records_deleted == 0
+ end
+ end
+
+ private
+
+ def created_cut_off
+ ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS.days.ago
+ end
+ end
+end
diff --git a/config/feature_flags/gitlab_com_derisk/jira_multiple_project_keys.yml b/config/feature_flags/gitlab_com_derisk/jira_multiple_project_keys.yml
deleted file mode 100644
index 44071555e14..00000000000
--- a/config/feature_flags/gitlab_com_derisk/jira_multiple_project_keys.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: jira_multiple_project_keys
-feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/440430
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146087
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455259
-milestone: '16.10'
-group: group::import and integrate
-type: gitlab_com_derisk
-default_enabled: false
diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml
index a7ab2c054a5..d17f80949de 100644
--- a/config/gitlab_loose_foreign_keys.yml
+++ b/config/gitlab_loose_foreign_keys.yml
@@ -303,6 +303,10 @@ p_ci_builds:
- table: ci_runners
column: runner_id
on_delete: async_nullify
+p_ci_builds_execution_configs:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
p_ci_builds_metadata:
- table: projects
column: project_id
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 6cd27637fbf..53b632e4dc4 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -870,6 +870,9 @@ Gitlab.ee do
Settings.cron_jobs['users_delete_unconfirmed_users_worker'] ||= {}
Settings.cron_jobs['users_delete_unconfirmed_users_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['users_delete_unconfirmed_users_worker']['job_class'] = 'Users::UnconfirmedUsersDeletionCronWorker'
+ Settings.cron_jobs['users_unconfirmed_secondary_emails_deletion_cron_worker'] ||= {}
+ Settings.cron_jobs['users_unconfirmed_secondary_emails_deletion_cron_worker']['cron'] ||= '0 * * * *'
+ Settings.cron_jobs['users_unconfirmed_secondary_emails_deletion_cron_worker']['job_class'] = 'Users::UnconfirmedSecondaryEmailsDeletionCronWorker'
Settings.cron_jobs['package_metadata_advisories_sync_worker'] ||= {}
Settings.cron_jobs['package_metadata_advisories_sync_worker']['cron'] ||= "*/5 * * * *"
Settings.cron_jobs['package_metadata_advisories_sync_worker']['job_class'] = 'PackageMetadata::AdvisoriesSyncWorker'
diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb
index 337ff6b0bbd..3a4d3d1138e 100644
--- a/config/initializers/postgres_partitioning.rb
+++ b/config/initializers/postgres_partitioning.rb
@@ -5,6 +5,7 @@ Gitlab::Database::Partitioning.register_models(
AuditEvent,
BatchedGitRefUpdates::Deletion,
Ci::BuildMetadata,
+ Ci::BuildExecutionConfig,
Ci::BuildName,
Ci::Catalog::Resources::Components::Usage,
Ci::Catalog::Resources::SyncEvent,
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 53cfb6c3995..21664b10899 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -88,4 +88,11 @@ namespace :import do
get :realtime_changes
post :upload
end
+
+ resources :source_users, only: [] do
+ member do
+ post :accept
+ post :decline
+ end
+ end
end
diff --git a/db/docs/p_ci_builds_execution_configs.yml b/db/docs/p_ci_builds_execution_configs.yml
new file mode 100644
index 00000000000..0901a4064fb
--- /dev/null
+++ b/db/docs/p_ci_builds_execution_configs.yml
@@ -0,0 +1,12 @@
+---
+table_name: p_ci_builds_execution_configs
+classes:
+- Ci::BuildExecutionConfig
+feature_categories:
+- pipeline_composition
+description: Represents execution config for a group of builds
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150451
+milestone: '17.0'
+gitlab_schema: gitlab_ci
+sharding_key:
+ project_id: projects
diff --git a/db/migrate/20240423064716_create_ci_build_execution_config.rb b/db/migrate/20240423064716_create_ci_build_execution_config.rb
new file mode 100644
index 00000000000..266a904629c
--- /dev/null
+++ b/db/migrate/20240423064716_create_ci_build_execution_config.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateCiBuildExecutionConfig < Gitlab::Database::Migration[2.2]
+ milestone '17.0'
+
+ def change
+ create_table(:p_ci_builds_execution_configs,
+ primary_key: [:id, :partition_id],
+ options: 'PARTITION BY LIST (partition_id)', if_not_exists: true) do |t|
+ t.bigserial :id, null: false
+ t.bigint :partition_id, null: false
+ t.bigint :project_id, null: false, index: true
+ t.bigint(:pipeline_id, null: false, index: true)
+ t.jsonb :run_steps, default: {}, null: false
+ end
+ end
+end
diff --git a/db/migrate/20240423102625_create_partitions_for_p_ci_builds_execution_configs.rb b/db/migrate/20240423102625_create_partitions_for_p_ci_builds_execution_configs.rb
new file mode 100644
index 00000000000..ad4a6f992cd
--- /dev/null
+++ b/db/migrate/20240423102625_create_partitions_for_p_ci_builds_execution_configs.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreatePartitionsForPCiBuildsExecutionConfigs < Gitlab::Database::Migration[2.2]
+ milestone '17.0'
+
+ def up
+ with_lock_retries do
+ connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_100
+ PARTITION OF p_ci_builds_execution_configs
+ FOR VALUES IN (100);
+
+ CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_101
+ PARTITION OF p_ci_builds_execution_configs
+ FOR VALUES IN (101);
+
+ CREATE TABLE IF NOT EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_102
+ PARTITION OF p_ci_builds_execution_configs
+ FOR VALUES IN (102);
+ SQL
+ end
+ end
+
+ def down
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_100;
+ DROP TABLE IF EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_101;
+ DROP TABLE IF EXISTS gitlab_partitions_dynamic.ci_builds_execution_configs_102;
+ SQL
+ end
+end
diff --git a/db/migrate/20240425091407_add_execution_config_id_to_p_ci_builds.rb b/db/migrate/20240425091407_add_execution_config_id_to_p_ci_builds.rb
new file mode 100644
index 00000000000..11d1a73e2a9
--- /dev/null
+++ b/db/migrate/20240425091407_add_execution_config_id_to_p_ci_builds.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddExecutionConfigIdToPCiBuilds < Gitlab::Database::Migration[2.2]
+ milestone '17.0'
+
+ # rubocop:disable Migration/AddColumnsToWideTables -- We need this to store run step config
+ def up
+ add_column :p_ci_builds, :execution_config_id, :bigint
+ end
+ # rubocop:enable Migration/AddColumnsToWideTables
+
+ def down
+ remove_column :p_ci_builds, :execution_config_id
+ end
+end
diff --git a/db/migrate/20240501103038_index_emails_on_created_at_where_confirmed_at_is_null.rb b/db/migrate/20240501103038_index_emails_on_created_at_where_confirmed_at_is_null.rb
new file mode 100644
index 00000000000..105e1babc98
--- /dev/null
+++ b/db/migrate/20240501103038_index_emails_on_created_at_where_confirmed_at_is_null.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class IndexEmailsOnCreatedAtWhereConfirmedAtIsNull < Gitlab::Database::Migration[2.2]
+ milestone '17.0'
+
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_emails_on_created_at_where_confirmed_at_is_null'
+
+ def up
+ add_concurrent_index :emails, :created_at, where: 'confirmed_at IS NULL', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :emails, name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20240426135340_prepare_async_index_to_execution_config_id_in_ci_build.rb b/db/post_migrate/20240426135340_prepare_async_index_to_execution_config_id_in_ci_build.rb
new file mode 100644
index 00000000000..b07b3a975e1
--- /dev/null
+++ b/db/post_migrate/20240426135340_prepare_async_index_to_execution_config_id_in_ci_build.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class PrepareAsyncIndexToExecutionConfigIdInCiBuild < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ disable_ddl_transaction!
+ milestone '17.0'
+
+ INDEX_NAME = 'index_p_ci_builds_on_execution_config_id'
+ COLUMNS = [:execution_config_id]
+
+ def up
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds) do |partition|
+ index_name = generated_index_name(partition.identifier, INDEX_NAME)
+ prepare_async_index(partition.identifier, COLUMNS, name: index_name, where: "execution_config_id IS NOT NULL")
+ end
+ end
+
+ def down
+ Gitlab::Database::PostgresPartitionedTable.each_partition(:p_ci_builds) do |partition|
+ index_name = generated_index_name(partition.identifier, INDEX_NAME)
+ unprepare_async_index(partition.identifier, index_name)
+ end
+ end
+end
diff --git a/db/post_migrate/20240502062514_add_foreign_key_from_pipeline_to_ci_builds_to_execution_configs.rb b/db/post_migrate/20240502062514_add_foreign_key_from_pipeline_to_ci_builds_to_execution_configs.rb
new file mode 100644
index 00000000000..c06569209bd
--- /dev/null
+++ b/db/post_migrate/20240502062514_add_foreign_key_from_pipeline_to_ci_builds_to_execution_configs.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddForeignKeyFromPipelineToCiBuildsToExecutionConfigs < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ milestone '17.0'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_partitioned_foreign_key(
+ :p_ci_builds_execution_configs, :ci_pipelines,
+ name: :fk_rails_c26408d02c, column: :pipeline_id,
+ on_delete: :cascade, reverse_lock_order: true
+ )
+ end
+
+ def down
+ with_lock_retries(raise_on_exhaustion: true) do
+ remove_foreign_key_if_exists(
+ :p_ci_builds_execution_configs, :ci_pipelines,
+ name: :fk_rails_c26408d02c, reverse_lock_order: true
+ )
+ end
+ end
+end
diff --git a/db/schema_migrations/20240423064716 b/db/schema_migrations/20240423064716
new file mode 100644
index 00000000000..29c00ef11fa
--- /dev/null
+++ b/db/schema_migrations/20240423064716
@@ -0,0 +1 @@
+3fd334e0639a0cc383f603738001b1a4b5bfbc94092e56c01040cbfbf515ae76
\ No newline at end of file
diff --git a/db/schema_migrations/20240423102625 b/db/schema_migrations/20240423102625
new file mode 100644
index 00000000000..834bcfc4db6
--- /dev/null
+++ b/db/schema_migrations/20240423102625
@@ -0,0 +1 @@
+f86ec5ddd56086044f26214622b2a656d21a8e258cdb24923063c3e046375cad
\ No newline at end of file
diff --git a/db/schema_migrations/20240425091407 b/db/schema_migrations/20240425091407
new file mode 100644
index 00000000000..a20462580d7
--- /dev/null
+++ b/db/schema_migrations/20240425091407
@@ -0,0 +1 @@
+324cf74a398ab88fd61018cd6bdea3ef699e91485e34a5fd02850644e9832187
\ No newline at end of file
diff --git a/db/schema_migrations/20240426135340 b/db/schema_migrations/20240426135340
new file mode 100644
index 00000000000..593d057970f
--- /dev/null
+++ b/db/schema_migrations/20240426135340
@@ -0,0 +1 @@
+091e3d00d140167fc7dba73fe46cc8df642bc1ca70b9741f0c0f8db38daea888
\ No newline at end of file
diff --git a/db/schema_migrations/20240501103038 b/db/schema_migrations/20240501103038
new file mode 100644
index 00000000000..96f07c3363a
--- /dev/null
+++ b/db/schema_migrations/20240501103038
@@ -0,0 +1 @@
+393c51beee226eadf2e9d4c5142669e2efb8f7a445bd18d6e6121727d241d9ac
\ No newline at end of file
diff --git a/db/schema_migrations/20240502062514 b/db/schema_migrations/20240502062514
new file mode 100644
index 00000000000..f904b8d6915
--- /dev/null
+++ b/db/schema_migrations/20240502062514
@@ -0,0 +1 @@
+43e1ebef09400b5199d9d2e1bb906bab62ee8530cbd8a785f2068f5f9b2f55d1
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 641abffb167..45ab1acb41d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1131,10 +1131,20 @@ CREATE TABLE p_ci_builds (
trigger_request_id_convert_to_bigint bigint,
upstream_pipeline_id bigint,
user_id bigint,
+ execution_config_id bigint,
CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL))
)
PARTITION BY LIST (partition_id);
+CREATE TABLE p_ci_builds_execution_configs (
+ id bigint NOT NULL,
+ partition_id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ pipeline_id bigint NOT NULL,
+ run_steps jsonb DEFAULT '{}'::jsonb NOT NULL
+)
+PARTITION BY LIST (partition_id);
+
CREATE TABLE p_ci_builds_metadata (
project_id integer NOT NULL,
timeout integer,
@@ -6264,6 +6274,7 @@ CREATE TABLE ci_builds (
trigger_request_id_convert_to_bigint bigint,
upstream_pipeline_id bigint,
user_id bigint,
+ execution_config_id bigint,
CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL))
);
@@ -12550,6 +12561,15 @@ CREATE SEQUENCE p_catalog_resource_sync_events_id_seq
ALTER SEQUENCE p_catalog_resource_sync_events_id_seq OWNED BY p_catalog_resource_sync_events.id;
+CREATE SEQUENCE p_ci_builds_execution_configs_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE p_ci_builds_execution_configs_id_seq OWNED BY p_ci_builds_execution_configs.id;
+
CREATE SEQUENCE p_ci_job_annotations_id_seq
START WITH 1
INCREMENT BY 1
@@ -19610,6 +19630,8 @@ ALTER TABLE ONLY p_catalog_resource_component_usages ALTER COLUMN id SET DEFAULT
ALTER TABLE ONLY p_catalog_resource_sync_events ALTER COLUMN id SET DEFAULT nextval('p_catalog_resource_sync_events_id_seq'::regclass);
+ALTER TABLE ONLY p_ci_builds_execution_configs ALTER COLUMN id SET DEFAULT nextval('p_ci_builds_execution_configs_id_seq'::regclass);
+
ALTER TABLE ONLY p_ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('ci_builds_metadata_id_seq'::regclass);
ALTER TABLE ONLY p_ci_job_annotations ALTER COLUMN id SET DEFAULT nextval('p_ci_job_annotations_id_seq'::regclass);
@@ -21939,6 +21961,9 @@ ALTER TABLE ONLY p_catalog_resource_sync_events
ALTER TABLE ONLY p_ci_build_names
ADD CONSTRAINT p_ci_build_names_pkey PRIMARY KEY (build_id, partition_id);
+ALTER TABLE ONLY p_ci_builds_execution_configs
+ ADD CONSTRAINT p_ci_builds_execution_configs_pkey PRIMARY KEY (id, partition_id);
+
ALTER TABLE ONLY p_ci_finished_build_ch_sync_events
ADD CONSTRAINT p_ci_finished_build_ch_sync_events_pkey PRIMARY KEY (build_id, partition);
@@ -25404,6 +25429,8 @@ CREATE INDEX index_elasticsearch_indexed_namespaces_on_created_at ON elasticsear
CREATE UNIQUE INDEX index_emails_on_confirmation_token ON emails USING btree (confirmation_token);
+CREATE INDEX index_emails_on_created_at_where_confirmed_at_is_null ON emails USING btree (created_at) WHERE (confirmed_at IS NULL);
+
CREATE UNIQUE INDEX index_emails_on_email ON emails USING btree (email);
CREATE INDEX index_emails_on_user_id ON emails USING btree (user_id);
@@ -26522,6 +26549,10 @@ CREATE INDEX index_p_ci_build_names_on_project_id_and_build_id ON ONLY p_ci_buil
CREATE INDEX index_p_ci_build_names_on_search_vector ON ONLY p_ci_build_names USING gin (search_vector);
+CREATE INDEX index_p_ci_builds_execution_configs_on_pipeline_id ON ONLY p_ci_builds_execution_configs USING btree (pipeline_id);
+
+CREATE INDEX index_p_ci_builds_execution_configs_on_project_id ON ONLY p_ci_builds_execution_configs USING btree (project_id);
+
CREATE INDEX index_p_ci_finished_build_ch_sync_events_finished_at ON ONLY p_ci_finished_build_ch_sync_events USING btree (partition, build_finished_at);
CREATE UNIQUE INDEX index_p_ci_job_annotations_on_partition_id_job_id_name ON ONLY p_ci_job_annotations USING btree (partition_id, job_id, name);
@@ -32469,6 +32500,9 @@ ALTER TABLE ONLY labels
ALTER TABLE ONLY project_feature_usages
ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+ALTER TABLE p_ci_builds_execution_configs
+ ADD CONSTRAINT fk_rails_c26408d02c FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY user_canonical_emails
ADD CONSTRAINT fk_rails_c2bd828b51 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index b03d880aa77..c2896f1be26 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -271,6 +271,78 @@ gitlab_rails['omniauth_providers'] = [
Microsoft has documented how its platform works with [the OIDC protocol](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc).
+#### Migrate to Generic OpenID Connect configuration
+
+You can migrate to the Generic OpenID Connect configuration from both `azure_activedirectory_v2` and `azure_oauth2`.
+
+First, set the `uid_field`, which differs between providers:
+
+| Provider | `uid` | Supporting information |
+|-----------------------------------------------------------------------------------------------------------------|-------|-----------------------------------------------------------------------|
+| [`omniauth-azure-oauth2`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/omniauth-azure-oauth2) | `sub` | Additional attributes `oid` and `tid` are offered within the `info` object. |
+| [`omniauth-azure-activedirectory-v2`](https://github.com/RIPAGlobal/omniauth-azure-activedirectory-v2/) | `oid` | You must configure `oid` as `uid_field` when migrating. |
+| [`omniauth_openid_connect`](https://github.com/omniauth/omniauth_openid_connect/) | `sub` | Specify `uid_field` to use another field. |
+
+To migrate to the Generic OpenID Connect configuration, you must change the configuration to the following:
+
+::Tabs
+
+:::Azure OAuth 2.0
+
+```ruby
+gitlab_rails['omniauth_providers'] = [
+ {
+ name: "azure_oauth2",
+ label: "Azure OIDC", # optional label for login button, defaults to "Openid Connect"
+ args: {
+ name: "azure_activedirectory_v2",
+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
+ scope: ["openid", "profile", "email"],
+ response_type: "code",
+ issuer: "https://login.microsoftonline.com//v2.0",
+ client_auth_method: "query",
+ discovery: true,
+ uid_field: "sub",
+ send_scope_to_token_endpoint: "false",
+ client_options: {
+ identifier: "",
+ secret: "",
+ redirect_uri: "https://gitlab.example.com/users/auth/azure_oauth2/callback"
+ }
+ }
+ }
+]
+```
+
+:::Azure Active Directory v2
+
+```ruby
+gitlab_rails['omniauth_providers'] = [
+ {
+ name: "azure_oauth2",
+ label: "Azure OIDC", # optional label for login button, defaults to "Openid Connect"
+ args: {
+ name: "azure_activedirectory_v2",
+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
+ scope: ["openid", "profile", "email"],
+ response_type: "code",
+ issuer: "https://login.microsoftonline.com//v2.0",
+ client_auth_method: "query",
+ discovery: true,
+ uid_field: "oid",
+ send_scope_to_token_endpoint: "false",
+ client_options: {
+ identifier: "",
+ secret: "",
+ redirect_uri: "https://gitlab.example.com/users/auth/azure_activedirectory_v2/callback"
+ }
+ }
+ }
+]
+```
+
+::EndTabs
+
### Configure Microsoft Azure Active Directory B2C
GitLab requires special
@@ -610,7 +682,7 @@ You can configure your application to use multiple OpenID Connect (OIDC) provide
You should do this in either of the following scenarios:
-- [Migrating to the OpenID Connect protocol](../../integration/azure.md#migrate-to-the-openid-connect-protocol).
+- [Migrating to the OpenID Connect protocol](#migrate-to-generic-openid-connect-configuration).
- Offering different levels of authentication.
NOTE:
diff --git a/doc/administration/postgresql/multiple_databases.md b/doc/administration/postgresql/multiple_databases.md
index 4b4d1f4f043..6f72a61a089 100644
--- a/doc/administration/postgresql/multiple_databases.md
+++ b/doc/administration/postgresql/multiple_databases.md
@@ -192,6 +192,33 @@ If something unexpected happens during the migration, it is safe to start over.
1. Configure GitLab to [use multiple databases](#set-up-multiple-databases).
+### Existing Linux package installations using streaming replication
+
+To reduce downtime, you can set up streaming replication to migrate existing data from the `main` database to the `ci` database.
+This procedure results in two database clusters.
+
+This procedure can be both time- and resource-consuming.
+Consider their trade-offs with availability before executing it.
+
+To set up streaming replication for creating two database clusters:
+
+1. Set up streaming replication from the GitLab database to new database instance.
+1. When the new replica has caught up, [disable background migrations](../../development/database/batched_background_migrations.md#enable-or-disable-background-migrations).
+1. [Ensure all background migrations are finished](../../update/background_migrations.md#check-the-status-of-batched-background-migrations).
+1. Stop GitLab, except for PostgreSQL:
+
+ ```shell
+ sudo gitlab-ctl stop
+ sudo gitlab-ctl start postgresql
+ ```
+
+1. After the replication is complete, stop the streaming replication, and promote the replica to a primary instance.
+ You now have two database clusters, one for `main`, and one for `ci`.
+1. Configure GitLab to [use multiple databases](#set-up-multiple-databases).
+
+For more information on how to set up Streaming Replication,
+see [PostgreSQL replication and failover for Linux package installations](replication_and_failover.md).
+
## Set up multiple databases
To configure GitLab to use multiple application databases, follow the instructions below for your installation type.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index cfc882bd1f7..561f34b8046 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -477,6 +477,24 @@ Fields related to Instance Security Dashboard.
Returns [`InstanceSecurityDashboard`](#instancesecuritydashboard).
+### `Query.integrationExclusions`
+
+DETAILS:
+**Introduced** in GitLab 17.0.
+**Status**: Experiment.
+
+Returns [`IntegrationExclusionConnection`](#integrationexclusionconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of integration. |
+
### `Query.issue`
Find an issue.
@@ -5428,6 +5446,54 @@ Input type: `InstanceGoogleCloudLoggingConfigurationUpdateInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `instanceGoogleCloudLoggingConfiguration` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | configuration updated. |
+### `Mutation.integrationExclusionCreate`
+
+DETAILS:
+**Introduced** in GitLab 17.0.
+**Status**: Experiment.
+
+Input type: `IntegrationExclusionCreateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of integration to exclude. |
+| `projectIds` | [`[ProjectID!]!`](#projectid) | Ids of projects to exclude. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `exclusions` | [`[IntegrationExclusion!]`](#integrationexclusion) | Integration exclusions created by the mutation. |
+
+### `Mutation.integrationExclusionDelete`
+
+DETAILS:
+**Introduced** in GitLab 17.0.
+**Status**: Experiment.
+
+Input type: `IntegrationExclusionDeleteInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of integration. |
+| `projectIds` | [`[ProjectID!]!`](#projectid) | Id of excluded project. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `exclusions` | [`[IntegrationExclusion!]`](#integrationexclusion) | Project no longer excluded due to the mutation. |
+
### `Mutation.issuableResourceLinkCreate`
Input type: `IssuableResourceLinkCreateInput`
@@ -12526,6 +12592,29 @@ The edge type for [`InstanceGoogleCloudLoggingConfigurationType`](#instancegoogl
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | The item at the end of the edge. |
+#### `IntegrationExclusionConnection`
+
+The connection type for [`IntegrationExclusion`](#integrationexclusion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `edges` | [`[IntegrationExclusionEdge]`](#integrationexclusionedge) | A list of edges. |
+| `nodes` | [`[IntegrationExclusion]`](#integrationexclusion) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `IntegrationExclusionEdge`
+
+The edge type for [`IntegrationExclusion`](#integrationexclusion).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`IntegrationExclusion`](#integrationexclusion) | The item at the end of the edge. |
+
#### `IssuableResourceLinkConnection`
The connection type for [`IssuableResourceLink`](#issuableresourcelink).
@@ -22949,6 +23038,16 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. |
| `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. |
+### `IntegrationExclusion`
+
+An integration to override the level settings of instance specific integrations.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `project` | [`Project`](#project) | Project that has been excluded from the instance specific integration. |
+
### `IssuableResourceLink`
Describes an issuable resource link for incident issues.
@@ -33410,6 +33509,14 @@ Import source.
| `MANIFEST` | Imported from Manifest. |
| `NONE` | Not imported. |
+### `IntegrationType`
+
+Integration Names.
+
+| Value | Description |
+| ----- | ----------- |
+| `BEYOND_IDENTITY` | Beyond Identity. |
+
### `IssuableResourceLinkType`
Issuable resource link type enum.
diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md
index 50d5f8304d5..5b970a26756 100644
--- a/doc/ci/components/index.md
+++ b/doc/ci/components/index.md
@@ -172,6 +172,39 @@ You can use any version supported by the component, but using a version publishe
to the CI/CD catalog is recommended. The version referenced with a commit SHA or branch name
might not be published in the CI/CD catalog, but could be used for testing.
+#### Semantic version ranges
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450835) in GitLab 16.11
+
+When [referencing a CI/CD catalog component](#component-versions), you can use a
+special format to specify the latest [semantic version](#semantic-versioning) in a range.
+
+To specify the latest release of:
+
+- A minor version, use both the major and minor version numbers in the reference,
+ but not the patch version number. For example, use `1.1` to use the latest version
+ that starts with `1.1`, including `1.1.0` or `1.1.9`, but not `1.2.0`.
+- A major version, use only the major version number in the reference. For example,
+ use `1` to use the latest version that starts with `1.`, like `1.0.0` or `1.9.9`,
+ but not `2.0.0`.
+- All versions, use `~latest` to use the latest released version.
+
+For example, a component is released in this exact order:
+
+1. `1.0.0`
+1. `1.1.0`
+1. `2.0.0`
+1. `1.1.1`
+1. `1.2.0`
+1. `2.1.0`
+1. `2.0.1`
+
+In this example, referencing the component with:
+
+- `1` would use the `1.2.0` version.
+- `1.1` would use the `1.1.1` version.
+- `~latest` would use the `2.1.0` version.
+
## CI/CD Catalog
DETAILS:
@@ -280,39 +313,6 @@ for communicating that a change is a major, minor, patch, or other kind of chang
For example, `1.0.0`, `2.3.4`, and `1.0.0-alpha` are all valid semantic versions.
-##### Semantic version ranges
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/450835) in GitLab 16.11
-
-When [referencing a CI/CD catalog component](#component-versions), you can use a
-special format to specify the latest version in a range.
-
-To specify the latest release of:
-
-- A minor version, use both the major and minor version numbers in the reference,
- but not the patch version number. For example, use `1.1` to use the latest version
- that starts with `1.1`, including `1.1.0` or `1.1.9`, but not `1.2.0`.
-- A major version, use only the major version number in the reference. For example,
- use `1` to use the latest version that starts with `1.`, like `1.0.0` or `1.9.9`,
- but not `2.0.0`.
-- All versions, use `~latest` to use the latest released version.
-
-For example, a component is released in this exact order:
-
-1. `1.0.0`
-1. `1.1.0`
-1. `2.0.0`
-1. `1.1.1`
-1. `1.2.0`
-1. `2.1.0`
-1. `2.0.1`
-
-In this example, referencing the component with:
-
-- `1` would use the `1.2.0` version.
-- `1.1` would use the `1.1.1` version.
-- `~latest` would use the `2.1.0` version.
-
### Unpublish a component project
To remove a component project from the catalog, turn off the [**CI/CD Catalog resource**](#set-a-component-project-as-a-catalog-project)
diff --git a/doc/development/pipelines/index.md b/doc/development/pipelines/index.md
index bd063667c8b..53c0ce526e4 100644
--- a/doc/development/pipelines/index.md
+++ b/doc/development/pipelines/index.md
@@ -13,8 +13,12 @@ which itself includes files under
for easier maintenance.
We're striving to [dogfood](https://handbook.gitlab.com/handbook/engineering/development/principles/#dogfooding)
-GitLab [CI/CD features and best-practices](../../ci/index.md)
-as much as possible.
+GitLab [CI/CD features and best-practices](../../ci/index.md) as much as possible.
+
+Do not use [CI/CD components](../../ci/components/index.md) in `gitlab-org/gitlab` pipelines
+unless they are mirrored on the `dev.gitlab.com` instance. CI/CD components do not work across different instances,
+and [cause failing pipelines](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17683#note_1795756077)
+on the `dev.gitlab.com` mirror if they do not exist on that instance.
## Predictive test jobs before a merge request is approved
diff --git a/doc/development/search/advanced_search_migration_styleguide.md b/doc/development/search/advanced_search_migration_styleguide.md
index f20153c98c0..58ea53673d9 100644
--- a/doc/development/search/advanced_search_migration_styleguide.md
+++ b/doc/development/search/advanced_search_migration_styleguide.md
@@ -58,6 +58,27 @@ To update Elastic index mappings, apply the configuration to the respective file
Migrations can be built with a retry limit and have the ability to be [failed and marked as halted](https://gitlab.com/gitlab-org/gitlab/-/blob/66e899b6637372a4faf61cfd2f254cbdd2fb9f6d/ee/lib/elastic/migration.rb#L40).
Any data or index cleanup needed to support migration retries should be handled in the migration.
+### Skipped migrations
+
+You can skip a migration by adding a `skip_if` proc which evaluates to `true` or `false`:
+
+```ruby
+class MigrationName < Elastic::Migration
+ skip_if ->() { true|false }
+```
+
+The migration is executed only if the condition is `false`. Skipped migrations will not be shown as part of pending migrations.
+
+Skipped migrations can be marked as obsolete, but the `skip_if` condition must be kept so that these migrations are always skipped.
+Once a skipped migration is obsolete, the only way to apply the change is by [recreating the index from scratch](../../integration/advanced_search/elasticsearch_troubleshooting.md#last-resort-to-recreate-an-index).
+
+Update the skipped migration's documentation file with the following attributes:
+
+```yaml
+skippable: true
+skip_condition: ''
+```
+
### Migration helpers
The following migration helpers are available in `ee/app/workers/concerns/elastic/`:
@@ -176,6 +197,8 @@ class MigrationName < Elastic::Migration
end
```
+When marking a skippable migration as obsolete, you must keep the `skip_if` condition.
+
#### `Elastic::MigrationCreateIndex`
Creates a new index.
@@ -455,6 +478,7 @@ being upgraded to, we do the following:
include Elastic::MigrationObsolete
```
+1. When marking a skippable migration as obsolete, keep the `skip_if` condition.
1. Delete any spec files to support this migration.
1. Verify that there are no references of the migration in the `.rubocop_todo/` directory.
1. Remove any logic handling backwards compatibility for this migration. You
diff --git a/doc/integration/advanced_search/elasticsearch.md b/doc/integration/advanced_search/elasticsearch.md
index 22b6be54fab..14aba7ede0e 100644
--- a/doc/integration/advanced_search/elasticsearch.md
+++ b/doc/integration/advanced_search/elasticsearch.md
@@ -655,6 +655,14 @@ any pending migrations that have been removed in the new version cannot be execu
In this case, you must
[re-create your index from scratch](elasticsearch_troubleshooting.md#last-resort-to-recreate-an-index).
+### Skippable migrations
+
+Skippable migrations are only executed when a condition is met.
+For example, if a migration depends on a specific version of Elasticsearch, it could be skipped until that version is reached.
+
+If a skippable migration is not executed by the time the migration is marked as obsolete, to apply the change you must
+[re-create the index](elasticsearch_troubleshooting.md#last-resort-to-recreate-an-index).
+
## GitLab advanced search Rake tasks
Rake tasks are available to:
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 4740fc447a5..8dcb45b64da 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -11,164 +11,7 @@ DETAILS:
**Offering:** Self-managed
You can enable the Microsoft Azure OAuth 2.0 OmniAuth provider and sign in to
-GitLab with your Microsoft Azure credentials. You can configure the provider that uses
-[the earlier Azure Active Directory v1.0 endpoint](https://learn.microsoft.com/en-us/previous-versions/azure/active-directory/azuread-dev/v1-protocols-oauth-code),
-or the provider that uses the v2.0 endpoint.
-
-NOTE:
-For new projects, Microsoft suggests you use the
-[OpenID Connect protocol](../administration/auth/oidc.md#configure-microsoft-azure),
-which uses the Microsoft identity platform (v2.0) endpoint.
-
-## Migrate to the OpenID Connect protocol
-
-To migrate to the OpenID Connect protocol, see [configure multiple OpenID Connect providers](../administration/auth/oidc.md#configure-multiple-openid-connect-providers).
-
-You must set the `uid_field`, which differs across the providers:
-
-| Provider | `uid` | Remarks |
-|-----------------------------------------------------------------------------------------------------------------|-------|-----------------------------------------------------------------------|
-| [`omniauth-azure-oauth2`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/omniauth-azure-oauth2) | `sub` | Additional attributes `oid`, `tid` are offered within the info object |
-| [`omniauth-azure-activedirectory-v2`](https://github.com/RIPAGlobal/omniauth-azure-activedirectory-v2/) | `oid` | You must configure `oid` as `uid_field` when migrating |
-| [`omniauth_openid_connect`](https://github.com/omniauth/omniauth_openid_connect/) | `sub` | Specify `uid_field` to use another field |
-
-To migrate from `omniauth-azure-oauth2` to `omniauth_openid_connect` you
-must change the configuration.
-
-::Tabs
-
-:::TabTitle Linux package (Omnibus)
-
-Remove some of the existing configuration and add new configuration as shown.
-
-```diff
-gitlab_rails['omniauth_providers'] = [
- {
- name: "azure_oauth2",
- # label: "Provider name", # optional label for login button, defaults to "Azure AD"
- args: {
-+ name: "azure_oauth2",
-+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
-+ scope: ["openid", "profile", "email"],
-+ response_type: "code",
-+ issuer: "https://login.microsoftonline.com//v2.0",
-+ client_auth_method: "query",
-+ discovery: true,
-+ uid_field: "sub",
-+ client_options: {
-+ identifier: "",
-+ secret: "",
-+ redirect_uri: "https://gitlab.example.com/users/auth/azure_oauth2/callback"
-+ }
-- client_id: "",
-- client_secret: "",
-- tenant_id: "",
- }
- }
-]
-```
-
-:::TabTitle Self-compiled (source)
-
-Remove some of the existing configuration and add new configuration as shown.
-
-```diff
- - { name: 'azure_oauth2',
- # label: 'Provider name', # optional label for login button, defaults to "Azure AD"
-- args: { client_id: '',
-- client_secret: '',
-- tenant_id: '' } }
-+ icon: "",
-+ args: {
-+ name: "azure_oauth2",
-+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
-+ scope: ["openid","profile","email"],
-+ response_type: "code",
-+ issuer: 'https://login.microsoftonline.com//v2.0',
-+ discovery: true,
-+ client_auth_method: 'query',
-+ uid_field: 'sub',
-+ send_scope_to_token_endpoint: "false",
-+ client_options: {
-+ identifier: "",
-+ secret: "",
-+ redirect_uri: "/users/auth/azure_oauth2/callback"
-+ }
-+ }
- }
-```
-
-::EndTabs
-
-To migrate for example from `omniauth-azure-activedirectory-v2` to `omniauth_openid_connect` you
-must change the configuration.
-
-::Tabs
-
-:::TabTitle Linux package (Omnibus)
-
-Remove some of the existing configuration and add new configuration as shown.
-
-```diff
-gitlab_rails['omniauth_providers'] = [
- {
- - name: "azure_activedirectory_v2",
- # label: "Provider name", # optional label for login button, defaults to "Azure AD v2"
- args: {
-+ name: "azure_activedirectory_v2",
-+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
-+ scope: ["openid", "profile", "email"],
-+ response_type: "code",
-+ issuer: "https://login.microsoftonline.com//v2.0",
-+ client_auth_method: "query",
-+ discovery: true,
-+ uid_field: "oid",
-+ client_options: {
-+ identifier: "",
-+ secret: "",
-+ redirect_uri: "https://gitlab.example.com/users/auth/azure_activedirectory_v2/callback"
-+ }
-- client_id: "",
-- client_secret: "",
-- tenant_id: "",
- }
- }
-]
-```
-
-:::TabTitle Self-compiled (source)
-
-Remove some of the existing configuration and add new configuration as shown.
-
-```diff
- - { name: 'azure_activedirectory_v2',
- # label: 'Provider name', # optional label for login button, defaults to "Azure AD v2"
-- args: { client_id: '',
-- client_secret: '',
-- tenant_id: '' } }
-+ icon: "",
-+ args: {
-+ name: "azure_activedirectory_v2",
-+ strategy_class: "OmniAuth::Strategies::OpenIDConnect",
-+ scope: ["openid","profile","email"],
-+ response_type: "code",
-+ issuer: 'https://login.microsoftonline.com//v2.0',
-+ discovery: true,
-+ client_auth_method: 'query',
-+ uid_field: 'oid',
-+ send_scope_to_token_endpoint: "false",
-+ client_options: {
-+ identifier: "",
-+ secret: "",
-+ redirect_uri: "/users/auth/azure_activedirectory_v2/callback"
-+ }
-+ }
- }
-```
-
-::EndTabs
-
-For more information on other customizations, see [`gitlab_username_claim`](omniauth.md#per-provider-configuration).
+GitLab with your Microsoft Azure credentials.
## Register an Azure application
@@ -180,9 +23,7 @@ an Azure application and get a client ID and secret key.
1. [Register an application](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app)
and provide the following information:
- The redirect URI, which requires the URL of the Azure OAuth callback of your GitLab
- installation. For example:
- - For the v1.0 endpoint: `https://gitlab.example.com/users/auth/azure_oauth2/callback`.
- - For the v2.0 endpoint: `https://gitlab.example.com/users/auth/azure_activedirectory_v2/callback`.
+ installation. `https://gitlab.example.com/users/auth/azure_activedirectory_v2/callback`.
- The application type, which must be set to **Web**.
1. Save the client ID and client secret. The client secret is only
displayed once.
@@ -195,7 +36,7 @@ In some Microsoft documentation, the terms are named `Application ID` and
## Add API permissions (scopes)
-If you're using the v2.0 endpoint, after you create the application, [configure it to expose a web API](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis).
+After you create the application, [configure it to expose a web API](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-configure-app-expose-web-apis).
Add the following delegated permissions under the Microsoft Graph API:
- `email`
@@ -206,6 +47,11 @@ Alternatively, add the `User.Read.All` application permission.
## Enable Microsoft OAuth in GitLab
+NOTE:
+For new projects, you should use the
+[OpenID Connect protocol](../administration/auth/oidc.md#configure-microsoft-azure),
+which uses the Microsoft identity platform (v2.0) endpoint.
+
1. On your GitLab server, open the configuration file.
- For Linux package installations:
@@ -223,7 +69,7 @@ Alternatively, add the `User.Read.All` application permission.
```
1. Configure the [common settings](omniauth.md#configure-common-settings)
- to add `azure_oauth2` as a single sign-on provider. This enables Just-In-Time
+ to add `azure_activedirectory_v2` as a single sign-on provider. This enables Just-In-Time
account provisioning for users who do not have an existing GitLab account.
1. Add the provider configuration. Replace ``, ``, and ``
@@ -231,24 +77,6 @@ Alternatively, add the `User.Read.All` application permission.
- For Linux package installations:
- For the v1.0 endpoint:
-
- ```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: "azure_oauth2",
- # label: "Provider name", # optional label for login button, defaults to "Azure AD"
- args: {
- client_id: "",
- client_secret: "",
- tenant_id: "",
- }
- }
- ]
- ```
-
- For the v2.0 endpoint:
-
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -261,9 +89,10 @@ Alternatively, add the `User.Read.All` application permission.
}
}
]
+
```
- For [alternative Azure clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud),
+ - For [alternative Azure clouds](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud),
configure `base_azure_url` under the `args` section. For example, for Azure Government Community Cloud (GCC):
```ruby
@@ -283,16 +112,6 @@ Alternatively, add the `User.Read.All` application permission.
- For self-compiled installations:
- For the v1.0 endpoint:
-
- ```yaml
- - { name: 'azure_oauth2',
- # label: 'Provider name', # optional label for login button, defaults to "Azure AD"
- args: { client_id: '',
- client_secret: '',
- tenant_id: '' } }
- ```
-
For the v2.0 endpoint:
```yaml
diff --git a/doc/subscriptions/subscription-add-ons.md b/doc/subscriptions/subscription-add-ons.md
index 0dd053f7521..4f813b57425 100644
--- a/doc/subscriptions/subscription-add-ons.md
+++ b/doc/subscriptions/subscription-add-ons.md
@@ -49,6 +49,20 @@ After you purchase GitLab Duo Pro, you can assign seats to billable users to gra
To use Code Suggestions in any project or group, a user must be assigned a seat in at least one top-level group.
+#### Enable automatic assignment of new users
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/13637) in GitLab 17.0 [with a flag](../administration/feature_flags.md) named `auto_assign_gitlab_duo_pro_seats`.
+
+You can enable automatic assignment of GitLab Duo Pro seats for new users. When this feature is enabled,
+any member added to a top-level group, subgroup, or project is automatically allocated a GitLab Duo Pro
+seat if one is available.
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > General**.
+1. Expand **Permissions and group features**.
+1. Locate **GitLab Duo Pro seats**.
+1. Select the **Automatic assignment of GitLab Duo Pro seats** checkbox.
+
### For self-managed
Prerequisites:
@@ -69,7 +83,7 @@ Prerequisites:
For self-managed instances, to enable GitLab Duo features,
You must [enable network connectivity](../user/ai_features_enable.md#configure-gitlab-duo-on-a-self-managed-instance).
-## Assign and remove seats in bulk
+## Assign and remove GitLab Duo Pro seats in bulk
You can assign or remove seats in bulk for multiple users.
diff --git a/doc/user/okrs.md b/doc/user/okrs.md
index 30165965716..4aa44f8177a 100644
--- a/doc/user/okrs.md
+++ b/doc/user/okrs.md
@@ -537,6 +537,7 @@ If you find a bug, [comment on the feedback issue](https://gitlab.com/gitlab-org
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139394) in GitLab 16.7.
> - Adding related items by entering their URLs and IDs [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/427594) in GitLab 16.8.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150148) in GitLab 17.0. Feature flag `linked_work_items` removed.
+> - [Changed](https://gitlab.com/groups/gitlab-org/-/epics/10267) minimum required role from Reporter (if true) to Guest in GitLab 17.0.
Linked items are a bi-directional relationship and appear in a block below
the Child objectives and key results. You can link an objective, key result, or a task in the same project with each other.
diff --git a/doc/user/packages/generic_packages/index.md b/doc/user/packages/generic_packages/index.md
index 6727f3bdc13..e90f38af016 100644
--- a/doc/user/packages/generic_packages/index.md
+++ b/doc/user/packages/generic_packages/index.md
@@ -237,6 +237,15 @@ It also demonstrates how to manage a semantic version for the generic package: s
## Troubleshooting
+### HTTP 403 errors
+
+You might get a `HTTP 403 Forbidden` error. This error happens when either:
+
+- You don't have access to a resource.
+- The package registry is not enabled for the project.
+
+To resolve the issue, ensure the package registry is enabled, and you have permission to access it.
+
### Internal Server error on large file uploads to S3
S3-compatible object storage [limits the size of a single PUT request to 5 GB](https://docs.aws.amazon.com/AmazonS3/latest/userguide/upload-objects.html). If the `aws_signature_version` is set to `2` in the [object storage connection settings](../../../administration/object_storage.md), attempting to publish a package file larger than the 5 GB limit can result in a `HTTP 500: Internal Server Error` response.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 2c5e9ee0e07..9cb0468d710 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -76,12 +76,16 @@ NOTE:
## Delete emails from your user profile
+> - Automatic deletion of unverified secondary email addresses [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151562) in GitLab 17.0.
+
You can delete a secondary email address from your account. You cannot delete your
primary email address.
If the deleted email address is used for any user emails, those user emails are
sent to the primary email address instead.
+Unverified secondary email addresses are automatically deleted after three days.
+
NOTE:
Because of [issue 438600](https://gitlab.com/gitlab-org/gitlab/-/issues/438600), group notifications are still sent to
the deleted email address.
diff --git a/doc/user/profile/service_accounts.md b/doc/user/profile/service_accounts.md
index b4ffc7205ca..394027390e1 100644
--- a/doc/user/profile/service_accounts.md
+++ b/doc/user/profile/service_accounts.md
@@ -135,9 +135,21 @@ Prerequisites:
Use the groups API to [rotate the personal access token](../../api/groups.md#rotate-a-personal-access-token-for-service-account-user) for a service account user.
+### Delete a service account
+
+Prerequisites:
+
+- You must be an administrator for the instance the service account is associated with.
+
+To delete a service account, [use the API to delete the service account user](../../api/users.md#user-deletion).
+
### Disable a service account
-You cannot directly disable or delete a service account. Instead, you must:
+Prerequisites:
+
+- You must have the Owner role for the group the service account is associated with.
+
+If you are not an administrator for the instance or group a service account is associated with, you cannot directly delete that service account. Instead:
1. Remove the service account as a member of all subgroups and projects:
diff --git a/doc/user/project/issues/related_issues.md b/doc/user/project/issues/related_issues.md
index 608d25f0c04..29128da9f31 100644
--- a/doc/user/project/issues/related_issues.md
+++ b/doc/user/project/issues/related_issues.md
@@ -10,6 +10,8 @@ DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
+> - [Changed](https://gitlab.com/groups/gitlab-org/-/epics/10267) minimum required role from Reporter (if true) to Guest in GitLab 17.0.
+
Linked issues are a bi-directional relationship between any two issues and appear in a block below
the issue description. You can link issues in different projects.
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index fb4b577bb23..151db92b283 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -541,6 +541,7 @@ If you find a bug, [comment on the feedback issue](https://gitlab.com/gitlab-org
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139394) in GitLab 16.7.
> - Adding related items by entering their URLs and IDs [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/427594) in GitLab 16.8.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150148) in GitLab 17.0. Feature flag `linked_work_items` removed.
+> - [Changed](https://gitlab.com/groups/gitlab-org/-/epics/10267) minimum required role from Reporter (if true) to Guest in GitLab 17.0.
Linked items are a bi-directional relationship and appear in a block below
the emoji reactions section. You can link an objective, key result, or a task in the same project with each other.
diff --git a/lib/bulk_imports/ndjson_pipeline.rb b/lib/bulk_imports/ndjson_pipeline.rb
index d7976389c80..a038d445f0b 100644
--- a/lib/bulk_imports/ndjson_pipeline.rb
+++ b/lib/bulk_imports/ndjson_pipeline.rb
@@ -26,7 +26,8 @@ module BulkImports
members_mapper: members_mapper,
object_builder: object_builder,
user: context.current_user,
- excluded_keys: import_export_config.relation_excluded_keys(key)
+ excluded_keys: import_export_config.relation_excluded_keys(key),
+ import_source: Import::SOURCE_DIRECT_TRANSFER
)
end
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index 19826138075..4cd383026dd 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -6,7 +6,7 @@ module Gitlab
class BaseHandler
attr_reader :mail, :mail_key
- HANDLER_ACTION_BASE_REGEX ||= /(?.+)-(?\d+)/
+ HANDLER_ACTION_BASE_REGEX = /(?.+)-(?\d+)/
def initialize(mail, mail_key)
@mail = mail
diff --git a/lib/gitlab/import_export/base/relation_factory.rb b/lib/gitlab/import_export/base/relation_factory.rb
index c3021f034cd..966fd15270e 100644
--- a/lib/gitlab/import_export/base/relation_factory.rb
+++ b/lib/gitlab/import_export/base/relation_factory.rb
@@ -46,7 +46,8 @@ module Gitlab
relation_name.to_s.constantize
end
- def initialize(relation_sym:, relation_index:, relation_hash:, members_mapper:, object_builder:, user:, importable:, excluded_keys: [])
+ # rubocop:disable Metrics/ParameterLists -- Keyword arguments are not adding complexity to initializer
+ def initialize(relation_sym:, relation_index:, relation_hash:, members_mapper:, object_builder:, user:, importable:, excluded_keys: [], import_source: nil)
@relation_sym = relation_sym
@relation_name = self.class.overrides[relation_sym]&.to_sym || relation_sym
@relation_index = relation_index
@@ -55,6 +56,7 @@ module Gitlab
@object_builder = object_builder
@user = user
@importable = importable
+ @import_source = import_source
@imported_object_retries = 0
@relation_hash[importable_column_name] = @importable.id
@original_user = {}
@@ -67,6 +69,7 @@ module Gitlab
# from the object attributes and the export will fail.
@relation_hash.except!(*excluded_keys)
end
+ # rubocop:enable Metrics/ParameterLists
# Creates an object from an actual model with name "relation_sym" with params from
# the relation_hash, updating references with new object IDs, mapping users using
@@ -182,6 +185,10 @@ module Gitlab
existing_or_new_object.importing = true
end
+ if @import_source && existing_or_new_object.respond_to?(:imported_from)
+ existing_or_new_object.imported_from = @import_source
+ end
+
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
# as the operation is not atomic, retry in the unlikely scenario an INSERT is
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f8c57413a0f..92490bc2450 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7517,9 +7517,6 @@ msgstr ""
msgid "Authors: %{authors}"
msgstr ""
-msgid "Auto Assign GitLab Duo Pro Seats"
-msgstr ""
-
msgid "Auto DevOps"
msgstr ""
@@ -13708,6 +13705,9 @@ msgstr ""
msgid "Confirm password"
msgstr ""
+msgid "Confirm this email address within %{cut_off_days} days, otherwise the email address is removed."
+msgstr ""
+
msgid "Confirm user"
msgstr ""
@@ -23253,6 +23253,9 @@ msgstr ""
msgid "GitLab Community Edition"
msgstr ""
+msgid "GitLab Duo Pro seats"
+msgstr ""
+
msgid "GitLab Duo could not connect to the AI provider."
msgstr ""
@@ -24977,6 +24980,12 @@ msgstr ""
msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
msgstr ""
+msgid "GroupSettings|Automatic assignment of GitLab Duo Pro seats"
+msgstr ""
+
+msgid "GroupSettings|Automatically assign newly added billable users to GitLab Duo seats."
+msgstr ""
+
msgid "GroupSettings|Available only on the top-level group. Applies to all subgroups. Groups already shared with a group outside %{group} are still shared unless removed manually."
msgstr ""
@@ -25031,9 +25040,6 @@ msgstr ""
msgid "GroupSettings|Emails are not encrypted. Concerned administrators may want to disable diff previews."
msgstr ""
-msgid "GroupSettings|Enable Auto Assign GitLab Duo Pro Seats"
-msgstr ""
-
msgid "GroupSettings|Enable GitLab Duo features for this group %{link_start}Learn more%{link_end}."
msgstr ""
@@ -25088,9 +25094,6 @@ msgstr ""
msgid "GroupSettings|If enabled, individual user accounts will be able to use only issued SSH certificates for Git access. It doesn't apply to service accounts, deploy keys, and other types of internal accounts."
msgstr ""
-msgid "GroupSettings|If enabled, newly added eligible members will automatically receive Duo Pro seats."
-msgstr ""
-
msgid "GroupSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories."
msgstr ""
diff --git a/qa/qa/page/project/settings/services/jira.rb b/qa/qa/page/project/settings/services/jira.rb
index 681f545f1bf..66fb20d1e0e 100644
--- a/qa/qa/page/project/settings/services/jira.rb
+++ b/qa/qa/page/project/settings/services/jira.rb
@@ -25,7 +25,7 @@ module QA
view 'app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue' do
element 'jira-issues-enabled-checkbox'
- element 'jira-project-key-field'
+ element 'jira-project-keys-field'
end
def setup_service_with(url:)
@@ -48,8 +48,8 @@ module QA
check_element('jira-issues-enabled-checkbox', true)
end
- def set_jira_project_key(key)
- fill_element('jira-project-key-field', key)
+ def set_jira_project_keys(key)
+ fill_element('jira-project-keys-field', key)
end
def click_save_changes_and_wait
diff --git a/qa/qa/service/docker_run/product_analytics/browser_sdk_app.rb b/qa/qa/service/docker_run/product_analytics/browser_sdk_app.rb
index 7bb4f9aa7bb..e34768212d3 100644
--- a/qa/qa/service/docker_run/product_analytics/browser_sdk_app.rb
+++ b/qa/qa/service/docker_run/product_analytics/browser_sdk_app.rb
@@ -17,7 +17,7 @@ module QA
@image = 'registry.gitlab.com/gitlab-org/analytics-section/product-analytics/' \
'gl-application-sdk-browser/example-app:main'
@name = 'browser_sdk'
- @sdk_host = sdk_host
+ @sdk_host = URI(sdk_host)
@sdk_app_id = sdk_app_id
@port = '8081'
diff --git a/qa/qa/service/docker_run/product_analytics/dotnet_sdk_app.rb b/qa/qa/service/docker_run/product_analytics/dotnet_sdk_app.rb
new file mode 100644
index 00000000000..a84e11d8773
--- /dev/null
+++ b/qa/qa/service/docker_run/product_analytics/dotnet_sdk_app.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module DockerRun
+ module ProductAnalytics
+ class DotnetSdkApp < Base
+ include Support::API
+
+ def initialize(sdk_host, sdk_app_id)
+ # Below is an image of a sample app that uses Product Analytics .NET SDK.
+ # The image is created in https://gitlab.com/gitlab-org/analytics-section/product-analytics/gl-application-sdk-dotnet
+ # It's buit on every merge to main branch in the repository.
+ # Note: set @host_name = 'localhost' here when running locally against GDK.
+ @image = 'registry.gitlab.com/gitlab-org/analytics-section/product-analytics/' \
+ 'gl-application-sdk-dotnet/example-app:main'
+ @name = 'dotnet_sdk'
+ @sdk_host = URI(sdk_host)
+ @sdk_app_id = sdk_app_id
+ @port = '5171'
+
+ super()
+ end
+
+ def register!
+ shell <<~CMD.tr("\n", ' ')
+ docker run -d --rm
+ --name #{@name}
+ --network #{network}
+ --hostname #{host_name}
+ -p #{@port}:#{@port}
+ -e PA_COLLECTOR_HOST=#{@sdk_host.host}
+ -e PA_COLLECTOR_PORT=#{@sdk_host.port}
+ -e PA_APPLICATION_ID=#{@sdk_app_id}
+ #{@image}
+ CMD
+
+ wait_for_app_available
+ end
+
+ private
+
+ def wait_for_app_available
+ Runtime::Logger.info("Waiting for .NET SDK sample app to become available at http://#{host_name}:#{@port}...")
+ Support::Waiter.wait_until(sleep_interval: 1,
+ message: "Wait for .NET SDK sample app to become available at http://#{host_name}:#{@port}") { app_available? }
+ Runtime::Logger.info('.NET SDK sample app is up and event is triggered!')
+ end
+
+ def app_available?
+ response = get "http://#{host_name}:#{@port}"
+ response.code == 200
+ rescue Errno::ECONNRESET, Errno::ECONNREFUSED, RestClient::ServerBrokeConnection => e
+ Runtime::Logger.debug(".NET SDK sample app is not yet available: #{e.inspect}")
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index c49d84b5935..fb749487a29 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe 'Database schema', feature_category: :database do
broadcast_messages: %w[namespace_id],
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
- ci_builds: %w[project_id runner_id user_id erased_by_id trigger_request_id partition_id auto_canceled_by_partition_id],
+ ci_builds: %w[project_id runner_id user_id erased_by_id trigger_request_id partition_id auto_canceled_by_partition_id execution_config_id],
ci_job_artifacts: %w[partition_id project_id job_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_pipeline_artifacts: %w[partition_id],
@@ -105,13 +105,14 @@ RSpec.describe 'Database schema', feature_category: :database do
oauth_access_grants: %w[resource_owner_id application_id],
oauth_access_tokens: %w[resource_owner_id application_id],
oauth_applications: %w[owner_id],
- p_ci_builds: %w[erased_by_id trigger_request_id partition_id auto_canceled_by_partition_id],
+ p_ci_builds: %w[erased_by_id trigger_request_id partition_id auto_canceled_by_partition_id execution_config_id],
p_batched_git_ref_updates_deletions: %w[project_id partition_id],
p_catalog_resource_sync_events: %w[catalog_resource_id project_id partition_id],
p_catalog_resource_component_usages: %w[used_by_project_id], # No FK constraint because we want to preserve historical usage data
p_ci_finished_build_ch_sync_events: %w[build_id],
p_ci_job_artifacts: %w[partition_id project_id job_id],
p_ci_pipeline_variables: %w[partition_id],
+ p_ci_builds_execution_configs: %w[partition_id],
p_ci_stages: %w[partition_id project_id pipeline_id],
project_build_artifacts_size_refreshes: %w[last_job_artifact_id],
project_data_transfers: %w[project_id namespace_id],
diff --git a/spec/factories/ci/builds_execution_configs.rb b/spec/factories/ci/builds_execution_configs.rb
new file mode 100644
index 00000000000..f8851f1c794
--- /dev/null
+++ b/spec/factories/ci/builds_execution_configs.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_builds_execution_configs, class: 'Ci::BuildExecutionConfig' do
+ pipeline { association(:ci_pipeline) }
+
+ project_id { pipeline.project_id }
+ partition_id { pipeline.partition_id }
+ run_steps do
+ [
+ {
+ 'name' => 'step1',
+ 'step' => 'echo',
+ 'inputs' => { 'message' => 'Hello, World!' }
+ }
+ ]
+ end
+
+ trait :with_script do
+ run_steps do
+ [
+ {
+ 'name' => 'script1',
+ 'script' => 'echo "Hello, World!"'
+ }
+ ]
+ end
+ end
+
+ trait :with_step_and_script do
+ run_steps do
+ [
+ {
+ 'name' => 'step1',
+ 'step' => 'echo',
+ 'inputs' => { 'message' => 'Hello, World!' }
+ },
+ {
+ 'name' => 'script1',
+ 'script' => 'ls -l'
+ }
+ ]
+ end
+ end
+ end
+end
diff --git a/spec/factories/import_source_users.rb b/spec/factories/import_source_users.rb
index 3ffa3cc97a4..c7a850388f8 100644
--- a/spec/factories/import_source_users.rb
+++ b/spec/factories/import_source_users.rb
@@ -18,5 +18,9 @@ FactoryBot.define do
trait :with_reassigned_by_user do
reassigned_by_user factory: :user
end
+
+ trait :awaiting_approval do
+ status { 1 }
+ end
end
end
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 6e8a6092667..5045f3b8119 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -46,7 +46,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
};
const preTag = ({ language, content = 'test' } = {}) => {
- const languageAttr = language ? ` lang="${language}"` : '';
+ const languageAttr = language ? ` data-canonical-lang="${language}"` : '';
return `${content}`;
};
@@ -135,14 +135,14 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
describe('preview button', () => {
it('does not appear for a regular code block', () => {
- tiptapEditor.commands.insertContent('var a = 2;
');
+ tiptapEditor.commands.insertContent('var a = 2;
');
expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false);
});
it.each`
diagramType | diagramCode
- ${'mermaid'} | ${'graph TD;\n A-->B;
'}
+ ${'mermaid'} | ${'graph TD;\n A-->B;
'}
${'nomnoml'} | ${'
'}
`('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => {
tiptapEditor.commands.insertContent(diagramCode);
@@ -219,7 +219,9 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
describe('Create custom type', () => {
beforeEach(async () => {
- tiptapEditor.commands.insertContent('var a = 2;
');
+ tiptapEditor.commands.insertContent(
+ 'var a = 2;
',
+ );
await wrapper.findComponent(GlDropdown).vm.show();
await wrapper.findByTestId('create-custom-type').trigger('click');
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 6f0c0ee6ed5..a56ffdf8fe7 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -2,25 +2,15 @@ import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight
import languageLoader from '~/content_editor/services/code_block_language_loader';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
-const CODE_BLOCK_HTML = `
-
-
- console.log('hello world')
-
-
-
`;
+const CODE_BLOCK_HTML = `
console.log('hello world')
`;
jest.mock('~/content_editor/services/code_block_language_loader');
describe('content_editor/extensions/code_block_highlight', () => {
- let parsedCodeBlockHtmlFixture;
let tiptapEditor;
let doc;
let codeBlock;
- const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
- const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
-
beforeEach(() => {
tiptapEditor = createTestEditor({
extensions: [CodeBlockHighlight],
@@ -38,28 +28,21 @@ describe('content_editor/extensions/code_block_highlight', () => {
describe('when parsing HTML', () => {
beforeEach(() => {
- parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
-
tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
});
- it('extracts language and params attributes from Markdown API output', () => {
- const language = preElement().getAttribute('lang');
- expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
- language,
- });
- });
-
- it('adds code, highlight, and js-syntax-highlight to code block element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
-
- expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
- });
-
- it('adds content-editor-code-block class to the pre element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
-
- expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ it('parses HTML correctly into a code block', () => {
+ expect(tiptapEditor.getJSON()).toEqual(
+ doc(
+ codeBlock(
+ {
+ language: 'javascript',
+ class: 'code highlight js-syntax-highlight language-javascript',
+ },
+ "console.log('hello world')",
+ ),
+ ).toJSON(),
+ );
});
it('includes the lowlight plugin', () => {
diff --git a/spec/frontend/content_editor/extensions/code_suggestion_spec.js b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
index b513735af22..0b5650614eb 100644
--- a/spec/frontend/content_editor/extensions/code_suggestion_spec.js
+++ b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
@@ -19,6 +19,8 @@ foo: bar
\`\`\`
`;
+const CODE_SUGGESTION_HTML = ``;
+
jest.mock('~/content_editor/services/utils', () => ({
memoizedGet: jest.fn().mockResolvedValue(SAMPLE_README_CONTENT),
}));
@@ -127,4 +129,27 @@ describe('content_editor/extensions/code_suggestion', () => {
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
});
+
+ describe('when parsing HTML', () => {
+ beforeEach(() => {
+ createEditor();
+
+ tiptapEditor.commands.setContent(CODE_SUGGESTION_HTML);
+ });
+
+ it('parses HTML correctly into a code suggestions block', () => {
+ expect(tiptapEditor.getJSON()).toEqual(
+ doc(
+ codeSuggestion(
+ {
+ language: 'suggestion',
+ langParams: '-0+0',
+ class: 'code highlight js-syntax-highlight language-suggestion',
+ },
+ ' options = [',
+ ),
+ ).toJSON(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/content_editor/extensions/diagram_spec.js b/spec/frontend/content_editor/extensions/diagram_spec.js
index b8d9e0b5aeb..c611e05884f 100644
--- a/spec/frontend/content_editor/extensions/diagram_spec.js
+++ b/spec/frontend/content_editor/extensions/diagram_spec.js
@@ -1,7 +1,30 @@
import Diagram from '~/content_editor/extensions/diagram';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+const DIAGRAM_HTML = `
pie title NETFLIX
"Time spent looking for movie" : 90
"Time spent watching it" : 10
`;
describe('content_editor/extensions/diagram', () => {
+ let tiptapEditor;
+ let doc;
+ let diagram;
+
+ const createEditor = () => {
+ tiptapEditor = createTestEditor({
+ extensions: [CodeBlockHighlight, Diagram],
+ });
+
+ ({
+ builders: { doc, diagram },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ diagram: { nodeType: Diagram.name },
+ },
+ }));
+ };
+
it('inherits from code block highlight extension', () => {
expect(Diagram.parent).toBe(CodeBlockHighlight);
});
@@ -13,4 +36,23 @@ describe('content_editor/extensions/diagram', () => {
}),
);
});
+
+ describe('when parsing HTML', () => {
+ beforeEach(() => {
+ createEditor();
+
+ tiptapEditor.commands.setContent(DIAGRAM_HTML);
+ });
+
+ it('parses HTML correctly into a diagram block', () => {
+ expect(tiptapEditor.getJSON()).toEqual(
+ doc(
+ diagram(
+ { language: 'mermaid' },
+ 'pie title NETFLIX\n "Time spent looking for movie" : 90\n "Time spent watching it" : 10',
+ ),
+ ).toJSON(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index d06a3d0a389..b8de438c302 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,5 +1,4 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
@@ -32,19 +31,13 @@ describe('JiraIssuesFields', () => {
};
const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findProjectKeys = () => wrapper.findComponent(GlFormInput);
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
- const findProjectKey = () => wrapper.findComponent(GlFormInput);
- const findProjectKeys = () => wrapper.findByTestId('jira-project-keys');
- const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
+ const findProjectKeysGroup = () => wrapper.findByTestId('jira-project-keys');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = (isEnabled = true) => findEnableCheckbox().vm.$emit('input', isEnabled);
- const assertProjectKeyState = (expectedStateValue) => {
- expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
- expect(findProjectKeyFormGroup().attributes('state')).toBe(expectedStateValue);
- };
-
describe('template', () => {
describe.each`
showJiraIssuesIntegration
@@ -80,8 +73,8 @@ describe('JiraIssuesFields', () => {
createComponent({ props: { initialProjectKey: '' } });
});
- it('does not render project_key input', () => {
- expect(findProjectKey().exists()).toBe(false);
+ it('does not render project keys input', () => {
+ expect(findProjectKeys().exists()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
@@ -99,134 +92,58 @@ describe('JiraIssuesFields', () => {
});
describe('on enable issues', () => {
- it('renders project_key input as required', async () => {
+ it('renders project keys input', async () => {
await setEnableCheckbox(true);
- expect(findProjectKey().exists()).toBe(true);
- expect(findProjectKey().attributes('required')).toBe('required');
+ expect(findProjectKeys().exists()).toBe(true);
});
});
});
- describe('when jira_multiple_project_keys is not enabled', () => {
+ describe('when initialProjectKeys is provided', () => {
+ const projectKeys = 'BE, FE';
+
beforeEach(() => {
createComponent({
mountFn: shallowMountExtended,
props: {
initialEnableJiraIssues: true,
- },
- });
- });
-
- it('does not render "Jira project keys" input', () => {
- expect(findProjectKeys().exists()).toBe(false);
- });
- });
-
- describe('when jira_multiple_project_keys is enabled', () => {
- beforeEach(() => {
- createComponent({
- mountFn: shallowMountExtended,
- props: {
- initialEnableJiraIssues: true,
- },
- provide: {
- glFeatures: {
- jiraMultipleProjectKeys: true,
- },
+ initialProjectKeys: projectKeys,
},
});
});
it('renders "Jira project keys" input', () => {
- expect(findProjectKeys().attributes('label')).toBe('Jira project keys');
+ expect(findProjectKeysGroup().attributes('label')).toBe('Jira project keys');
+ expect(findProjectKeys().attributes('value')).toBe(projectKeys);
});
});
- describe('Vulnerabilities creation', () => {
+ describe('when section is issue creation (for vulnarabilities)', () => {
+ const jiraIssueType = 'some-jira-issue-type';
+
beforeEach(() => {
- createComponent();
+ createComponent({
+ mountFn: shallowMountExtended,
+ props: {
+ isIssueCreation: true,
+ initialVulnerabilitiesIssuetype: jiraIssueType,
+ },
+ });
});
- it.each([true, false])(
- 'shows the jira-vulnerabilities component correctly when jira issues enables is set to "%s"',
- async (hasJiraIssuesEnabled) => {
- await setEnableCheckbox(hasJiraIssuesEnabled);
-
- expect(findJiraForVulnerabilities().exists()).toBe(hasJiraIssuesEnabled);
- },
- );
-
- it('passes down the correct show-full-feature property', async () => {
- await setEnableCheckbox(true);
- expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBe('true');
- wrapper.setProps({ showJiraVulnerabilitiesIntegration: false });
- await nextTick();
- expect(findJiraForVulnerabilities().attributes('show-full-feature')).toBeUndefined();
- });
-
- it('passes down the correct initial-issue-type-id value when value is empty', async () => {
- await setEnableCheckbox(true);
- expect(findJiraForVulnerabilities().attributes('initial-issue-type-id')).toBeUndefined();
- });
-
- it('passes down the correct initial-issue-type-id value when value is not empty', async () => {
- const jiraIssueType = 'some-jira-issue-type';
- wrapper.setProps({ initialVulnerabilitiesIssuetype: jiraIssueType });
- await setEnableCheckbox(true);
- expect(findJiraForVulnerabilities().attributes('initial-issue-type-id')).toBe(
- jiraIssueType,
- );
+ it('renders "Jira for vulnerabilities" component', () => {
+ expect(findJiraForVulnerabilities().attributes()).toMatchObject({
+ 'show-full-feature': 'true',
+ 'initial-issue-type-id': jiraIssueType,
+ });
});
it('emits "request-jira-issue-types` when the jira-vulnerabilities component requests to fetch issue types', async () => {
- await setEnableCheckbox(true);
await findJiraForVulnerabilities().vm.$emit('request-jira-issue-types');
expect(wrapper.emitted('request-jira-issue-types')).toHaveLength(1);
});
});
-
- describe('Project key input field', () => {
- it('sets Project Key `state` attribute to `true` by default', () => {
- createComponent({
- props: {
- initialProjectKey: '',
- initialEnableJiraIssues: true,
- },
- mountFn: shallowMountExtended,
- });
-
- assertProjectKeyState('true');
- });
-
- describe('when `isValidated` prop is true', () => {
- beforeEach(() => {
- createComponent({
- props: {
- initialProjectKey: '',
- initialEnableJiraIssues: true,
- isValidated: true,
- },
- mountFn: shallowMountExtended,
- });
- });
-
- describe('with no project key', () => {
- it('sets Project Key `state` attribute to `undefined`', () => {
- assertProjectKeyState(undefined);
- });
- });
-
- describe('when project key is set', () => {
- it('sets Project Key `state` attribute to `true`', async () => {
- // set the project key
- await findProjectKey().vm.$emit('input', 'AB');
-
- assertProjectKeyState('true');
- });
- });
- });
- });
});
});
diff --git a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
index 86fcd038d46..1e1239d4b28 100644
--- a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
+++ b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
@@ -10,6 +10,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import {
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_DRAFT,
+ TOKEN_TYPE_SOURCE_BRANCH,
TOKEN_TYPE_TARGET_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { mergeRequestListTabs } from '~/vue_shared/issuable/list/constants';
@@ -109,13 +110,14 @@ describe('Merge requests list app', () => {
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers: [] },
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_TARGET_BRANCH },
+ { type: TOKEN_TYPE_SOURCE_BRANCH },
]);
});
});
describe('when all tokens are available', () => {
beforeEach(async () => {
- setWindowLocation('?draft=yes&target_branches[]="branch-a"');
+ setWindowLocation('?draft=yes&target_branches[]=branch-a&source_branches[]=branch-b');
window.gon = {
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
@@ -142,6 +144,7 @@ describe('Merge requests list app', () => {
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_TARGET_BRANCH },
+ { type: TOKEN_TYPE_SOURCE_BRANCH },
]);
});
@@ -149,6 +152,7 @@ describe('Merge requests list app', () => {
expect(findIssuableList().props('initialFilterValue')).toMatchObject([
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_TARGET_BRANCH },
+ { type: TOKEN_TYPE_SOURCE_BRANCH },
]);
});
});
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 0b5739be9a1..ec4a1aa15f7 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -155,4 +155,14 @@ RSpec.describe GitlabSchema.types['Query'], feature_category: :shared do
is_expected.to have_graphql_resolver(Resolvers::Ml::ModelDetailResolver)
end
end
+
+ describe 'integration_exclusions field' do
+ subject { described_class.fields['integrationExclusions'] }
+
+ it 'returns metadata', :aggregate_failures do
+ is_expected.to have_graphql_arguments(:integrationName)
+ is_expected.to have_graphql_type(Types::Integrations::ExclusionType.connection_type)
+ is_expected.to have_graphql_resolver(Resolvers::Integrations::ExclusionsResolver)
+ end
+ end
end
diff --git a/spec/initializers/net_http_patch_spec.rb b/spec/initializers/net_http_patch_spec.rb
index 959eae954c4..246d9f1efb3 100644
--- a/spec/initializers/net_http_patch_spec.rb
+++ b/spec/initializers/net_http_patch_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Net::HTTP patch proxy user and password encoding', feature_categ
before do
allow(ENV).to receive(:[]).and_call_original
- allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
+ stub_env('http_proxy', http_proxy)
end
it { is_expected.to eq(nil) }
@@ -64,7 +64,7 @@ RSpec.describe 'Net::HTTP patch proxy user and password encoding', feature_categ
before do
allow(ENV).to receive(:[]).and_call_original
- allow(ENV).to receive(:[]).with('http_proxy').and_return(http_proxy)
+ stub_env('http_proxy', http_proxy)
end
it { is_expected.to eq(nil) }
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
index 55a9292e48d..56ebb885f5e 100644
--- a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -143,7 +143,8 @@ RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
members_mapper: instance_of(BulkImports::UsersMapper),
object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
user: user,
- excluded_keys: nil
+ excluded_keys: nil,
+ import_source: Import::SOURCE_DIRECT_TRANSFER
)
.and_return(relation_object)
expect(relation_object).to receive(:assign_attributes).with(group: group)
diff --git a/spec/lib/gitlab/database/reflection_spec.rb b/spec/lib/gitlab/database/reflection_spec.rb
index aed0537af32..8371cfadf40 100644
--- a/spec/lib/gitlab/database/reflection_spec.rb
+++ b/spec/lib/gitlab/database/reflection_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Gitlab::Database::Reflection, feature_category: :database do
context 'when a username is not set' do
it 'returns the value of the USER environment variable' do
allow(database).to receive(:config).and_return(username: nil)
- allow(ENV).to receive(:[]).with('USER').and_return('bob')
+ stub_env('USER', 'bob')
expect(database.username).to eq('bob')
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e34b3a88abe..c59d9f0a7e8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -349,6 +349,7 @@ ci_pipelines:
- package_file_build_infos
- build_trace_chunks
- pipeline_metadata
+- build_execution_configs
pipeline_metadata:
- project
- pipeline
@@ -382,6 +383,7 @@ builds:
- needs
- resource
- pipeline
+- execution_config
- sourced_pipeline
- resource_group
- metadata
@@ -659,6 +661,7 @@ project:
- import_data
- commit_statuses
- ci_pipelines
+- build_execution_configs
- all_pipelines
- stages
- ci_refs
diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
index 5e63804c51c..86238e8d6e3 100644
--- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
+RSpec.describe Gitlab::ImportExport::Base::RelationFactory, feature_category: :importers do
let(:user) { create(:admin) }
let(:project) { create(:project) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:relation_sym) { :project_snippets }
let(:relation_hash) { {} }
let(:excluded_keys) { [] }
+ let(:import_source) { nil }
subject do
described_class.create( # rubocop:disable Rails/SaveBang
@@ -19,7 +20,8 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
members_mapper: members_mapper,
user: user,
importable: project,
- excluded_keys: excluded_keys
+ excluded_keys: excluded_keys,
+ import_source: import_source
)
end
@@ -90,6 +92,24 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
expect(subject).to be_instance_of(Note)
end
+ context 'when import_source is given' do
+ let(:import_source) { Import::SOURCE_DIRECT_TRANSFER }
+
+ it 'sets the imported_from' do
+ expect(subject.imported_from).to eq(import_source.to_s)
+ end
+
+ context 'when object does not have an imported_from attribute' do
+ let(:relation_sym) { :user }
+ let(:relation_hash) { attributes_for(:user) }
+
+ it 'works without an error' do
+ expect(subject).not_to respond_to(:imported_from) # Sanity check: This must be true for test subject
+ expect(subject).to be_instance_of(User)
+ end
+ end
+ end
+
context 'when relation contains user references' do
let(:new_user) { create(:user) }
let(:exported_member) do
diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb
index 8bb60daaf57..65bcd08dade 100644
--- a/spec/mailers/devise_mailer_spec.rb
+++ b/spec/mailers/devise_mailer_spec.rb
@@ -54,6 +54,39 @@ RSpec.describe DeviseMailer, feature_category: :user_management do
expect(subject.body.encoded).to have_text user.email
end
end
+
+ context 'for secondary email' do
+ let(:secondary_email) { create(:email) }
+
+ subject { described_class.confirmation_instructions(secondary_email, 'faketoken', opts) }
+
+ it_behaves_like 'it validates recipients'
+
+ it 'has the correct subject and body', :aggregate_failures do
+ is_expected.to have_subject I18n.t('devise.mailer.confirmation_instructions.subject')
+
+ is_expected.to have_text_part_content(
+ format(_("%{name}, confirm your email address now!"), name: secondary_email.user.name)
+ )
+ is_expected.to have_html_part_content(
+ format(_("%{name}, confirm your email address now!"), name: secondary_email.user.name)
+ )
+
+ is_expected.to have_text_part_content(
+ secondary_email.email
+ )
+ is_expected.to have_html_part_content(
+ secondary_email.email
+ )
+
+ is_expected.to have_text_part_content(
+ format(_('Confirm this email address within %{cut_off_days} days, otherwise the email address is removed.'), cut_off_days: ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS)
+ )
+ is_expected.to have_html_part_content(
+ format(_('Confirm this email address within %{cut_off_days} days, otherwise the email address is removed.'), cut_off_days: ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS)
+ )
+ end
+ end
end
describe '#password_change_by_admin' do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 0582c3823b0..f2e352b3c2d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -38,6 +38,12 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { expect(setting.silent_admin_exports_enabled).to eq(false) }
end
+ describe 'USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS' do
+ subject { described_class::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS }
+
+ it { is_expected.to eq(3) }
+ end
+
describe 'validations' do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
diff --git a/spec/models/ci/build_execution_config_spec.rb b/spec/models/ci/build_execution_config_spec.rb
new file mode 100644
index 00000000000..4604a14bb68
--- /dev/null
+++ b/spec/models/ci/build_execution_config_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::BuildExecutionConfig, type: :model, feature_category: :pipeline_composition do
+ let_it_be(:execution_config) { create(:ci_builds_execution_configs) }
+
+ it { is_expected.to belong_to(:pipeline).class_name('Ci::Pipeline').inverse_of(:build_execution_configs) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:builds).class_name('Ci::Build').inverse_of(:execution_config) }
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:run_steps) }
+ end
+
+ describe 'run_steps' do
+ it { is_expected.not_to allow_value("string").for(:run_steps) }
+ it { is_expected.not_to allow_value(1.0).for(:run_steps) }
+
+ it { is_expected.not_to allow_value(nil).for(:run_steps) }
+
+ it do
+ is_expected.to allow_value(
+ [
+ {
+ 'name' => 'step1',
+ 'step' => 'echo',
+ 'inputs' => { 'message' => 'Hello, World!' }
+ }
+ ]
+ ).for(:run_steps)
+ end
+ end
+
+ describe 'partitioning', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:execution_config) { FactoryBot.build(:ci_builds_execution_configs, pipeline: pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'assigns partition id to execution config' do
+ execution_config.save!
+
+ expect(execution_config.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index ec676a503c6..01de572c829 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to belong_to(:pipeline).inverse_of(:builds) }
+ it { is_expected.to belong_to(:execution_config).class_name('Ci::BuildExecutionConfig').inverse_of(:builds) }
it { is_expected.to have_many(:needs).with_foreign_key(:build_id) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index f532477f7e5..c61ed4990bb 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -30,6 +30,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
it { is_expected.to have_many(:trigger_requests).with_foreign_key(:commit_id).inverse_of(:pipeline) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:builds) }
+ it { is_expected.to have_many(:build_execution_configs).class_name('Ci::BuildExecutionConfig').inverse_of(:pipeline) }
it do
is_expected.to have_many(:statuses_order_id_desc)
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 3b4e6898d91..590fc0a184f 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -98,7 +98,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do
pipeline_id report_results pending_state pages_deployments
queuing_entry runtime_metadata trace_metadata
dast_site_profile dast_scanner_profile stage_id dast_site_profiles_build
- dast_scanner_profiles_build auto_canceled_by_partition_id].freeze
+ dast_scanner_profiles_build auto_canceled_by_partition_id execution_config_id execution_config].freeze
end
before_all do
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index 72a4e219a48..cadde88b712 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -193,6 +193,7 @@ RSpec.describe Ci::Partitionable, feature_category: :continuous_integration do
it 'returns a list of CI models being partitioned' do
expected_list = %w[
Ci::BuildMetadata
+ Ci::BuildExecutionConfig
Ci::BuildName
Ci::JobAnnotation
Ci::JobArtifact
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index ecb8f72470d..b73e525a324 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -42,14 +42,67 @@ RSpec.describe Email do
end
describe 'scopes' do
- let(:user) { create(:user, :unconfirmed) }
+ let_it_be(:unconfirmed_user) { create(:user, :unconfirmed) }
+ let_it_be(:confirmed_user) { create(:user) }
- it 'scopes confirmed emails' do
- create(:email, :confirmed, user: user)
- create(:email, user: user)
+ let_it_be(:unconfirmed_primary_email) { unconfirmed_user.email }
+ let_it_be(:confirmed_primary_email) { described_class.find_by_email(confirmed_user.email) }
- expect(user.emails.count).to eq 2
- expect(user.emails.confirmed.count).to eq 1
+ let_it_be(:unconfirmed_secondary_email) { create(:email, user: confirmed_user) }
+ let_it_be(:confirmed_secondary_email) { create(:email, :confirmed, user: confirmed_user) }
+
+ describe '.confirmed' do
+ it 'returns confirmed emails' do
+ expect(described_class.confirmed).to contain_exactly(
+ # after user's primary email is confirmed it is stored to 'emails' table
+ confirmed_primary_email,
+ confirmed_secondary_email
+ )
+ end
+ end
+
+ describe '.unconfirmed' do
+ it 'returns unconfirmed secondary emails' do
+ expect(described_class.unconfirmed).to contain_exactly(
+ # excludes `unconfirmed_primary_email` because
+ # user's primary email is not stored to 'emails' table till it is confirmed
+ unconfirmed_secondary_email
+ )
+ end
+ end
+
+ describe '.unconfirmed_and_created_before' do
+ let(:created_cut_off) { 3.days.ago }
+
+ let!(:unconfirmed_secondary_email_created_before_cut_off) do
+ create(:email, created_at: created_cut_off - 1.second)
+ end
+
+ let!(:unconfirmed_secondary_email_created_at_cut_off) do
+ create(:email, created_at: created_cut_off)
+ end
+
+ let!(:unconfirmed_secondary_email_created_after_cut_off) do
+ create(:email, created_at: created_cut_off + 1.second)
+ end
+
+ let!(:confirmed_secondary_email_created_before_cut_off) do
+ create(:email, :confirmed, created_at: created_cut_off - 1.second)
+ end
+
+ let!(:confirmed_secondary_email_created_at_cut_off) do
+ create(:email, :confirmed, created_at: created_cut_off)
+ end
+
+ let!(:confirmed_secondary_email_created_after_cut_off) do
+ create(:email, :confirmed, created_at: created_cut_off + 1.second)
+ end
+
+ it 'returns unconfirmed secondary emails created before timestamp passed in' do
+ expect(described_class.unconfirmed_and_created_before(created_cut_off)).to contain_exactly(
+ unconfirmed_secondary_email_created_before_cut_off
+ )
+ end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 7371f80de06..788201727bf 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -500,46 +500,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
- describe 'before_save :ensure_runner_registration_token_disabled_on_com' do
- let(:parent_namespace) { nil }
- let!(:namespace) { create(:group, parent: parent_namespace) }
-
- context 'when on self-managed' do
- it 'does not create a settings record' do
- expect(namespace.allow_runner_registration_token?).to eq true
- end
- end
-
- context 'when instance is dedicated' do
- before do
- Gitlab::CurrentSettings.update!(gitlab_dedicated_instance: true)
- end
-
- it 'does not create a settings record' do
- expect(namespace.allow_runner_registration_token?).to eq true
- end
- end
-
- context 'when on SaaS', :saas do
- it 'creates a settings record disallowing runner registration tokens' do
- expect(namespace.allow_runner_registration_token?).to eq false
- end
-
- context 'when namespace is not top-most' do
- let!(:parent_namespace) { create(:group) }
-
- it 'does not create a settings record and uses the default value for column' do
- expect(namespace.allow_runner_registration_token).to eq true
- end
-
- it 'uses the value from the parent namespace' do
- # allow_runner_registration_token? uses the value of the parent namespace and instance, so should return false
- expect(namespace.allow_runner_registration_token?).to eq false
- end
- end
- end
- end
-
context 'when creating a new project' do
let_it_be(:group) { create(:group) }
diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb
index 43464715203..1aaf71830ad 100644
--- a/spec/models/import/source_user_spec.rb
+++ b/spec/models/import/source_user_spec.rb
@@ -35,6 +35,27 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do
end
end
+ describe 'after_transition callback' do
+ subject(:source_user) { create(:import_source_user, :awaiting_approval, :with_reassign_to_user) }
+
+ it 'does not unset reassign_to_user on other transitions' do
+ expect { source_user.accept! }
+ .not_to change { source_user.reload.reassign_to_user }
+ end
+
+ it 'unsets reassign_to_user when rejected' do
+ expect { source_user.reject! }
+ .to change { source_user.reload.reassign_to_user }
+ .from(an_instance_of(User)).to(nil)
+ end
+
+ it 'unsets reassign_to_user when assignment is cancelled' do
+ expect { source_user.cancel_assignment! }
+ .to change { source_user.reload.reassign_to_user }
+ .from(an_instance_of(User)).to(nil)
+ end
+ end
+
describe '.find_source_user' do
let_it_be(:namespace_1) { create(:namespace) }
let_it_be(:namespace_2) { create(:namespace) }
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 500f8a0369a..40de1709b1a 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -257,21 +257,6 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
it 'includes SECTION_TYPE_JIRA_ISSUE_CREATION' do
expect(sections).to include(described_class::SECTION_TYPE_JIRA_ISSUE_CREATION)
end
-
- context 'when jira_multiple_project_keys feature is disabled' do
- before do
- stub_feature_flags(jira_multiple_project_keys: false)
- end
-
- it 'does not include SECTION_TYPE_JIRA_ISSUE_CREATION' do
- expect(sections).not_to include(described_class::SECTION_TYPE_JIRA_ISSUE_CREATION)
- end
-
- it 'section SECTION_TYPE_JIRA_ISSUES title is "Issues"' do
- jira_issues_section = integration.sections.find { |s| s[:type] == described_class::SECTION_TYPE_JIRA_ISSUES }
- expect(jira_issues_section[:title]).to eq('Issues')
- end
- end
end
context 'when instance_level? is true' do
@@ -446,16 +431,6 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
expect(integration.jira_tracker_data.project_keys).to eq(project_keys)
end
- context 'when the jira_multiple_project_keys feature is disabled' do
- before do
- stub_feature_flags(jira_multiple_project_keys: false)
- end
-
- it 'copies project_key into project_keys' do
- expect(integration.jira_tracker_data.project_keys).to eq([project_key])
- end
- end
-
context 'when loading serverInfo' do
context 'with a Cloud instance' do
let(:server_info_results) { { 'deploymentType' => 'Cloud' } }
@@ -763,22 +738,6 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do
expect(WebMock).to have_requested(:get, issue_url)
end
end
-
- context 'when jira_multiple_project_keys feature is disabled' do
- before do
- stub_feature_flags(jira_multiple_project_keys: false)
- end
-
- context 'when project_key matches issue_key' do
- let(:project_key) { 'JIRA' }
-
- it 'calls the Jira API to get the issue' do
- find_issue
-
- expect(WebMock).to have_requested(:get, issue_url)
- end
- end
- end
end
end
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
index 301a019dbeb..17a8ab370f4 100644
--- a/spec/models/work_items/parent_link_spec.rb
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -151,6 +151,21 @@ RSpec.describe WorkItems::ParentLink, feature_category: :portfolio_management do
it 'existing link is still valid' do
expect(link1).to be_valid
end
+
+ context 'when parent already exceeds maximum number of links' do
+ let_it_be(:task3) { create(:work_item, :task, project: project) }
+ let_it_be(:link2) { create(:parent_link, work_item_parent: issue, work_item: task2) }
+
+ it 'only invalidates new links' do
+ link3 = build(:parent_link, work_item_parent: issue, work_item: task3)
+
+ expect(link3).not_to be_valid
+ expect(link3.errors[:work_item_parent]).to include('parent already has maximum number of children.')
+
+ expect(link1).to be_valid
+ expect(link2).to be_valid
+ end
+ end
end
context 'when setting confidentiality' do
diff --git a/spec/requests/api/graphql/integrations/exclusions_spec.rb b/spec/requests/api/graphql/integrations/exclusions_spec.rb
new file mode 100644
index 00000000000..b99873bfea9
--- /dev/null
+++ b/spec/requests/api/graphql/integrations/exclusions_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Querying for integration exclusions', feature_category: :integrations do
+ include GraphqlHelpers
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:admin_user) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let(:current_user) { admin_user }
+ let(:query) { graphql_query_for('integrationExclusions', args, fields) }
+ let(:args) { { 'integrationName' => :BEYOND_IDENTITY } }
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ project {
+ id
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'when the user is authorized' do
+ 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
+
+ let!(:propagated_integration) do
+ create(:beyond_identity_integration, active: false, instance: false, project: project,
+ inherit_from_id: instance_integration.id)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query that returns data'
+
+ it 'returns projects that are custom exclusions' do
+ nodes = graphql_data['integrationExclusions']['nodes']
+ expect(nodes.size).to eq(1)
+ expect(nodes).to include(a_hash_including('project' => { 'id' => project2.to_global_id.to_s }))
+ end
+ end
+
+ context 'when the user is not authorized' do
+ let(:current_user) { user }
+
+ it 'responds with an error' do
+ post_graphql(query, current_user: current_user)
+ expect(graphql_errors.first['message']).to eq(
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ )
+ end
+ end
+
+ context 'when the user is not authenticated' do
+ let(:current_user) { nil }
+
+ it 'responds with an error' do
+ post_graphql(query, current_user: current_user)
+ expect(graphql_errors.first['message']).to eq(
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ )
+ end
+ end
+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
new file mode 100644
index 00000000000..5d3e19c100d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Integrations::Exclusions::Create, feature_category: :integrations do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:admin_user) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let(:current_user) { admin_user }
+ let(:mutation) { graphql_mutation(:integration_exclusion_create, args) }
+ let(:args) do
+ {
+ 'integrationName' => 'BEYOND_IDENTITY',
+ 'projectIds' => project_ids
+ }
+ end
+
+ let(:project_ids) { [project.to_global_id.to_s] }
+
+ subject(:resolve_mutation) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when the user is not authorized' do
+ let(:current_user) { user }
+
+ it 'responds with an error' do
+ resolve_mutation
+ expect(graphql_errors.first['message']).to eq(
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ )
+ end
+ end
+
+ context 'when the user is authorized' do
+ let(:current_user) { admin_user }
+
+ it 'creates inactive integrations for the projects' do
+ expect { resolve_mutation }.to change { Integration.count }.from(0).to(1)
+ end
+
+ context 'when integrations exist for the projects' do
+ 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)
+ end
+
+ let(:project_ids) { [project, project2].map { |p| p.to_global_id.to_s } }
+
+ it 'updates existing integrations and creates integrations for projects' do
+ expect { resolve_mutation }.to change { Integration.count }.from(2).to(3)
+ existing_exclusion.reload
+ expect(existing_exclusion).not_to be_active
+ expect(existing_exclusion.inherit_from_id).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb
new file mode 100644
index 00000000000..2e4dce2e4d0
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Integrations::Exclusions::Delete, feature_category: :integrations do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:admin_user) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+ let(:current_user) { admin_user }
+ let(:mutation) { graphql_mutation(:integration_exclusion_delete, args) }
+ let(:args) do
+ {
+ 'integrationName' => 'BEYOND_IDENTITY',
+ 'projectIds' => project_ids
+ }
+ end
+
+ let(:project_ids) { [project.to_global_id.to_s] }
+
+ subject(:resolve_mutation) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when the user is not authorized' do
+ let(:current_user) { user }
+
+ it 'responds with an error' do
+ resolve_mutation
+ expect(graphql_errors.first['message']).to eq(
+ Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR
+ )
+ end
+ end
+
+ context 'when the user is authorized' do
+ let(:current_user) { admin_user }
+
+ it 'returns an empty array' do
+ resolve_mutation
+ expect(graphql_data['integrationExclusionDelete']['exclusions']).to eq([])
+ end
+
+ context 'and there are integrations' do
+ let!(:instance_integration) { create(:beyond_identity_integration) }
+ let!(:existing_exclusion) do
+ create(:beyond_identity_integration, project: project, active: false, inherit_from_id: nil,
+ instance: false)
+ end
+
+ it 'enables the integration for the specified project' do
+ resolve_mutation
+
+ existing_exclusion.reload
+ expect(existing_exclusion).to be_activated
+ expect(existing_exclusion.inherit_from_id).to eq(instance_integration.id)
+ exclusion_response = graphql_data['integrationExclusionDelete']['exclusions'][0]
+ expect(exclusion_response['project']['id']).to eq(project.to_global_id.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/requests/import/source_users_controller_spec.rb b/spec/requests/import/source_users_controller_spec.rb
new file mode 100644
index 00000000000..63b3634e022
--- /dev/null
+++ b/spec/requests/import/source_users_controller_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::SourceUsersController, feature_category: :importers do
+ shared_examples 'it requires feature flag' do
+ context 'when :improved_user_mapping is disabled' do
+ it 'returns 404' do
+ stub_feature_flags(bulk_import_user_mapping: false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ shared_examples 'it requires the user to be signed in' do
+ context 'when the user is not signed in' do
+ it 'redirects to the login screen' do
+ subject
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ let_it_be_with_reload(:source_user) { create(:import_source_user, :with_reassign_to_user, :awaiting_approval) }
+
+ describe 'POST /accept' do
+ let(:path) { accept_import_source_user_path(source_user) }
+
+ subject(:accept_invite) { post path }
+
+ context 'when signed in' do
+ before do
+ sign_in(source_user.reassign_to_user)
+ end
+
+ it { expect { accept_invite }.to change { source_user.reload.completed? }.from(false).to(true) }
+
+ it 'redirects with a notice when accepted' do
+ accept_invite
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to match(/You have approved the reassignment/)
+ end
+
+ it 'can only be accepted by the reassign_to_user' do
+ source_user.update!(reassign_to_user: create(:user))
+
+ expect { accept_invite }.not_to change { source_user.reload.status }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'cannot be accepted twice' do
+ source_user.accept!
+
+ accept_invite
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:alert]).to match(/could not be accepted/)
+ end
+ end
+
+ it_behaves_like 'it requires feature flag'
+ it_behaves_like 'it requires the user to be signed in'
+ end
+
+ describe 'POST /decline' do
+ let(:path) { decline_import_source_user_path(source_user) }
+
+ subject(:reject_invite) { post path }
+
+ context 'when signed in' do
+ before do
+ sign_in(source_user.reassign_to_user)
+ end
+
+ it { expect { reject_invite }.to change { source_user.reload.rejected? }.from(false).to(true) }
+ it { expect { reject_invite }.to change { source_user.reload.reassign_to_user }.from(instance_of(User)).to(nil) }
+
+ it 'redirects with a notice' do
+ reject_invite
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:notice]).to match(/You have rejected the reassignment/)
+ end
+
+ it 'cannot be declined after being accepted' do
+ source_user.accept!
+
+ reject_invite
+
+ expect(response).to redirect_to(root_path)
+ expect(flash[:alert]).to match(/could not be declined/)
+ end
+ end
+
+ it_behaves_like 'it requires feature flag'
+ it_behaves_like 'it requires the user to be signed in'
+ end
+end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index ac829264778..d105a3308d3 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -128,6 +128,24 @@ RSpec.describe Groups::CreateService, '#execute', feature_category: :groups_and_
context 'with before_commit callback' do
it_behaves_like 'has sync-ed traversal_ids'
end
+
+ describe 'handling of allow_runner_registration_token default' do
+ context 'when on self-managed' do
+ it 'does not disallow runner registration token' do
+ expect(created_group.allow_runner_registration_token?).to eq true
+ end
+ end
+
+ context 'when instance is dedicated' do
+ before do
+ Gitlab::CurrentSettings.update!(gitlab_dedicated_instance: true)
+ end
+
+ it 'does not disallow runner registration token' do
+ expect(created_group.allow_runner_registration_token?).to eq true
+ end
+ end
+ end
end
context 'when user can not create a group' do
diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
index 391336526e3..cea5ce74f07 100644
--- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
@@ -18,6 +18,7 @@ RSpec.shared_context 'with FOSS query type fields' do
:gitpod_enabled,
:group,
:groups,
+ :integration_exclusions,
:issue,
:issues,
:jobs,
diff --git a/spec/workers/users/unconfirmed_secondary_emails_deletion_cron_worker_spec.rb b/spec/workers/users/unconfirmed_secondary_emails_deletion_cron_worker_spec.rb
new file mode 100644
index 00000000000..cd6ede8ce2e
--- /dev/null
+++ b/spec/workers/users/unconfirmed_secondary_emails_deletion_cron_worker_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::UnconfirmedSecondaryEmailsDeletionCronWorker, feature_category: :user_management do
+ subject(:worker) { described_class.new }
+
+ it_behaves_like 'an idempotent worker'
+
+ describe '#perform' do
+ it 'deletes unconfirmed secondary emails created before the cutoff', :aggregate_failures, :freeze_time do
+ cut_off = ApplicationSetting::USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS.days.ago
+
+ unconfirmed_secondary_email_created_before_cut_off_1 = create(
+ :email,
+ created_at: cut_off - 1.second
+ )
+
+ unconfirmed_secondary_email_created_before_cut_off_2 = create(
+ :email,
+ created_at: cut_off - 1.day
+ )
+
+ unconfirmed_secondary_email_created_at_cut_off = create(
+ :email,
+ created_at: cut_off
+ )
+
+ unconfirmed_secondary_email_created_after_cut_off_1 = create(
+ :email,
+ created_at: cut_off + 1.second
+ )
+
+ unconfirmed_secondary_email_created_after_cut_off_2 = create(
+ :email,
+ created_at: cut_off + 1.day
+ )
+
+ confirmed_secondary_email_created_before_cut_off_1 = create(
+ :email,
+ :confirmed,
+ created_at: cut_off - 1.second
+ )
+
+ confirmed_secondary_email_created_before_cut_off_2 = create(
+ :email,
+ :confirmed,
+ created_at: cut_off - 1.day
+ )
+
+ confirmed_secondary_email_created_at_cut_off = create(
+ :email,
+ :confirmed,
+ created_at: cut_off
+ )
+
+ confirmed_secondary_email_created_after_cut_off_1 = create(
+ :email,
+ :confirmed,
+ created_at: cut_off + 1.second
+ )
+
+ confirmed_secondary_email_created_after_cut_off_2 = create(
+ :email,
+ :confirmed,
+ created_at: cut_off + 1.day
+ )
+
+ expect { worker.perform }.to change { Email.count }.by(-2)
+
+ expect(Email.exists?(unconfirmed_secondary_email_created_before_cut_off_1.id)).to eq(false)
+ expect(Email.exists?(unconfirmed_secondary_email_created_before_cut_off_2.id)).to eq(false)
+ expect(Email.exists?(unconfirmed_secondary_email_created_at_cut_off.id)).to eq(true)
+ expect(Email.exists?(unconfirmed_secondary_email_created_after_cut_off_1.id)).to eq(true)
+ expect(Email.exists?(unconfirmed_secondary_email_created_after_cut_off_2.id)).to eq(true)
+
+ expect(Email.exists?(confirmed_secondary_email_created_before_cut_off_1.id)).to eq(true)
+ expect(Email.exists?(confirmed_secondary_email_created_before_cut_off_2.id)).to eq(true)
+ expect(Email.exists?(confirmed_secondary_email_created_at_cut_off.id)).to eq(true)
+ expect(Email.exists?(confirmed_secondary_email_created_after_cut_off_1.id)).to eq(true)
+ expect(Email.exists?(confirmed_secondary_email_created_after_cut_off_2.id)).to eq(true)
+ end
+ end
+end
diff --git a/tooling/danger/project_helper.rb b/tooling/danger/project_helper.rb
index 2a2019c96a8..bea637a88aa 100644
--- a/tooling/danger/project_helper.rb
+++ b/tooling/danger/project_helper.rb
@@ -3,7 +3,7 @@
module Tooling
module Danger
module ProjectHelper
- CI_ONLY_RULES ||= %w[
+ CI_ONLY_RULES = %w[
ce_ee_vue_templates
datateam
feature_flag