From aa068e95cbe9bc1fbf90a45978811cc28d2bc739 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 11 Apr 2025 03:07:15 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../layout/line_continuation_spacing.yml | 1 - .../rspec/multiple_memoized_helpers.yml | 1 + .../mutations/work_items/bulk_update.rb | 4 - app/helpers/application_settings_helper.rb | 20 +-- app/helpers/groups_helper.rb | 14 +- app/mailers/emails/groups.rb | 14 ++ app/mailers/previews/notify_preview.rb | 11 ++ .../groups/mark_for_deletion_service.rb | 8 +- app/services/notification_service.rb | 13 ++ .../projects/mark_for_deletion_service.rb | 4 +- .../group_scheduled_for_deletion.html.haml | 7 + .../group_scheduled_for_deletion.text.erb | 4 + .../beta/bulk_update_work_items_mutation.yml | 9 - ... => group_deletion_notification_email.yml} | 7 +- .../work_item_custom_lifecycle_statuses.yml | 14 ++ db/docs/work_item_custom_lifecycles.yml | 13 ++ db/docs/work_item_custom_statuses.yml | 13 ++ db/docs/work_item_type_custom_lifecycles.yml | 13 ++ ...224050_create_work_item_custom_statuses.rb | 19 +++ ...mespace_fk_to_work_item_custom_statuses.rb | 15 ++ ...2202_create_work_item_custom_lifecycles.rb | 22 +++ ...space_fk_to_work_item_custom_lifecycles.rb | 15 ++ ...tatus_fk_to_work_item_custom_lifecycles.rb | 15 ++ ...tatus_fk_to_work_item_custom_lifecycles.rb | 15 ++ ...tatus_fk_to_work_item_custom_lifecycles.rb | 15 ++ ...ate_work_item_custom_lifecycle_statuses.rb | 18 ++ ..._to_work_item_custom_lifecycle_statuses.rb | 20 +++ ..._to_work_item_custom_lifecycle_statuses.rb | 15 ++ ..._to_work_item_custom_lifecycle_statuses.rb | 15 ++ ..._to_work_item_custom_lifecycle_statuses.rb | 15 ++ ...create_work_item_type_custom_lifecycles.rb | 18 ++ ..._fk_to_work_item_type_custom_lifecycles.rb | 15 ++ ..._fk_to_work_item_type_custom_lifecycles.rb | 15 ++ ..._fk_to_work_item_type_custom_lifecycles.rb | 15 ++ ...onstraints_to_work_item_custom_statuses.rb | 20 +++ db/schema_migrations/20250408224050 | 1 + db/schema_migrations/20250408230625 | 1 + db/schema_migrations/20250408232202 | 1 + db/schema_migrations/20250408233737 | 1 + db/schema_migrations/20250408233905 | 1 + db/schema_migrations/20250408234023 | 1 + db/schema_migrations/20250408234111 | 1 + db/schema_migrations/20250408234759 | 1 + db/schema_migrations/20250408235240 | 1 + db/schema_migrations/20250408235857 | 1 + db/schema_migrations/20250408235947 | 1 + db/schema_migrations/20250409000206 | 1 + db/schema_migrations/20250409000920 | 1 + db/schema_migrations/20250409002550 | 1 + db/schema_migrations/20250409002718 | 1 + db/schema_migrations/20250409002923 | 1 + db/schema_migrations/20250409005125 | 1 + db/structure.sql | 158 ++++++++++++++++++ doc/.vale/gitlab_base/SentenceSpacing.yml | 3 +- .../compliance/audit_event_streaming.md | 2 +- doc/administration/moderate_users.md | 2 +- doc/administration/object_storage.md | 2 +- doc/administration/raketasks/maintenance.md | 2 +- doc/api/todos.md | 2 +- doc/development/fe_guide/icons.md | 2 +- doc/development/fe_guide/troubleshooting.md | 4 +- .../standard_context_fields.md | 4 +- .../sec/analyzer_development_guide.md | 18 +- doc/development/work_items_widgets.md | 8 +- doc/integration/openid_connect_provider.md | 2 +- doc/security/hardening_nist_800_53.md | 2 +- .../responding_to_security_incidents.md | 2 +- .../customizing_analyzer_settings.md | 2 +- .../customizing_analyzer_settings.md | 2 +- .../policies/scan_execution_policies.md | 2 +- .../secret_detection/pipeline/_index.md | 15 +- doc/user/compliance/audit_event_streaming.md | 2 +- doc/user/group/epics/manage_epics.md | 2 + doc/user/project/settings/import_export.md | 2 +- doc/user/work_items/custom_fields.md | 4 +- lib/api/entities/wiki_page/meta.rb | 13 ++ spec/fast_spec_helper.rb | 1 + spec/helpers/groups_helper_spec.rb | 89 +++++++++- spec/mailers/emails/groups_spec.rb | 30 ++++ .../mutations/work_items/bulk_update_spec.rb | 14 -- spec/requests/api/todos_spec.rb | 32 ++-- .../groups/mark_for_deletion_service_spec.rb | 69 ++++++-- spec/services/notification_service_spec.rb | 58 +++++++ .../mark_for_deletion_service_spec.rb | 33 ---- 84 files changed, 911 insertions(+), 141 deletions(-) create mode 100644 app/views/notify/group_scheduled_for_deletion.html.haml create mode 100644 app/views/notify/group_scheduled_for_deletion.text.erb delete mode 100644 config/feature_flags/beta/bulk_update_work_items_mutation.yml rename config/feature_flags/gitlab_com_derisk/{project_deletion_notification_email.yml => group_deletion_notification_email.yml} (81%) create mode 100644 db/docs/work_item_custom_lifecycle_statuses.yml create mode 100644 db/docs/work_item_custom_lifecycles.yml create mode 100644 db/docs/work_item_custom_statuses.yml create mode 100644 db/docs/work_item_type_custom_lifecycles.yml create mode 100644 db/migrate/20250408224050_create_work_item_custom_statuses.rb create mode 100644 db/migrate/20250408230625_add_namespace_fk_to_work_item_custom_statuses.rb create mode 100644 db/migrate/20250408232202_create_work_item_custom_lifecycles.rb create mode 100644 db/migrate/20250408233737_add_namespace_fk_to_work_item_custom_lifecycles.rb create mode 100644 db/migrate/20250408233905_add_open_status_fk_to_work_item_custom_lifecycles.rb create mode 100644 db/migrate/20250408234023_add_closed_status_fk_to_work_item_custom_lifecycles.rb create mode 100644 db/migrate/20250408234111_add_duplicate_status_fk_to_work_item_custom_lifecycles.rb create mode 100644 db/migrate/20250408234759_create_work_item_custom_lifecycle_statuses.rb create mode 100644 db/migrate/20250408235240_add_constraints_to_work_item_custom_lifecycle_statuses.rb create mode 100644 db/migrate/20250408235857_add_namespace_fk_to_work_item_custom_lifecycle_statuses.rb create mode 100644 db/migrate/20250408235947_add_lifecycle_fk_to_work_item_custom_lifecycle_statuses.rb create mode 100644 db/migrate/20250409000206_add_status_fk_to_work_item_custom_lifecycle_statuses.rb create mode 100644 db/migrate/20250409000920_create_work_item_type_custom_lifecycles.rb create mode 100644 db/migrate/20250409002550_add_namespace_fk_to_work_item_type_custom_lifecycles.rb create mode 100644 db/migrate/20250409002718_add_work_item_type_fk_to_work_item_type_custom_lifecycles.rb create mode 100644 db/migrate/20250409002923_add_lifecycle_fk_to_work_item_type_custom_lifecycles.rb create mode 100644 db/migrate/20250409005125_add_constraints_to_work_item_custom_statuses.rb create mode 100644 db/schema_migrations/20250408224050 create mode 100644 db/schema_migrations/20250408230625 create mode 100644 db/schema_migrations/20250408232202 create mode 100644 db/schema_migrations/20250408233737 create mode 100644 db/schema_migrations/20250408233905 create mode 100644 db/schema_migrations/20250408234023 create mode 100644 db/schema_migrations/20250408234111 create mode 100644 db/schema_migrations/20250408234759 create mode 100644 db/schema_migrations/20250408235240 create mode 100644 db/schema_migrations/20250408235857 create mode 100644 db/schema_migrations/20250408235947 create mode 100644 db/schema_migrations/20250409000206 create mode 100644 db/schema_migrations/20250409000920 create mode 100644 db/schema_migrations/20250409002550 create mode 100644 db/schema_migrations/20250409002718 create mode 100644 db/schema_migrations/20250409002923 create mode 100644 db/schema_migrations/20250409005125 create mode 100644 lib/api/entities/wiki_page/meta.rb diff --git a/.rubocop_todo/layout/line_continuation_spacing.yml b/.rubocop_todo/layout/line_continuation_spacing.yml index 27844e6da48..bf933ed6ff7 100644 --- a/.rubocop_todo/layout/line_continuation_spacing.yml +++ b/.rubocop_todo/layout/line_continuation_spacing.yml @@ -2,7 +2,6 @@ # Cop supports --autocorrect. Layout/LineContinuationSpacing: Exclude: - - 'app/helpers/application_settings_helper.rb' - 'app/helpers/projects_helper.rb' - 'app/helpers/tags_helper.rb' - 'app/helpers/tree_helper.rb' diff --git a/.rubocop_todo/rspec/multiple_memoized_helpers.yml b/.rubocop_todo/rspec/multiple_memoized_helpers.yml index 3bbe5778d2f..e6072e5ec50 100644 --- a/.rubocop_todo/rspec/multiple_memoized_helpers.yml +++ b/.rubocop_todo/rspec/multiple_memoized_helpers.yml @@ -25,6 +25,7 @@ RSpec/MultipleMemoizedHelpers: - 'spec/requests/api/issues/issues_spec.rb' - 'spec/requests/api/issues/put_projects_issues_spec.rb' - 'spec/requests/api/maven_packages_spec.rb' + - 'spec/requests/api/todos_spec.rb' - 'spec/requests/api/users_spec.rb' - 'spec/services/boards/issues/list_service_spec.rb' - 'spec/services/labels/promote_service_spec.rb' diff --git a/app/graphql/mutations/work_items/bulk_update.rb b/app/graphql/mutations/work_items/bulk_update.rb index 0efbce47f4b..27841ef3aa0 100644 --- a/app/graphql/mutations/work_items/bulk_update.rb +++ b/app/graphql/mutations/work_items/bulk_update.rb @@ -33,10 +33,6 @@ module Mutations description: 'Number of work items that were successfully updated.' def ready?(**args) - if Feature.disabled?(:bulk_update_work_items_mutation, parent_for!(args[:parent_id])) - raise_resource_not_available_error!('`bulk_update_work_items_mutation` feature flag is disabled.') - end - if args[:ids].size > MAX_WORK_ITEMS raise Gitlab::Graphql::Errors::ArgumentError, format( diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index f02e1928355..a71e7eddb59 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -193,7 +193,7 @@ module ApplicationSettingsHelper end def external_authorization_description - s_("ExternalAuthorization|Access to projects is validated on an external service "\ + s_("ExternalAuthorization|Access to projects is validated on an external service " \ "using their classification label.") end @@ -202,39 +202,39 @@ module ApplicationSettingsHelper end def external_authorization_timeout_help_text - s_("ExternalAuthorization|Period GitLab waits for a response from the external "\ + s_("ExternalAuthorization|Period GitLab waits for a response from the external " \ "service. If there is no response, access is denied. Default: 0.5 seconds.") end def external_authorization_url_help_text - s_("ExternalAuthorization|URL to which the projects make authorization requests. If the URL is blank, cross-project "\ - "features are available and can still specify classification "\ + s_("ExternalAuthorization|URL to which the projects make authorization requests. If the URL is blank, cross-project " \ + "features are available and can still specify classification " \ "labels for projects.") end def external_authorization_client_certificate_help_text - s_("ExternalAuthorization|Certificate used to authenticate with the external authorization service. "\ + s_("ExternalAuthorization|Certificate used to authenticate with the external authorization service. " \ "If blank, the server certificate is validated when accessing over HTTPS.") end def external_authorization_client_key_help_text - s_("ExternalAuthorization|Private key of client authentication certificate. "\ + s_("ExternalAuthorization|Private key of client authentication certificate. " \ "Encrypted when stored.") end def external_authorization_client_pass_help_text - s_("ExternalAuthorization|Passphrase required to decrypt the private key. "\ + s_("ExternalAuthorization|Passphrase required to decrypt the private key. " \ "Encrypted when stored.") end def external_authorization_client_url_help_text - s_("ExternalAuthorization|Classification label to use when requesting authorization if no specific "\ + s_("ExternalAuthorization|Classification label to use when requesting authorization if no specific " \ "label is defined on the project.") end def sidekiq_job_limiter_mode_help_text - _("How the job limiter handles jobs exceeding the thresholds specified below. "\ - "The 'track' mode only logs the jobs. The 'compress' mode compresses the jobs and "\ + _("How the job limiter handles jobs exceeding the thresholds specified below. " \ + "The 'track' mode only logs the jobs. The 'compress' mode compresses the jobs and " \ "raises an exception if the compressed size exceeds the limit.") end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index a1ba695e39a..ddacb46f081 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -86,14 +86,22 @@ module GroupsHelper } end - # Overridden in EE def remove_group_message(group, permanently_remove) + return permanently_delete_group_message(group) if permanently_remove + return permanently_delete_group_message(group) unless group.adjourned_deletion? + return permanently_delete_group_message(group) if group.marked_for_deletion? + + date = permanent_deletion_date_formatted(Date.current) + + _("The contents of this group, its subgroups and projects will be permanently deleted after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered.") % + { date: date, deletion_adjourned_period: group.deletion_adjourned_period } + end + + def permanently_delete_group_message(group) content = ''.html_safe content << content_tag(:span, format(_("You are about to delete the group %{group_name}."), group_name: group.name)) - additional_content = additional_removed_items(group) content << additional_content if additional_content.present? - content << remove_group_warning end diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb index 4922dff8a1c..505b5e3597d 100644 --- a/app/mailers/emails/groups.rb +++ b/app/mailers/emails/groups.rb @@ -2,6 +2,8 @@ module Emails module Groups + include NamespacesHelper + def group_was_exported_email(current_user, group) group_email(current_user, group, _('Group was exported')) end @@ -15,6 +17,18 @@ module Emails @errors = errors mail_with_locale(to: current_user.notification_email_for(@group), subject: subject(subj)) end + + def group_scheduled_for_deletion(recipient_id, group_id) + @group = ::Group.find(group_id) + @user = ::User.find(recipient_id) + @deletion_due_in_days = ::Gitlab::CurrentSettings.deletion_adjourned_period.days + @deletion_date = permanent_deletion_date_formatted(@group.marked_for_deletion_on, format: '%B %-d, %Y') + + email_with_layout( + to: @user.email, + subject: subject('Group scheduled for deletion') + ) + end end end diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index dc8d0a7933b..9c9ec693362 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -443,6 +443,17 @@ class NotifyPreview < ActionMailer::Preview end end + def group_scheduled_for_deletion + cleanup do + group.create_deletion_schedule!( + marked_for_deletion_on: Time.current, + deleting_user: user + ) + + ::Notify.group_scheduled_for_deletion(user.id, group.id).message + end + end + private def project diff --git a/app/services/groups/mark_for_deletion_service.rb b/app/services/groups/mark_for_deletion_service.rb index 3631471f386..c3b85b51a99 100644 --- a/app/services/groups/mark_for_deletion_service.rb +++ b/app/services/groups/mark_for_deletion_service.rb @@ -18,8 +18,12 @@ module Groups # rubocop:disable Gitlab/BoundedContexts -- existing top-level mod private - # overridden in EE - def send_group_deletion_notification; end + def send_group_deletion_notification + return unless ::Feature.enabled?(:group_deletion_notification_email, group) && + group.adjourned_deletion? + + ::NotificationService.new.group_scheduled_for_deletion(group) + end def create_deletion_schedule deletion_schedule = group.build_deletion_schedule(deletion_schedule_params) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 1c6ea33ed7c..5c7f8853e99 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -801,6 +801,19 @@ class NotificationService end end + def group_scheduled_for_deletion(group) + return if group.emails_disabled? + + recipients = group.members.active_without_invites_and_requests.owners.map(&:user) + + recipients.each do |recipient| + mailer.group_scheduled_for_deletion( + recipient.id, + group.id + ).deliver_later + end + end + protected def new_resource_email(target, current_user, method) diff --git a/app/services/projects/mark_for_deletion_service.rb b/app/services/projects/mark_for_deletion_service.rb index 32a653af6df..ac564e73fe5 100644 --- a/app/services/projects/mark_for_deletion_service.rb +++ b/app/services/projects/mark_for_deletion_service.rb @@ -29,9 +29,7 @@ module Projects private def send_project_deletion_notification - return unless ::Feature.enabled?(:project_deletion_notification_email, project) && - project.adjourned_deletion? && - project.marked_for_deletion? + return unless project.adjourned_deletion? && project.marked_for_deletion? ::NotificationService.new.project_scheduled_for_deletion(project) end diff --git a/app/views/notify/group_scheduled_for_deletion.html.haml b/app/views/notify/group_scheduled_for_deletion.html.haml new file mode 100644 index 00000000000..0fef9704b1e --- /dev/null +++ b/app/views/notify/group_scheduled_for_deletion.html.haml @@ -0,0 +1,7 @@ +%p + = _('Hi %{username}!') % { username: sanitize_name(@user.name) } +%p + = _('Your group %{group_name} has been marked for deletion and will be removed in %{days}.').html_safe % { group_name: link_to(@group.full_name, group_url(@group)), days: pluralize((@deletion_due_in_days / 1.day).to_i, _('day')) } +%p + - link_start = ''.html_safe % { url: url_for(controller: 'groups', action: 'edit', id: @group.full_path, anchor: 'js-advanced-settings', only_path: false) } + = _('If this was a mistake, you can %{link_start}retain the group%{link_end} before %{deletion_date}.').html_safe % { link_start: link_start, link_end: ''.html_safe, deletion_date: @deletion_date } diff --git a/app/views/notify/group_scheduled_for_deletion.text.erb b/app/views/notify/group_scheduled_for_deletion.text.erb new file mode 100644 index 00000000000..4a4beb0e7c2 --- /dev/null +++ b/app/views/notify/group_scheduled_for_deletion.text.erb @@ -0,0 +1,4 @@ +<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %> +<%= _('Your group %{group_name} has been marked for deletion and will be removed in %{days}.') % { group_name: @group.full_name, days: pluralize((@deletion_due_in_days / 1.day).to_i, _('day')) } %> +<%= _('View your group: %{group_url}') % { group_url: group_url(@group) } %> +<%= _('If this was a mistake, you can retain the group before %{deletion_date}: %{retention_url}') % { retention_url: url_for(controller: 'groups', action: 'edit', id: @group.full_path, anchor: 'js-advanced-settings', only_path: false), deletion_date: @deletion_date } %> diff --git a/config/feature_flags/beta/bulk_update_work_items_mutation.yml b/config/feature_flags/beta/bulk_update_work_items_mutation.yml deleted file mode 100644 index bcf587c958e..00000000000 --- a/config/feature_flags/beta/bulk_update_work_items_mutation.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: bulk_update_work_items_mutation -feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434296 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161507 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/476586 -milestone: '17.4' -group: group::project management -type: beta -default_enabled: true diff --git a/config/feature_flags/gitlab_com_derisk/project_deletion_notification_email.yml b/config/feature_flags/gitlab_com_derisk/group_deletion_notification_email.yml similarity index 81% rename from config/feature_flags/gitlab_com_derisk/project_deletion_notification_email.yml rename to config/feature_flags/gitlab_com_derisk/group_deletion_notification_email.yml index ecbd3fa6896..b9bdaee7d28 100644 --- a/config/feature_flags/gitlab_com_derisk/project_deletion_notification_email.yml +++ b/config/feature_flags/gitlab_com_derisk/group_deletion_notification_email.yml @@ -1,10 +1,9 @@ --- -name: project_deletion_notification_email +name: group_deletion_notification_email feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/522883 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184026 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/525979 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185270 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/526037 milestone: '17.11' group: group::authorization type: gitlab_com_derisk default_enabled: false - diff --git a/db/docs/work_item_custom_lifecycle_statuses.yml b/db/docs/work_item_custom_lifecycle_statuses.yml new file mode 100644 index 00000000000..98d792a2c12 --- /dev/null +++ b/db/docs/work_item_custom_lifecycle_statuses.yml @@ -0,0 +1,14 @@ +--- +table_name: work_item_custom_lifecycle_statuses +classes: +- WorkItems::Statuses::Custom::LifecycleStatus +feature_categories: +- team_planning +description: Stores the association between custom lifecycles and statuses with position + information for ordering +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186050 +milestone: '17.11' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces +table_size: small diff --git a/db/docs/work_item_custom_lifecycles.yml b/db/docs/work_item_custom_lifecycles.yml new file mode 100644 index 00000000000..9716ffa143d --- /dev/null +++ b/db/docs/work_item_custom_lifecycles.yml @@ -0,0 +1,13 @@ +--- +table_name: work_item_custom_lifecycles +classes: +- WorkItems::Statuses::Custom::Lifecycle +feature_categories: +- team_planning +description: Stores namespace-level custom lifecycle configurations with default statuses for open, closed, and duplicate states +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186050 +milestone: '17.11' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces +table_size: small diff --git a/db/docs/work_item_custom_statuses.yml b/db/docs/work_item_custom_statuses.yml new file mode 100644 index 00000000000..01397d9d87e --- /dev/null +++ b/db/docs/work_item_custom_statuses.yml @@ -0,0 +1,13 @@ +--- +table_name: work_item_custom_statuses +classes: +- WorkItems::Statuses::Custom::Status +feature_categories: +- team_planning +description: Stores namespace-level custom status definitions for work items +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186050 +milestone: '17.11' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces +table_size: small diff --git a/db/docs/work_item_type_custom_lifecycles.yml b/db/docs/work_item_type_custom_lifecycles.yml new file mode 100644 index 00000000000..ff8a617a3e7 --- /dev/null +++ b/db/docs/work_item_type_custom_lifecycles.yml @@ -0,0 +1,13 @@ +--- +table_name: work_item_type_custom_lifecycles +classes: +- WorkItems::TypeCustomLifecycle +feature_categories: +- team_planning +description: Associates work item types with custom lifecycles within a namespace +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186050 +milestone: '17.11' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces +table_size: small diff --git a/db/migrate/20250408224050_create_work_item_custom_statuses.rb b/db/migrate/20250408224050_create_work_item_custom_statuses.rb new file mode 100644 index 00000000000..8bc266b5044 --- /dev/null +++ b/db/migrate/20250408224050_create_work_item_custom_statuses.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateWorkItemCustomStatuses < Gitlab::Database::Migration[2.2] + milestone '17.11' + + def change + # Factory: /ee/spec/factories/work_items/statuses/custom/statuses.rb + create_table :work_item_custom_statuses do |t| # rubocop:disable Migration/EnsureFactoryForTable -- reason above + t.bigint :namespace_id, null: false + t.timestamps_with_timezone null: false + t.integer :category, null: false, default: 1, limit: 1 + t.text :name, null: false, limit: 255 + t.text :description, limit: 255 + t.text :color, null: false, limit: 7 + + t.index [:namespace_id, :name], unique: true + end + end +end diff --git a/db/migrate/20250408230625_add_namespace_fk_to_work_item_custom_statuses.rb b/db/migrate/20250408230625_add_namespace_fk_to_work_item_custom_statuses.rb new file mode 100644 index 00000000000..3a4894f5971 --- /dev/null +++ b/db/migrate/20250408230625_add_namespace_fk_to_work_item_custom_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceFkToWorkItemCustomStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_statuses, :namespaces, + column: :namespace_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_statuses, column: :namespace_id + end +end diff --git a/db/migrate/20250408232202_create_work_item_custom_lifecycles.rb b/db/migrate/20250408232202_create_work_item_custom_lifecycles.rb new file mode 100644 index 00000000000..a5a5668b3b6 --- /dev/null +++ b/db/migrate/20250408232202_create_work_item_custom_lifecycles.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateWorkItemCustomLifecycles < Gitlab::Database::Migration[2.2] + milestone '17.11' + + def change + # Factory: /ee/spec/factories/work_items/statuses/custom/lifecycles.rb + create_table :work_item_custom_lifecycles do |t| # rubocop:disable Migration/EnsureFactoryForTable -- reason above + t.bigint :namespace_id, null: false + t.bigint :default_open_status_id, null: false, + index: { name: 'idx_wi_custom_lifecycles_on_open_status_id' } + t.bigint :default_closed_status_id, null: false, + index: { name: 'idx_wi_custom_lifecycles_on_closed_status_id' } + t.bigint :default_duplicate_status_id, null: false, + index: { name: 'idx_wi_custom_lifecycles_on_duplicate_status_id' } + t.timestamps_with_timezone null: false + t.text :name, null: false, limit: 255 + + t.index [:namespace_id, :name], unique: true + end + end +end diff --git a/db/migrate/20250408233737_add_namespace_fk_to_work_item_custom_lifecycles.rb b/db/migrate/20250408233737_add_namespace_fk_to_work_item_custom_lifecycles.rb new file mode 100644 index 00000000000..34bbbf75b93 --- /dev/null +++ b/db/migrate/20250408233737_add_namespace_fk_to_work_item_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceFkToWorkItemCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :namespaces, + column: :namespace_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :namespace_id + end +end diff --git a/db/migrate/20250408233905_add_open_status_fk_to_work_item_custom_lifecycles.rb b/db/migrate/20250408233905_add_open_status_fk_to_work_item_custom_lifecycles.rb new file mode 100644 index 00000000000..63744efe1f7 --- /dev/null +++ b/db/migrate/20250408233905_add_open_status_fk_to_work_item_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddOpenStatusFkToWorkItemCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :work_item_custom_statuses, + column: :default_open_status_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :default_open_status_id + end +end diff --git a/db/migrate/20250408234023_add_closed_status_fk_to_work_item_custom_lifecycles.rb b/db/migrate/20250408234023_add_closed_status_fk_to_work_item_custom_lifecycles.rb new file mode 100644 index 00000000000..f0f460f40fc --- /dev/null +++ b/db/migrate/20250408234023_add_closed_status_fk_to_work_item_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddClosedStatusFkToWorkItemCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :work_item_custom_statuses, + column: :default_closed_status_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :default_closed_status_id + end +end diff --git a/db/migrate/20250408234111_add_duplicate_status_fk_to_work_item_custom_lifecycles.rb b/db/migrate/20250408234111_add_duplicate_status_fk_to_work_item_custom_lifecycles.rb new file mode 100644 index 00000000000..bfa8107039d --- /dev/null +++ b/db/migrate/20250408234111_add_duplicate_status_fk_to_work_item_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddDuplicateStatusFkToWorkItemCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :work_item_custom_statuses, + column: :default_duplicate_status_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :default_duplicate_status_id + end +end diff --git a/db/migrate/20250408234759_create_work_item_custom_lifecycle_statuses.rb b/db/migrate/20250408234759_create_work_item_custom_lifecycle_statuses.rb new file mode 100644 index 00000000000..e22bff45f4f --- /dev/null +++ b/db/migrate/20250408234759_create_work_item_custom_lifecycle_statuses.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateWorkItemCustomLifecycleStatuses < Gitlab::Database::Migration[2.2] + milestone '17.11' + + def change + # Factory: /ee/spec/factories/work_items/statuses/custom/lifecycle_statuses.rb + create_table :work_item_custom_lifecycle_statuses do |t| # rubocop:disable Migration/EnsureFactoryForTable -- reason above + t.bigint :namespace_id, null: false, index: { name: 'idx_wi_custom_lifecycle_statuses_on_namespace_id' } + t.bigint :lifecycle_id, null: false + t.bigint :status_id, null: false, index: { name: 'idx_wi_custom_lifecycle_statuses_on_status_id' } + t.timestamps_with_timezone null: false + t.integer :position, null: false, default: 0 + + t.index [:lifecycle_id, :status_id], unique: true, name: 'idx_lifecycle_statuses_on_lifecycle_and_status' + end + end +end diff --git a/db/migrate/20250408235240_add_constraints_to_work_item_custom_lifecycle_statuses.rb b/db/migrate/20250408235240_add_constraints_to_work_item_custom_lifecycle_statuses.rb new file mode 100644 index 00000000000..fde16a75c10 --- /dev/null +++ b/db/migrate/20250408235240_add_constraints_to_work_item_custom_lifecycle_statuses.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddConstraintsToWorkItemCustomLifecycleStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_check_constraint :work_item_custom_lifecycle_statuses, 'position >= 0', constraint_name + end + + def down + remove_check_constraint :work_item_custom_lifecycle_statuses, constraint_name + end + + private + + def constraint_name + check_constraint_name(:work_item_custom_lifecycle_statuses, :position, 'positive') + end +end diff --git a/db/migrate/20250408235857_add_namespace_fk_to_work_item_custom_lifecycle_statuses.rb b/db/migrate/20250408235857_add_namespace_fk_to_work_item_custom_lifecycle_statuses.rb new file mode 100644 index 00000000000..7942bcc66e8 --- /dev/null +++ b/db/migrate/20250408235857_add_namespace_fk_to_work_item_custom_lifecycle_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceFkToWorkItemCustomLifecycleStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycle_statuses, :namespaces, + column: :namespace_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycle_statuses, column: :namespace_id + end +end diff --git a/db/migrate/20250408235947_add_lifecycle_fk_to_work_item_custom_lifecycle_statuses.rb b/db/migrate/20250408235947_add_lifecycle_fk_to_work_item_custom_lifecycle_statuses.rb new file mode 100644 index 00000000000..78b04452ce7 --- /dev/null +++ b/db/migrate/20250408235947_add_lifecycle_fk_to_work_item_custom_lifecycle_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddLifecycleFkToWorkItemCustomLifecycleStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycle_statuses, :work_item_custom_lifecycles, + column: :lifecycle_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycle_statuses, column: :lifecycle_id + end +end diff --git a/db/migrate/20250409000206_add_status_fk_to_work_item_custom_lifecycle_statuses.rb b/db/migrate/20250409000206_add_status_fk_to_work_item_custom_lifecycle_statuses.rb new file mode 100644 index 00000000000..2b0c04fc368 --- /dev/null +++ b/db/migrate/20250409000206_add_status_fk_to_work_item_custom_lifecycle_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddStatusFkToWorkItemCustomLifecycleStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycle_statuses, :work_item_custom_statuses, + column: :status_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycle_statuses, column: :status_id + end +end diff --git a/db/migrate/20250409000920_create_work_item_type_custom_lifecycles.rb b/db/migrate/20250409000920_create_work_item_type_custom_lifecycles.rb new file mode 100644 index 00000000000..58b661a0d76 --- /dev/null +++ b/db/migrate/20250409000920_create_work_item_type_custom_lifecycles.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateWorkItemTypeCustomLifecycles < Gitlab::Database::Migration[2.2] + milestone '17.11' + + def change + # Factory: /ee/spec/factories/work_items/type_custom_lifecycles.rb + create_table :work_item_type_custom_lifecycles do |t| # rubocop:disable Migration/EnsureFactoryForTable -- reason above + t.bigint :namespace_id, null: false + t.bigint :work_item_type_id, null: false, index: { name: 'idx_wi_type_custom_lifecycles_on_work_item_type_id' } + t.bigint :lifecycle_id, null: false, index: { name: 'idx_wi_type_custom_lifecycles_on_lifecycle_id' } + t.timestamps_with_timezone null: false + + t.index [:namespace_id, :work_item_type_id, :lifecycle_id], + name: 'idx_wi_type_custom_lifecycles_on_namespace_type_lifecycle', unique: true + end + end +end diff --git a/db/migrate/20250409002550_add_namespace_fk_to_work_item_type_custom_lifecycles.rb b/db/migrate/20250409002550_add_namespace_fk_to_work_item_type_custom_lifecycles.rb new file mode 100644 index 00000000000..3c554355fbe --- /dev/null +++ b/db/migrate/20250409002550_add_namespace_fk_to_work_item_type_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNamespaceFkToWorkItemTypeCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_type_custom_lifecycles, :namespaces, + column: :namespace_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_type_custom_lifecycles, column: :namespace_id + end +end diff --git a/db/migrate/20250409002718_add_work_item_type_fk_to_work_item_type_custom_lifecycles.rb b/db/migrate/20250409002718_add_work_item_type_fk_to_work_item_type_custom_lifecycles.rb new file mode 100644 index 00000000000..e82f83a2222 --- /dev/null +++ b/db/migrate/20250409002718_add_work_item_type_fk_to_work_item_type_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddWorkItemTypeFkToWorkItemTypeCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_type_custom_lifecycles, :work_item_types, + column: :work_item_type_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_type_custom_lifecycles, column: :work_item_type_id + end +end diff --git a/db/migrate/20250409002923_add_lifecycle_fk_to_work_item_type_custom_lifecycles.rb b/db/migrate/20250409002923_add_lifecycle_fk_to_work_item_type_custom_lifecycles.rb new file mode 100644 index 00000000000..13fa498f442 --- /dev/null +++ b/db/migrate/20250409002923_add_lifecycle_fk_to_work_item_type_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddLifecycleFkToWorkItemTypeCustomLifecycles < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_concurrent_foreign_key :work_item_type_custom_lifecycles, :work_item_custom_lifecycles, + column: :lifecycle_id, on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :work_item_type_custom_lifecycles, column: :lifecycle_id + end +end diff --git a/db/migrate/20250409005125_add_constraints_to_work_item_custom_statuses.rb b/db/migrate/20250409005125_add_constraints_to_work_item_custom_statuses.rb new file mode 100644 index 00000000000..fb250fa95c1 --- /dev/null +++ b/db/migrate/20250409005125_add_constraints_to_work_item_custom_statuses.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddConstraintsToWorkItemCustomStatuses < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.11' + + def up + add_check_constraint :work_item_custom_statuses, 'category > 0', constraint_name + end + + def down + remove_check_constraint :work_item_custom_statuses, constraint_name + end + + private + + def constraint_name + check_constraint_name(:work_item_custom_statuses, :category, 'positive') + end +end diff --git a/db/schema_migrations/20250408224050 b/db/schema_migrations/20250408224050 new file mode 100644 index 00000000000..a7510bdb8ad --- /dev/null +++ b/db/schema_migrations/20250408224050 @@ -0,0 +1 @@ +714cab6ca7bc87f6039c0c1d967b1e2388bd8a1f31de93989f778bfb7b78f2d3 \ No newline at end of file diff --git a/db/schema_migrations/20250408230625 b/db/schema_migrations/20250408230625 new file mode 100644 index 00000000000..7ec18622776 --- /dev/null +++ b/db/schema_migrations/20250408230625 @@ -0,0 +1 @@ +585ce1ee7e12eed9f42dea313b51a06066a5246dca33fd01406e6cc00fc7c11f \ No newline at end of file diff --git a/db/schema_migrations/20250408232202 b/db/schema_migrations/20250408232202 new file mode 100644 index 00000000000..05e12abb1ae --- /dev/null +++ b/db/schema_migrations/20250408232202 @@ -0,0 +1 @@ +fa33adec5be175d106873a06238e19550477a1148c7df1e0afd92ff4e9e38754 \ No newline at end of file diff --git a/db/schema_migrations/20250408233737 b/db/schema_migrations/20250408233737 new file mode 100644 index 00000000000..e568dab50b7 --- /dev/null +++ b/db/schema_migrations/20250408233737 @@ -0,0 +1 @@ +f0812398c78e3638bcbbe0ab36ef7c00be3a75d934cf67d649fbfa49f9bac82d \ No newline at end of file diff --git a/db/schema_migrations/20250408233905 b/db/schema_migrations/20250408233905 new file mode 100644 index 00000000000..2e3bf8a7ffc --- /dev/null +++ b/db/schema_migrations/20250408233905 @@ -0,0 +1 @@ +f596ff1f7890e5c72fa188b695a75f599178897c0d8b03e4cf922771099ef570 \ No newline at end of file diff --git a/db/schema_migrations/20250408234023 b/db/schema_migrations/20250408234023 new file mode 100644 index 00000000000..73592310503 --- /dev/null +++ b/db/schema_migrations/20250408234023 @@ -0,0 +1 @@ +5e8d636a4769ae19d0e6944cc44158375591caec73d98d00976800330711f95b \ No newline at end of file diff --git a/db/schema_migrations/20250408234111 b/db/schema_migrations/20250408234111 new file mode 100644 index 00000000000..eaae85f1350 --- /dev/null +++ b/db/schema_migrations/20250408234111 @@ -0,0 +1 @@ +b694bfa9009e4157239b4aa8f00bec178e0e34623390dca776d8749252370f7f \ No newline at end of file diff --git a/db/schema_migrations/20250408234759 b/db/schema_migrations/20250408234759 new file mode 100644 index 00000000000..dba8efe9a58 --- /dev/null +++ b/db/schema_migrations/20250408234759 @@ -0,0 +1 @@ +e813a2c65a5bfe770ca7265dbfafc1c0f07bc675dd3d1019bcdd3c73dca308b6 \ No newline at end of file diff --git a/db/schema_migrations/20250408235240 b/db/schema_migrations/20250408235240 new file mode 100644 index 00000000000..4af59868492 --- /dev/null +++ b/db/schema_migrations/20250408235240 @@ -0,0 +1 @@ +fea14c21c6c6095e1cc75ec5d91ca68ee251526ec57fd6dc6158661f42419c7b \ No newline at end of file diff --git a/db/schema_migrations/20250408235857 b/db/schema_migrations/20250408235857 new file mode 100644 index 00000000000..43de59246f3 --- /dev/null +++ b/db/schema_migrations/20250408235857 @@ -0,0 +1 @@ +c5371d0dc3018f961b6896ee397ad0480cbbda052e982f2ae9435807bfd4e85b \ No newline at end of file diff --git a/db/schema_migrations/20250408235947 b/db/schema_migrations/20250408235947 new file mode 100644 index 00000000000..3c3a3b96973 --- /dev/null +++ b/db/schema_migrations/20250408235947 @@ -0,0 +1 @@ +d82db1ad54f57442fbf34fab3b9c47889b306c663f7334899b89a1993a3c38b8 \ No newline at end of file diff --git a/db/schema_migrations/20250409000206 b/db/schema_migrations/20250409000206 new file mode 100644 index 00000000000..80778cec696 --- /dev/null +++ b/db/schema_migrations/20250409000206 @@ -0,0 +1 @@ +a6db729f2309953268392c13dc4d5bf1324590f6608646567251ec832e8bd051 \ No newline at end of file diff --git a/db/schema_migrations/20250409000920 b/db/schema_migrations/20250409000920 new file mode 100644 index 00000000000..9dee456af84 --- /dev/null +++ b/db/schema_migrations/20250409000920 @@ -0,0 +1 @@ +1d701d25a12f8bcf46a22d3d9ea7863e14e50855d73afc2e9ce897ff4d53bb8c \ No newline at end of file diff --git a/db/schema_migrations/20250409002550 b/db/schema_migrations/20250409002550 new file mode 100644 index 00000000000..26f2ae2e437 --- /dev/null +++ b/db/schema_migrations/20250409002550 @@ -0,0 +1 @@ +e719a4ce58be91853f8f640e13d7bc4291e743f6fd281655de49249a36606f1d \ No newline at end of file diff --git a/db/schema_migrations/20250409002718 b/db/schema_migrations/20250409002718 new file mode 100644 index 00000000000..c2d1d10a749 --- /dev/null +++ b/db/schema_migrations/20250409002718 @@ -0,0 +1 @@ +f6c271a17b3f5a898b1b3c3616e2c3057312c05b0e94eed57c383d4059189394 \ No newline at end of file diff --git a/db/schema_migrations/20250409002923 b/db/schema_migrations/20250409002923 new file mode 100644 index 00000000000..fc84f11b67c --- /dev/null +++ b/db/schema_migrations/20250409002923 @@ -0,0 +1 @@ +8fe7d68d8b13040e3d89792ef26146b3f14f785cdc14abf76d6b1ef22bad5ebe \ No newline at end of file diff --git a/db/schema_migrations/20250409005125 b/db/schema_migrations/20250409005125 new file mode 100644 index 00000000000..5cbe7c194a0 --- /dev/null +++ b/db/schema_migrations/20250409005125 @@ -0,0 +1 @@ +6c2672fb3be2944c3840986ce70797f2b262596b8709f858305174620387e5f1 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d2550bd3a2e..67617f0d1d5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25606,6 +25606,71 @@ CREATE SEQUENCE work_item_current_statuses_id_seq ALTER SEQUENCE work_item_current_statuses_id_seq OWNED BY work_item_current_statuses.id; +CREATE TABLE work_item_custom_lifecycle_statuses ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + lifecycle_id bigint NOT NULL, + status_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + "position" integer DEFAULT 0 NOT NULL, + CONSTRAINT check_91172799d3 CHECK (("position" >= 0)) +); + +CREATE SEQUENCE work_item_custom_lifecycle_statuses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE work_item_custom_lifecycle_statuses_id_seq OWNED BY work_item_custom_lifecycle_statuses.id; + +CREATE TABLE work_item_custom_lifecycles ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + default_open_status_id bigint NOT NULL, + default_closed_status_id bigint NOT NULL, + default_duplicate_status_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + name text NOT NULL, + CONSTRAINT check_1feff2de99 CHECK ((char_length(name) <= 255)) +); + +CREATE SEQUENCE work_item_custom_lifecycles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE work_item_custom_lifecycles_id_seq OWNED BY work_item_custom_lifecycles.id; + +CREATE TABLE work_item_custom_statuses ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + category smallint DEFAULT 1 NOT NULL, + name text NOT NULL, + description text, + color text NOT NULL, + CONSTRAINT check_4789467800 CHECK ((char_length(color) <= 7)), + CONSTRAINT check_720a7c4d24 CHECK ((char_length(name) <= 255)), + CONSTRAINT check_8ea8b3c991 CHECK ((char_length(description) <= 255)), + CONSTRAINT check_ff2bac1606 CHECK ((category > 0)) +); + +CREATE SEQUENCE work_item_custom_statuses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE work_item_custom_statuses_id_seq OWNED BY work_item_custom_statuses.id; + CREATE TABLE work_item_dates_sources ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, @@ -25766,6 +25831,24 @@ CREATE SEQUENCE work_item_type_custom_fields_id_seq ALTER SEQUENCE work_item_type_custom_fields_id_seq OWNED BY work_item_type_custom_fields.id; +CREATE TABLE work_item_type_custom_lifecycles ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + work_item_type_id bigint NOT NULL, + lifecycle_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE work_item_type_custom_lifecycles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE work_item_type_custom_lifecycles_id_seq OWNED BY work_item_type_custom_lifecycles.id; + CREATE TABLE work_item_type_user_preferences ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -28017,6 +28100,12 @@ ALTER TABLE ONLY wiki_repository_states ALTER COLUMN id SET DEFAULT nextval('wik ALTER TABLE ONLY work_item_current_statuses ALTER COLUMN id SET DEFAULT nextval('work_item_current_statuses_id_seq'::regclass); +ALTER TABLE ONLY work_item_custom_lifecycle_statuses ALTER COLUMN id SET DEFAULT nextval('work_item_custom_lifecycle_statuses_id_seq'::regclass); + +ALTER TABLE ONLY work_item_custom_lifecycles ALTER COLUMN id SET DEFAULT nextval('work_item_custom_lifecycles_id_seq'::regclass); + +ALTER TABLE ONLY work_item_custom_statuses ALTER COLUMN id SET DEFAULT nextval('work_item_custom_statuses_id_seq'::regclass); + ALTER TABLE ONLY work_item_hierarchy_restrictions ALTER COLUMN id SET DEFAULT nextval('work_item_hierarchy_restrictions_id_seq'::regclass); ALTER TABLE ONLY work_item_number_field_values ALTER COLUMN id SET DEFAULT nextval('work_item_number_field_values_id_seq'::regclass); @@ -28031,6 +28120,8 @@ ALTER TABLE ONLY work_item_text_field_values ALTER COLUMN id SET DEFAULT nextval ALTER TABLE ONLY work_item_type_custom_fields ALTER COLUMN id SET DEFAULT nextval('work_item_type_custom_fields_id_seq'::regclass); +ALTER TABLE ONLY work_item_type_custom_lifecycles ALTER COLUMN id SET DEFAULT nextval('work_item_type_custom_lifecycles_id_seq'::regclass); + ALTER TABLE ONLY work_item_type_user_preferences ALTER COLUMN id SET DEFAULT nextval('work_item_type_user_preferences_id_seq'::regclass); ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass); @@ -31215,6 +31306,15 @@ ALTER TABLE ONLY work_item_colors ALTER TABLE ONLY work_item_current_statuses ADD CONSTRAINT work_item_current_statuses_pkey PRIMARY KEY (id); +ALTER TABLE ONLY work_item_custom_lifecycle_statuses + ADD CONSTRAINT work_item_custom_lifecycle_statuses_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT work_item_custom_lifecycles_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY work_item_custom_statuses + ADD CONSTRAINT work_item_custom_statuses_pkey PRIMARY KEY (id); + ALTER TABLE ONLY work_item_dates_sources ADD CONSTRAINT work_item_dates_sources_pkey PRIMARY KEY (issue_id); @@ -31242,6 +31342,9 @@ ALTER TABLE ONLY work_item_text_field_values ALTER TABLE ONLY work_item_type_custom_fields ADD CONSTRAINT work_item_type_custom_fields_pkey PRIMARY KEY (id); +ALTER TABLE ONLY work_item_type_custom_lifecycles + ADD CONSTRAINT work_item_type_custom_lifecycles_pkey PRIMARY KEY (id); + ALTER TABLE ONLY work_item_type_user_preferences ADD CONSTRAINT work_item_type_user_preferences_pkey PRIMARY KEY (id); @@ -33154,6 +33257,8 @@ CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_ CREATE INDEX idx_keys_expires_at_and_before_expiry_notification_undelivered ON keys USING btree (date(timezone('UTC'::text, expires_at)), before_expiry_notification_delivered_at) WHERE (before_expiry_notification_delivered_at IS NULL); +CREATE UNIQUE INDEX idx_lifecycle_statuses_on_lifecycle_and_status ON work_item_custom_lifecycle_statuses USING btree (lifecycle_id, status_id); + CREATE INDEX idx_member_roles_on_base_access_level ON member_roles USING btree (base_access_level); CREATE INDEX idx_members_created_at_user_id_invite_token ON members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL)); @@ -33426,6 +33531,16 @@ CREATE UNIQUE INDEX idx_wi_current_statuses_on_wi_id_custom_status_id_unique ON CREATE UNIQUE INDEX idx_wi_current_statuses_on_wi_id_system_def_status_id_unique ON work_item_current_statuses USING btree (work_item_id, system_defined_status_id); +CREATE INDEX idx_wi_custom_lifecycle_statuses_on_namespace_id ON work_item_custom_lifecycle_statuses USING btree (namespace_id); + +CREATE INDEX idx_wi_custom_lifecycle_statuses_on_status_id ON work_item_custom_lifecycle_statuses USING btree (status_id); + +CREATE INDEX idx_wi_custom_lifecycles_on_closed_status_id ON work_item_custom_lifecycles USING btree (default_closed_status_id); + +CREATE INDEX idx_wi_custom_lifecycles_on_duplicate_status_id ON work_item_custom_lifecycles USING btree (default_duplicate_status_id); + +CREATE INDEX idx_wi_custom_lifecycles_on_open_status_id ON work_item_custom_lifecycles USING btree (default_open_status_id); + CREATE UNIQUE INDEX idx_wi_number_values_on_work_item_id_custom_field_id ON work_item_number_field_values USING btree (work_item_id, custom_field_id); CREATE INDEX idx_wi_select_field_values_on_custom_field_select_option_id ON work_item_select_field_values USING btree (custom_field_select_option_id); @@ -33436,6 +33551,12 @@ CREATE UNIQUE INDEX idx_wi_text_values_on_work_item_id_custom_field_id ON work_i CREATE UNIQUE INDEX idx_wi_type_custom_fields_on_ns_id_wi_type_id_custom_field_id ON work_item_type_custom_fields USING btree (namespace_id, work_item_type_id, custom_field_id); +CREATE INDEX idx_wi_type_custom_lifecycles_on_lifecycle_id ON work_item_type_custom_lifecycles USING btree (lifecycle_id); + +CREATE UNIQUE INDEX idx_wi_type_custom_lifecycles_on_namespace_type_lifecycle ON work_item_type_custom_lifecycles USING btree (namespace_id, work_item_type_id, lifecycle_id); + +CREATE INDEX idx_wi_type_custom_lifecycles_on_work_item_type_id ON work_item_type_custom_lifecycles USING btree (work_item_type_id); + CREATE INDEX idx_zoekt_last_indexed_at_gt_used_storage_bytes_updated_at ON zoekt_indices USING btree (used_storage_bytes_updated_at) WHERE (last_indexed_at >= used_storage_bytes_updated_at); CREATE INDEX idx_zoekt_repositories_on_zoekt_index_id_and_size_bytes ON zoekt_repositories USING btree (zoekt_index_id, size_bytes); @@ -38006,6 +38127,10 @@ CREATE INDEX index_work_item_current_statuses_on_namespace_id ON work_item_curre CREATE UNIQUE INDEX index_work_item_current_statuses_on_work_item_id ON work_item_current_statuses USING btree (work_item_id); +CREATE UNIQUE INDEX index_work_item_custom_lifecycles_on_namespace_id_and_name ON work_item_custom_lifecycles USING btree (namespace_id, name); + +CREATE UNIQUE INDEX index_work_item_custom_statuses_on_namespace_id_and_name ON work_item_custom_statuses USING btree (namespace_id, name); + CREATE INDEX index_work_item_hierarchy_restrictions_on_child_type_id ON work_item_hierarchy_restrictions USING btree (child_type_id); CREATE UNIQUE INDEX index_work_item_hierarchy_restrictions_on_parent_and_child ON work_item_hierarchy_restrictions USING btree (parent_type_id, child_type_id); @@ -41688,6 +41813,9 @@ ALTER TABLE ONLY deployments ALTER TABLE ONLY projects_branch_rules_merge_request_approval_settings ADD CONSTRAINT fk_00acf20382 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_00c659d395 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY epics ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL; @@ -41712,9 +41840,15 @@ ALTER TABLE ONLY audit_events_instance_google_cloud_logging_configurations ALTER TABLE ONLY service_desk_settings ADD CONSTRAINT fk_03afb71f06 FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE ONLY work_item_type_custom_lifecycles + ADD CONSTRAINT fk_03c6229585 FOREIGN KEY (lifecycle_id) REFERENCES work_item_custom_lifecycles(id) ON DELETE CASCADE; + ALTER TABLE ONLY design_management_designs_versions ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_type_custom_lifecycles + ADD CONSTRAINT fk_0425cd8e8b FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY external_status_checks_protected_branches ADD CONSTRAINT fk_0480f2308c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -41766,6 +41900,9 @@ ALTER TABLE ONLY observability_traces_issues_connections ALTER TABLE ONLY targeted_message_dismissals ADD CONSTRAINT fk_08c30af7ff FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_lifecycle_statuses + ADD CONSTRAINT fk_08e006a1a3 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY merge_request_assignment_events ADD CONSTRAINT fk_08f7602bfd FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; @@ -41781,6 +41918,9 @@ ALTER TABLE ONLY dast_sites ALTER TABLE ONLY project_saved_replies ADD CONSTRAINT fk_0ace76afbb FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE NOT VALID; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_0b028ab81c FOREIGN KEY (default_open_status_id) REFERENCES work_item_custom_statuses(id) ON DELETE CASCADE; + ALTER TABLE ONLY subscription_seat_assignments ADD CONSTRAINT fk_0b6bc63773 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -41835,6 +41975,9 @@ ALTER TABLE ONLY packages_package_file_build_infos ALTER TABLE ONLY audit_events_streaming_event_type_filters ADD CONSTRAINT fk_107946dffb FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_type_custom_lifecycles + ADD CONSTRAINT fk_111d417cb7 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE; + ALTER TABLE ONLY group_deletion_schedules ADD CONSTRAINT fk_11e3ebfcdd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -42183,6 +42326,9 @@ ALTER TABLE ONLY sprints ALTER TABLE ONLY operations_feature_flags_issues ADD CONSTRAINT fk_3685a990ae FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_statuses + ADD CONSTRAINT fk_3694bacabe FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY push_event_payloads ADD CONSTRAINT fk_36c74129da FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; @@ -42579,6 +42725,9 @@ ALTER TABLE ONLY audit_events_streaming_instance_namespace_filters ALTER TABLE ONLY terraform_state_versions ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_6e8df43239 FOREIGN KEY (default_duplicate_status_id) REFERENCES work_item_custom_statuses(id) ON DELETE CASCADE; + ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_6ee8249821 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -42717,6 +42866,9 @@ ALTER TABLE ONLY issue_customer_relations_contacts ALTER TABLE ONLY ssh_signatures ADD CONSTRAINT fk_7d2f93996c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_7d5eb33a21 FOREIGN KEY (default_closed_status_id) REFERENCES work_item_custom_statuses(id) ON DELETE CASCADE; + ALTER TABLE ONLY sent_notifications ADD CONSTRAINT fk_7d7663e36a FOREIGN KEY (issue_email_participant_id) REFERENCES issue_email_participants(id) ON DELETE SET NULL NOT VALID; @@ -42840,6 +42992,9 @@ ALTER TABLE ONLY protected_branch_merge_access_levels ALTER TABLE ONLY work_item_dates_sources ADD CONSTRAINT fk_8a4948b668 FOREIGN KEY (start_date_sourcing_work_item_id) REFERENCES issues(id) ON DELETE SET NULL; +ALTER TABLE ONLY work_item_custom_lifecycle_statuses + ADD CONSTRAINT fk_8a6dadaf44 FOREIGN KEY (status_id) REFERENCES work_item_custom_statuses(id) ON DELETE CASCADE; + ALTER TABLE ONLY targeted_message_namespaces ADD CONSTRAINT fk_8ba73cd32a FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -43050,6 +43205,9 @@ ALTER TABLE ONLY issuable_metric_images ALTER TABLE ONLY operations_strategies ADD CONSTRAINT fk_a542e10c31 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_lifecycle_statuses + ADD CONSTRAINT fk_a546eef539 FOREIGN KEY (lifecycle_id) REFERENCES work_item_custom_lifecycles(id) ON DELETE CASCADE; + ALTER TABLE ONLY lfs_objects_projects ADD CONSTRAINT fk_a56e02279c FOREIGN KEY (lfs_object_id) REFERENCES lfs_objects(id) ON DELETE RESTRICT NOT VALID; diff --git a/doc/.vale/gitlab_base/SentenceSpacing.yml b/doc/.vale/gitlab_base/SentenceSpacing.yml index b5f3e523a47..dc811286690 100644 --- a/doc/.vale/gitlab_base/SentenceSpacing.yml +++ b/doc/.vale/gitlab_base/SentenceSpacing.yml @@ -5,7 +5,7 @@ # # For a list of all options, see https://vale.sh/docs/topics/styles/ extends: existence -message: "Use exactly one space between sentences and clauses. Check '%s' for spacing problems." +message: "Use exactly one space with punctuation. Check '%s' for spacing problems." link: https://docs.gitlab.com/development/documentation/styleguide/#punctuation vocab: false level: error @@ -13,3 +13,4 @@ nonword: true tokens: - '[a-z][.?!,][A-Z]' - '[\w.?!,\(\)\-":] {2,}[\w.?!,\(\)\-":]' + - '[a-z] +[.?!,:] +' diff --git a/doc/administration/compliance/audit_event_streaming.md b/doc/administration/compliance/audit_event_streaming.md index c79147154f0..48aa2685560 100644 --- a/doc/administration/compliance/audit_event_streaming.md +++ b/doc/administration/compliance/audit_event_streaming.md @@ -199,7 +199,7 @@ To update a streaming destination's event filters: ### Override default content type header By default, streaming destinations use a `content-type` header of `application/x-www-form-urlencoded`. However, you -might want to set the `content-type` header to something else. For example ,`application/json`. +might want to set the `content-type` header to something else. For example, `application/json`. To override the `content-type` header default value for an instance streaming destination, use either: diff --git a/doc/administration/moderate_users.md b/doc/administration/moderate_users.md index fcd5d61ebb5..9569e7d4222 100644 --- a/doc/administration/moderate_users.md +++ b/doc/administration/moderate_users.md @@ -343,7 +343,7 @@ To unban a user: 1. On the left sidebar, at the bottom, select **Admin**. 1. Select **Overview > Users**. -1. In the search box , filter by **State=Banned** and press Enter. +1. In the search box, filter by **State=Banned** and press Enter. 1. Next to the member you want to ban, select the vertical ellipsis ({{< icon name="ellipsis_v" >}}). 1. From the dropdown list, select **Unban member**. diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 9898d3b6777..7ebed3ebf6a 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -1162,7 +1162,7 @@ Prerequisites: The configuration process is interactive. Add at least two "remotes": one for the object storage provider your data is currently on (`old`), and one for the provider you are moving to (`new`). -1. Verify that you can read the old data. The following example refers to the `uploads` bucket , but your bucket may have a different name: +1. Verify that you can read the old data. The following example refers to the `uploads` bucket, but your bucket may have a different name: ```shell rclone ls old:uploads | head diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index d4aa8dbe450..98a0629fc71 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -628,7 +628,7 @@ The messages returned are informational and can be ignored. ### PostgreSQL socket errors when executing the `gitlab:env:info` Rake task -After running `sudo gitlab-rake gitlab:env:info` on Gitaly or other non-Rails nodes , you might see the following error: +After running `sudo gitlab-rake gitlab:env:info` on Gitaly or other non-Rails nodes, you might see the following error: ```plaintext PG::ConnectionBad: could not connect to server: No such file or directory diff --git a/doc/api/todos.md b/doc/api/todos.md index 2c25f12d6ec..7e0b4004401 100644 --- a/doc/api/todos.md +++ b/doc/api/todos.md @@ -33,7 +33,7 @@ Parameters: | `project_id` | integer | no | The ID of a project | | `group_id` | integer | no | The ID of a group | | `state` | string | no | The state of the to-do item. Can be either `pending` or `done` | -| `type` | string | no | The type of to-do item. Can be either `Issue`, `MergeRequest`, `Commit`, `Epic`, `DesignManagement::Design`, `AlertManagement::Alert`, `Project`, `Namespace` or `Vulnerability` | +| `type` | string | no | The type of to-do item. Can be either `Issue`, `MergeRequest`, `Commit`, `Epic`, `DesignManagement::Design`, `AlertManagement::Alert`, `Project`, `Namespace`, `Vulnerability` or `WikiPage::Meta` | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/todos" diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index 7a20e1bb083..8c3947b6b28 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -27,7 +27,7 @@ sprite_icon(icon_name, size: nil, css_class: '') - **`icon_name`**: Use the `icon_name` for the SVG sprite in the list of ([GitLab SVGs](https://gitlab-org.gitlab.io/gitlab-svgs)). -- **`size` (optional)**: Use one of the following sizes : 16, 24, 32, 48, 72 (this +- **`size` (optional)**: Use one of the following sizes: 16, 24, 32, 48, 72 (this is translated into a `s16` class) - **`css_class` (optional)**: If you want to add additional CSS classes. diff --git a/doc/development/fe_guide/troubleshooting.md b/doc/development/fe_guide/troubleshooting.md index 1cdfd1ed87d..5a692dd00cd 100644 --- a/doc/development/fe_guide/troubleshooting.md +++ b/doc/development/fe_guide/troubleshooting.md @@ -102,7 +102,7 @@ If you see errors like `Missing field 'descriptionHtml' while writing result` , The error "Missing field 'description'" indicates that your GraphQL query expects a field named "description" in the response, but the data you're receiving from your backend (or how it's being processed by Apollo Client) is missing that field. This is causing Apollo Client's cache to fail when it tries to update the store with the incomplete data. -To debug this , follow the below steps +To debug this, follow the below steps 1. Open the error stack developer console @@ -139,7 +139,7 @@ If data related to a query has been invalidated or updated, the cache might not When using mutations, the cache might not automatically update unless you configure `refetchQueries` or use a manual cache update after the mutation. -For example : in the first query you have a couple of fields that were not requested in the subsequent query +For example: in the first query you have a couple of fields that were not requested in the subsequent query ```graphql query workItemTreeQuery($id: WorkItemID!, $pageSize: Int = 100, $endCursor: String) { diff --git a/doc/development/internal_analytics/internal_event_instrumentation/standard_context_fields.md b/doc/development/internal_analytics/internal_event_instrumentation/standard_context_fields.md index 0868aeddca1..c1e07e06fc4 100644 --- a/doc/development/internal_analytics/internal_event_instrumentation/standard_context_fields.md +++ b/doc/development/internal_analytics/internal_event_instrumentation/standard_context_fields.md @@ -17,8 +17,8 @@ Standard context, also referred to as [Cloud context](https://gitlab.com/gitlab- | Field | Type | Description | Example | |-------------------|---------------|---------------------------------------------------------------------------------------------------|---------------------| -| `project_id` | integer, null | ID of the associated project. This is available when tracking is done inside any project path. (example : [GitLab project](https://gitlab.com/gitlab-org/gitlab)) | `12345` | -| `namespace_id` | integer, null | ID of the associated namespace. This is available when tracking is done inside any group path. (example : [GitLab-org](https://gitlab.com/gitlab-org)) | `67890` | +| `project_id` | integer, null | ID of the associated project. This is available when tracking is done inside any project path. (example: [GitLab project](https://gitlab.com/gitlab-org/gitlab)) | `12345` | +| `namespace_id` | integer, null | ID of the associated namespace. This is available when tracking is done inside any group path. (example: [GitLab-org](https://gitlab.com/gitlab-org)) | `67890` | | `user_id` | integer, null | ID of the associated user. This gets pseudonymized in the Snowplow enricher. Refer to the [metrics dictionary](https://metrics.gitlab.com/identifiers/). | `longhash` | | `global_user_id` | string, null | An anonymized `user_id` hash unique across instances. | `longhash` | | `is_gitlab_team_member` | boolean, null | Indicates if the action was triggered by a GitLab team member. | `true`, `false` | diff --git a/doc/development/sec/analyzer_development_guide.md b/doc/development/sec/analyzer_development_guide.md index 6e2f2d2ffde..d596d8d57ea 100644 --- a/doc/development/sec/analyzer_development_guide.md +++ b/doc/development/sec/analyzer_development_guide.md @@ -163,11 +163,23 @@ To use Docker with `replace` in the `go.mod` file: Users may use tools other than Docker to orchestrate their containers and run their analyzers, such as [containerd](https://containerd.io/), [Podman](https://podman.io/), or [skopeo](https://github.com/containers/skopeo). -In order to avoid inadvertently adding proprietary Docker features which might break customer tools, we [run a periodic test](https://gitlab.com/gitlab-org/security-products/tests/analyzer-containerization-support/-/blob/main/.gitlab-ci.yml?ref_type=heads) for all analyzers, to ensure that these tools still function as expected, and a Slack alert is raised if a failure occurs. +To ensure compatibility with these tools, we [periodicically test](https://gitlab.com/gitlab-org/security-products/tests/analyzer-containerization-support/-/blob/main/.gitlab-ci.yml?ref_type=heads) +all analyzers using a scheduled pipeline. A Slack alert is raised if a test fails. -In addition to the periodic test, analyzers using the [`ci-templates` `docker-test.yml` template](https://gitlab.com/gitlab-org/security-products/ci-templates/-/blob/master/includes-dev/docker-test.yml) include a [`check docker manifest`](https://gitlab.com/gitlab-org/security-products/ci-templates/-/blob/c0f217560b134f4ebe6024b26a41f77cea885c2c/includes-dev/docker-test.yml#L157-165) test in their pipelines, to prevent proprietary Docker features from being merged in the first place. +To avoid compatibility issues when building analyzer Docker images, use the [OCI media types](https://docs.docker.com/build/exporters/#oci-media-types) instead of the default proprietary Docker media types. -When creating a new analyzer, or changing the location of existing analyzer images, ensure that the analyzer is accounted for in the periodic test and consider using the shared [`ci-templates`](https://gitlab.com/gitlab-org/security-products/ci-templates/). +In addition to the periodic test, we ensure compatibility for users of the [`ci-templates` repo](https://gitlab.com/gitlab-org/security-products/ci-templates): + +1. Analyzers using the [`ci-templates` `docker-test.yml` template](https://gitlab.com/gitlab-org/security-products/ci-templates/-/blob/master/includes-dev/docker-test.yml) +include [`tests`](https://gitlab.com/gitlab-org/security-products/ci-templates/-/blob/08319f7586fd9cc66f58ca894525ab54a2b7d831/includes-dev/docker-test.yml#L155-179) to ensure our Docker images function correctly with supported Docker tools. + + These tests are executed in Merge Request pipelines and scheduled pipelines, and prevent images from being released if they break the supported Docker tools. +1. The [`ci-templates` `docker.yml` template](https://gitlab.com/gitlab-org/security-products/ci-templates/-/blob/master/includes-dev/docker.yml) +specifies [`oci-mediatypes=true`](https://docs.docker.com/build/exporters/#oci-media-types) for the `docker buildx` command when building analyzer images. +This builds images using [OCI](https://opencontainers.org/) media types rather than Docker proprietary media types. + +When creating a new analyzer, or changing the location of existing analyzer images, +add it to the periodic test, or consider using the shared [`ci-templates`](https://gitlab.com/gitlab-org/security-products/ci-templates/) which includes an automated test. ## Analyzer scripts diff --git a/doc/development/work_items_widgets.md b/doc/development/work_items_widgets.md index eae11aff246..66705151100 100644 --- a/doc/development/work_items_widgets.md +++ b/doc/development/work_items_widgets.md @@ -121,7 +121,7 @@ We also have a [reusable base dropdown widget wrapper](https://gitlab.com/gitlab 1. Depending on the input field i.e a dropdown, input text or any other custom design we should make sure that we use an [existing wrapper](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget.vue) or completely new component 1. Ideally any new widget should be behind an FF to make sure we have room for testing unless there is a priority for the widget. 1. Create the new widget in the [folder](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/assets/javascripts/work_items/components) -1. If it is an editable widget in the sidebar , you should include it in [work_item_attributes_wrapper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue) +1. If it is an editable widget in the sidebar, you should include it in [work_item_attributes_wrapper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue) ### Steps @@ -174,9 +174,9 @@ To resolve this, update the mocked `Gitlab::QueryLimiting::Transaction.threshold Since create view is almost identical to detail view, and we wanted to store in the draft data of each widget, each new work item for a specific type has a new cache entry apollo. -For example , when we initialise the create view , we have a function `setNewWorkItemCache` [in work items cache utils](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/graphql/cache_utils) which is called in both [create view work item modal](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/components/create_work_item_modal.vue) and also [create work item component](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/components/create_work_item.vue) +For example, when we initialise the create view, we have a function `setNewWorkItemCache` [in work items cache utils](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/graphql/cache_utils) which is called in both [create view work item modal](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/components/create_work_item_modal.vue) and also [create work item component](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/components/create_work_item.vue) -You can include the create work item view in any vue file depending on usage. If you pass the `workItemType` of the create view , it will only include the applicable work item widgets which are fetched from [work item types query](../api/graphql/reference/_index.md#workitemtype) and only showing the ones in [widget definitions](../api/graphql/reference/_index.md#workitemwidgetdefinition) +You can include the create work item view in any vue file depending on usage. If you pass the `workItemType` of the create view, it will only include the applicable work item widgets which are fetched from [work item types query](../api/graphql/reference/_index.md#workitemtype) and only showing the ones in [widget definitions](../api/graphql/reference/_index.md#workitemwidgetdefinition) We have a [local mutation](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/graphql/update_new_work_item.mutation.graphql) to update the work item draft data in create view @@ -202,7 +202,7 @@ if (this.workItemId === newWorkItemId(this.workItemType)) { ### Support new work item widget in local mutation -1. Add the input type in [work item local mutation typedefs](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/graphql/typedefs.graphql#L55). It can be anything , a custom object or a primitive value. +1. Add the input type in [work item local mutation typedefs](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/work_items/graphql/typedefs.graphql#L55). It can be anything, a custom object or a primitive value. Example if you want add `parent` which has the name and ID of the parent of the work item diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md index 2f50357a08f..f7d9502255f 100644 --- a/doc/integration/openid_connect_provider.md +++ b/doc/integration/openid_connect_provider.md @@ -31,7 +31,7 @@ the [doorkeeper-openid_connect repository](https://github.com/doorkeeper-gem/doo ## Enable OIDC for OAuth applications To enable OIDC for an OAuth application, you need to select the `openid` scope in the application -settings. For more information , see [Configure GitLab as an OAuth 2.0 authentication identity provider](oauth_provider.md). +settings. For more information, see [Configure GitLab as an OAuth 2.0 authentication identity provider](oauth_provider.md). ## Settings discovery diff --git a/doc/security/hardening_nist_800_53.md b/doc/security/hardening_nist_800_53.md index 9e6418ff7ca..f395b570f01 100644 --- a/doc/security/hardening_nist_800_53.md +++ b/doc/security/hardening_nist_800_53.md @@ -232,7 +232,7 @@ as WebAuthn, must be used (IA-2). - GitLab [provides instructions](../user/ssh.md) on how to configure SSH keys to authenticate and communicate with Git. [Commits can be signed](../user/project/repository/signed_commits/ssh.md), providing additional verification for anyone with a public key. -- Keys should be configured to meet applicable strength and complexity requirements, such as using FIPS 140-2 and FIPS 140-3 validated ciphers . Administrators can [restrict minimum key technologies and key lengths](ssh_keys_restrictions.md). Additionally, administrators can [block or ban compromised keys](ssh_keys_restrictions.md#block-banned-or-compromised-keys). +- Keys should be configured to meet applicable strength and complexity requirements, such as using FIPS 140-2 and FIPS 140-3 validated ciphers. Administrators can [restrict minimum key technologies and key lengths](ssh_keys_restrictions.md). Additionally, administrators can [block or ban compromised keys](ssh_keys_restrictions.md#block-banned-or-compromised-keys). **Personal access tokens** diff --git a/doc/security/responding_to_security_incidents.md b/doc/security/responding_to_security_incidents.md index e1e3353ab16..5888b3960dc 100644 --- a/doc/security/responding_to_security_incidents.md +++ b/doc/security/responding_to_security_incidents.md @@ -202,4 +202,4 @@ You can also consider implementing abuse rate limiting as detailed in [Git abuse GitLab SIRT maintains an active repository of detections in the [GitLab SIRT public project](https://gitlab.com/gitlab-security-oss/guard/-/tree/main/detections). -The detections in this repository are based on the audit events and in the general Sigma rule format. You can use sigma rule converter to get the rules in your desired format. Please refer to the repository for more information about Sigma format and tools related to it . Make sure you have GitLab audit logs ingested to your SIEM. You should follow the audit event streaming guide [for your self-managed instance](../administration/audit_event_streaming/_index.md) or [GitLab.com top-level group](../user/compliance/audit_event_streaming.md) to stream audit events to your desired destination. +The detections in this repository are based on the audit events and in the general Sigma rule format. You can use sigma rule converter to get the rules in your desired format. Please refer to the repository for more information about Sigma format and tools related to it. Make sure you have GitLab audit logs ingested to your SIEM. You should follow the audit event streaming guide [for your self-managed instance](../administration/audit_event_streaming/_index.md) or [GitLab.com top-level group](../user/compliance/audit_event_streaming.md) to stream audit events to your desired destination. diff --git a/doc/user/application_security/api_fuzzing/configuration/customizing_analyzer_settings.md b/doc/user/application_security/api_fuzzing/configuration/customizing_analyzer_settings.md index 32ab4795bd0..2356928c6e9 100644 --- a/doc/user/application_security/api_fuzzing/configuration/customizing_analyzer_settings.md +++ b/doc/user/application_security/api_fuzzing/configuration/customizing_analyzer_settings.md @@ -624,7 +624,7 @@ In the previous sample, you could use the script `user-pre-scan-set-up.sh` to al ## Exclude Paths -When testing an API it can be useful to exclude certain paths. For example, you might exclude testing of an authentication service or an older version of the API. To exclude paths, use the `FUZZAPI_EXCLUDE_PATHS` CI/CD variable . This variable is specified in your `.gitlab-ci.yml` file. To exclude multiple paths, separate entries using the `;` character. In the provided paths you can use a single character wildcard `?` and `*` for a multiple character wildcard. +When testing an API it can be useful to exclude certain paths. For example, you might exclude testing of an authentication service or an older version of the API. To exclude paths, use the `FUZZAPI_EXCLUDE_PATHS` CI/CD variable. This variable is specified in your `.gitlab-ci.yml` file. To exclude multiple paths, separate entries using the `;` character. In the provided paths you can use a single character wildcard `?` and `*` for a multiple character wildcard. To verify the paths are excluded, review the `Tested Operations` and `Excluded Operations` portion of the job output. You should not see any excluded paths listed under `Tested Operations`. diff --git a/doc/user/application_security/api_security_testing/configuration/customizing_analyzer_settings.md b/doc/user/application_security/api_security_testing/configuration/customizing_analyzer_settings.md index b764d05e0bc..1396cebbd06 100644 --- a/doc/user/application_security/api_security_testing/configuration/customizing_analyzer_settings.md +++ b/doc/user/application_security/api_security_testing/configuration/customizing_analyzer_settings.md @@ -654,7 +654,7 @@ Consider using `APISEC_REQUEST_HEADERS_BASE64` when storing secret header values ## Exclude Paths -When testing an API it can be useful to exclude certain paths. For example, you might exclude testing of an authentication service or an older version of the API. To exclude paths, use the `APISEC_EXCLUDE_PATHS` CI/CD variable . This variable is specified in your `.gitlab-ci.yml` file. To exclude multiple paths, separate entries using the `;` character. In the provided paths you can use a single character wildcard `?` and `*` for a multiple character wildcard. +When testing an API it can be useful to exclude certain paths. For example, you might exclude testing of an authentication service or an older version of the API. To exclude paths, use the `APISEC_EXCLUDE_PATHS` CI/CD variable. This variable is specified in your `.gitlab-ci.yml` file. To exclude multiple paths, separate entries using the `;` character. In the provided paths you can use a single character wildcard `?` and `*` for a multiple character wildcard. To verify the paths are excluded, review the `Tested Operations` and `Excluded Operations` portion of the job output. You should not see any excluded paths listed under `Tested Operations`. diff --git a/doc/user/application_security/policies/scan_execution_policies.md b/doc/user/application_security/policies/scan_execution_policies.md index 8a95ffc3a39..4241187a51e 100644 --- a/doc/user/application_security/policies/scan_execution_policies.md +++ b/doc/user/application_security/policies/scan_execution_policies.md @@ -396,7 +396,7 @@ scan. mode with `SECRET_DETECTION_LOG_OPTIONS` set to the commit range between last run and current SHA. You can override this behavior by specifying CI/CD variables in the scan execution policy. For more information, see - [Full history pipeline secret detection](../secret_detection/pipeline/_index.md#historic-scan). + [Full history pipeline secret detection](../secret_detection/pipeline/_index.md#run-a-historic-scan). - For `triggered` scan execution policies, secret detection works just like regular scan [configured manually in the `.gitlab-ci.yml`](../secret_detection/pipeline/_index.md#edit-the-gitlab-ciyml-file-manually). - Container scanning: A scan that is configured for the `pipeline` rule type ignores the agent diff --git a/doc/user/application_security/secret_detection/pipeline/_index.md b/doc/user/application_security/secret_detection/pipeline/_index.md index 4cc5e1b77e9..b9f1f50bb8c 100644 --- a/doc/user/application_security/secret_detection/pipeline/_index.md +++ b/doc/user/application_security/secret_detection/pipeline/_index.md @@ -72,17 +72,28 @@ By default, when you run a pipeline: To override the default behavior, use the [available CI/CD variables](configure.md#available-cicd-variables). -### Historic scan +### Run a historic scan By default, pipeline secret detection scans only the current state of the Git repository. Any secrets contained in the repository's history are not detected. Run a historic scan to check for secrets from all commits and branches in the Git repository. -You should do a historic scan only once, after enabling pipeline secret detection. Historic scans +You should run a historic scan only once, after enabling pipeline secret detection. Historic scans can take a long time, especially for larger repositories with lengthy Git histories. After completing an initial historic scan, use only standard pipeline secret detection as part of your pipeline. +To run a historic scan: + +1. On the left sidebar, select **Search or go to** and find your project. +1. Select **Build > Pipelines**. +1. Select **New pipeline**. +1. Add a CI/CD variable: + 1. From the dropdown list, select **Variable**. + 1. In the **Input variable key** box, enter `SECRET_DETECTION_HISTORIC_SCAN`. + 1. In the **Input variable value** box, enter `true`. +1. Select **New pipeline**. + ### Advanced vulnerability tracking {{< details >}} diff --git a/doc/user/compliance/audit_event_streaming.md b/doc/user/compliance/audit_event_streaming.md index 205c17330a8..908c4696109 100644 --- a/doc/user/compliance/audit_event_streaming.md +++ b/doc/user/compliance/audit_event_streaming.md @@ -221,7 +221,7 @@ To update a streaming destination's namespace filters: ### Override default content type header By default, streaming destinations use a `content-type` header of `application/x-www-form-urlencoded`. However, you -might want to set the `content-type` header to something else. For example ,`application/json`. +might want to set the `content-type` header to something else. For example, `application/json`. To override the `content-type` header default value for a top-level group streaming destination, use either: diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md index fd90ca555e1..b6df1442abb 100644 --- a/doc/user/group/epics/manage_epics.md +++ b/doc/user/group/epics/manage_epics.md @@ -462,6 +462,7 @@ than 1000. The cached value is rounded to thousands or millions and updated ever {{< history >}} - Filtering by group was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385191) in GitLab 15.9. +- Filtering by custom fields was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/525462) in GitLab 17.11. {{< /history >}} @@ -474,6 +475,7 @@ You can filter the list of epics by: - Confidentiality - Reaction emoji - Groups +- [Custom fields](../../../../../gitlab/doc/user/work_items/custom_fields.md) enabled for epics ![epics filter](img/epics_filter_v14_7.png) diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 027a90a4999..ce7b8d82625 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -97,7 +97,7 @@ You can add or remove data from export files. For example, you can: To edit a project export file: 1. Extract the exported `.tar.gz` file. -1. Edit the appropriate file . For example, `tree/project/project_members.ndjson`. +1. Edit the appropriate file. For example, `tree/project/project_members.ndjson`. 1. Compress the files back to a `.tar.gz` file. You can also make sure that all members were exported by checking the `project_members.ndjson` file. diff --git a/doc/user/work_items/custom_fields.md b/doc/user/work_items/custom_fields.md index 909fad3e0ae..ce2b76ed02c 100644 --- a/doc/user/work_items/custom_fields.md +++ b/doc/user/work_items/custom_fields.md @@ -34,9 +34,7 @@ Configure custom fields for a group to track data points like business value, ri These fields appear in all work items across the group, its subgroups, and projects. Custom fields help teams standardize how they record and report information across the entire workflow. -This standardization creates consistency across projects. - - +This standardization creates consistency across projects and supports more powerful filtering and reporting capabilities. Choose from various field types to accommodate different data requirements and planning scenarios: - Single-select diff --git a/lib/api/entities/wiki_page/meta.rb b/lib/api/entities/wiki_page/meta.rb new file mode 100644 index 00000000000..6f82eca3350 --- /dev/null +++ b/lib/api/entities/wiki_page/meta.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class WikiPage + class Meta < Grape::Entity + expose :id, documentation: { type: 'integer', example: 2 } + expose :canonical_slug, as: :slug, documentation: { type: 'string', example: 'home' } + expose :title, documentation: { type: 'string', example: 'Page title' } + end + end + end +end diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb index 6377910a258..8b61a49a472 100644 --- a/spec/fast_spec_helper.rb +++ b/spec/fast_spec_helper.rb @@ -43,6 +43,7 @@ module FastSpecHelper # @return [void] def self.spec_requires_and_configuration require 'gitlab/rspec/next_instance_of' + require 'hashdiff' require_relative 'support/patches/rspec_mocks_doubles_fast_spec_helper_patch' require_relative 'support/matchers/result_matchers' require_relative 'support/railway_oriented_programming' diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 11bde0ac982..cc22cc95206 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -735,22 +735,22 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do describe('#group_confirm_modal_data') do using RSpec::Parameterized::TableSyntax - let_it_be(:group) { create(:group, path: "foo") } fake_form_id = "fake_form_id" - where(:prevent_delete_response, :is_button_disabled, :form_value_id, :permanently_remove, :button_text, :has_security_policy_project) do - true | "true" | nil | false | "Delete" | true - true | "true" | fake_form_id | true | nil | false - false | "true" | nil | false | "Delete group" | true - false | "false" | fake_form_id | true | nil | false + where(:prevent_delete_response, :adjourned_deletion, :is_button_disabled, :form_value_id, :permanently_remove, :button_text, :has_security_policy_project) do + true | false | "true" | nil | false | "Delete" | true + true | true | "true" | fake_form_id | true | nil | false + false | false | "true" | nil | false | "Delete group" | true + false | true | "false" | fake_form_id | true | nil | false end with_them do it "returns expected parameters" do allow(group).to receive(:linked_to_subscription?).and_return(prevent_delete_response) + allow(group).to receive(:adjourned_deletion?).and_return(adjourned_deletion) - expected = helper.group_confirm_modal_data(group: group, remove_form_id: form_value_id, button_text: button_text, has_security_policy_project: has_security_policy_project) + expected = helper.group_confirm_modal_data(group: group, remove_form_id: form_value_id, button_text: button_text, has_security_policy_project: has_security_policy_project, permanently_remove: permanently_remove) expect(expected).to eq({ button_text: button_text.nil? ? "Delete group" : button_text, confirm_danger_message: remove_group_message(group, permanently_remove), @@ -764,6 +764,81 @@ RSpec.describe GroupsHelper, feature_category: :groups_and_projects do end end + describe '#remove_group_message' do + let_it_be(:group) { create(:group) } + let(:delayed_deletion_message) { "The contents of this group, its subgroups and projects will be permanently deleted after" } + let(:permanent_deletion_message) { ["You are about to delete the group #{group.name}", "After you delete a group, you cannot restore it or its components."] } + + subject { helper.remove_group_message(group, false) } + + shared_examples 'permanent deletion message' do + it 'returns the message related to permanent deletion' do + expect(subject).to include(*permanent_deletion_message) + end + end + + shared_examples 'delayed deletion message' do + it 'returns the message related to delayed deletion' do + expect(subject).to include(delayed_deletion_message) + end + end + + context 'delayed deletion feature is available' do + before do + allow(group).to receive(:adjourned_deletion?).and_return(true) + end + + it_behaves_like 'delayed deletion message' + + context 'group is already marked for deletion' do + before do + create(:group_deletion_schedule, group: group, marked_for_deletion_on: Date.current) + allow(group).to receive(:marked_for_deletion?).and_return(true) + end + + it_behaves_like 'permanent deletion message' + end + + context 'when group delay deletion is enabled' do + before do + stub_application_setting(delayed_group_deletion: true) + end + + it_behaves_like 'delayed deletion message' + end + + context 'when group delay deletion is disabled' do + before do + stub_application_setting(delayed_group_deletion: false) + end + + it_behaves_like 'delayed deletion message' + end + + context "group has not been marked for deletion" do + let(:group) { build(:group) } + + context "'permanently_remove' argument is set to 'true'" do + it "displays permanent deletion message" do + allow(group).to receive(:marked_for_deletion?).and_return(false) + allow(group).to receive(:adjourned_deletion?).and_return(true) + + expect(subject).to include(delayed_deletion_message) + expect(helper.remove_group_message(group, true)).to include(*permanent_deletion_message) + end + end + end + end + + context 'delayed deletion feature is not available' do + before do + stub_feature_flags(downtier_delayed_deletion: false) + end + + it_behaves_like 'permanent deletion message' + end + end + describe '#group_merge_requests' do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } diff --git a/spec/mailers/emails/groups_spec.rb b/spec/mailers/emails/groups_spec.rb index 01846172440..fdd82077579 100644 --- a/spec/mailers/emails/groups_spec.rb +++ b/spec/mailers/emails/groups_spec.rb @@ -6,8 +6,10 @@ require 'email_spec' RSpec.describe Emails::Groups do include EmailSpec::Matchers + # rubocop:disable RSpec/FactoryBot/AvoidCreate -- Need associations let(:group) { create(:group) } let(:user) { create(:user) } + # rubocop:enable RSpec/FactoryBot/AvoidCreate before do group.add_owner(user) @@ -38,4 +40,32 @@ RSpec.describe Emails::Groups do expect(subject).to have_body_text "Group #{group.name} couldn't be exported." end end + + describe '#group_scheduled_for_deletion' do + # rubocop:disable RSpec/FactoryBot/AvoidCreate -- Need associations + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group_with_deletion_schedule, owners: user) } + let_it_be(:sub_group) { create(:group_with_deletion_schedule, parent: group) } + # rubocop:enable RSpec/FactoryBot/AvoidCreate + + let_it_be(:deletion_adjourned_period) { 7 } + let_it_be(:deletion_date) { (Time.current + deletion_adjourned_period.days).strftime('%B %-d, %Y') } + let_it_be(:group_retain_url) { "http://localhost/groups/#{sub_group.full_path}/-/edit#js-advanced-settings" } + + before do + stub_application_setting(deletion_adjourned_period: deletion_adjourned_period) + end + + subject { Notify.group_scheduled_for_deletion(user.id, sub_group.id) } + + it 'has the expected content', :aggregate_failures, :freeze_time do + is_expected.to have_subject("#{sub_group.name} | Group scheduled for deletion") + + is_expected.to have_body_text( + "has been marked for deletion and will be removed in #{deletion_adjourned_period} days." + ) + is_expected.to have_body_text(deletion_date) + is_expected.to have_body_text("href=\"#{group_retain_url}\"") + end + end end diff --git a/spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb index 4a766b04778..001782d7b01 100644 --- a/spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb @@ -43,20 +43,6 @@ RSpec.describe 'Bulk update work items', feature_category: :team_planning do end end - context 'when the `bulk_update_work_items_mutation` feature flag is disabled' do - before do - stub_feature_flags(bulk_update_work_items_mutation: false) - end - - it 'returns a resource not available error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect_graphql_errors_to_include( - '`bulk_update_work_items_mutation` feature flag is disabled.' - ) - end - end - context 'when user can not update all work_items' do let_it_be(:forbidden_work_item) { create(:work_item, project: private_project) } let(:updatable_work_item_ids) { updatable_work_items.map { |i| i.to_gid.to_s } + [forbidden_work_item.to_gid.to_s] } diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 3bc1feaced9..e4439557ef7 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -20,6 +20,8 @@ RSpec.describe API::Todos, feature_category: :source_code_management do let_it_be(:group_request_todo) { create(:todo, author: author_1, user: john_doe, project: nil, group: group_2, target: group_2, action: Todo::MEMBER_ACCESS_REQUESTED) } let_it_be(:alert_todo) { create(:todo, project: project_1, author: john_doe, user: john_doe, target: alert) } let_it_be(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) } + let_it_be(:wiki_page_meta) { create(:wiki_page_meta, :for_wiki_page, container: project_1) } + let_it_be(:wiki_page_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: wiki_page_meta, action: Todo::MENTIONED) } let_it_be(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe, target: issue) } let_it_be(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe, target: create(:issue, project: project_2)) } let_it_be(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) } @@ -69,7 +71,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(8) + expect(json_response.length).to eq(9) expect(json_response[0]).to include( 'id' => pending_5.id, @@ -108,9 +110,19 @@ RSpec.describe API::Todos, feature_category: :source_code_management do ) ) - # Only issues get a merge request count at the moment - expect(json_response[4].dig('target', 'merge_requests_count')).to be_nil expect(json_response[4]).to include( + 'target_type' => 'WikiPage::Meta', + 'action_name' => 'mentioned', + 'target' => hash_including( + 'id' => wiki_page_meta.id, + 'title' => wiki_page_meta.title, + 'slug' => wiki_page_meta.canonical_slug + ) + ) + + # Only issues get a merge request count at the moment + expect(json_response[5].dig('target', 'merge_requests_count')).to be_nil + expect(json_response[5]).to include( 'target_type' => 'MergeRequest', 'target' => hash_including( 'upvotes' => 1, @@ -118,7 +130,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do ) ) - expect(json_response[5]).to include( + expect(json_response[6]).to include( 'target_type' => 'AlertManagement::Alert', 'target' => hash_including( 'iid' => alert.iid, @@ -126,7 +138,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do ) ) - expect(json_response[6]).to include( + expect(json_response[7]).to include( 'target_type' => 'Namespace', 'action_name' => 'member_access_requested', 'target' => hash_including( @@ -137,7 +149,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do 'target_url' => Gitlab::Routing.url_helpers.group_group_members_url(group_2, tab: 'access_requests') ) - expect(json_response[7]).to include( + expect(json_response[8]).to include( 'target_type' => 'Project', 'action_name' => 'member_access_requested', 'target' => hash_including( @@ -158,7 +170,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do get api('/todos', john_doe) - expect(json_response.count).to eq(8) + expect(json_response.count).to eq(9) expect(json_response.map { |t| t['id'] }).not_to include(no_access_todo.id, pending_4.id) end end @@ -170,7 +182,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) end end @@ -216,7 +228,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(5) + expect(json_response.length).to eq(6) end end @@ -227,7 +239,7 @@ RSpec.describe API::Todos, feature_category: :source_code_management do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(2) + expect(json_response.length).to eq(3) end end end diff --git a/spec/services/groups/mark_for_deletion_service_spec.rb b/spec/services/groups/mark_for_deletion_service_spec.rb index 04ac4289438..92e3d62568d 100644 --- a/spec/services/groups/mark_for_deletion_service_spec.rb +++ b/spec/services/groups/mark_for_deletion_service_spec.rb @@ -6,13 +6,12 @@ RSpec.describe Groups::MarkForDeletionService, feature_category: :groups_and_pro let_it_be(:user) { create(:user) } let(:licensed) { false } let(:service) { described_class.new(group, user, {}) } + let_it_be_with_reload(:group) { create(:group, owners: user) } subject(:result) { service.execute(licensed: licensed) } context 'when marking the group for deletion' do context 'with user that can admin the group' do - let_it_be_with_reload(:group) { create(:group, owners: user) } - context 'for a group that has not been marked for deletion' do it 'marks the group for deletion', :freeze_time do result @@ -25,12 +24,6 @@ RSpec.describe Groups::MarkForDeletionService, feature_category: :groups_and_pro expect(result).to eq({ status: :success }) end - it 'calls send_group_deletion_notification when successful' do - expect(service).to receive(:send_group_deletion_notification) - - result - end - it 'logs the event' do allow(Gitlab::AppLogger).to receive(:info).and_call_original expect(Gitlab::AppLogger).to receive(:info).with( @@ -40,6 +33,62 @@ RSpec.describe Groups::MarkForDeletionService, feature_category: :groups_and_pro result end + context 'when notification feature flag is enabled and adjourned deletion is enabled' do + before do + stub_feature_flags(group_deletion_notification_email: true) + allow(group).to receive(:adjourned_deletion?).and_return(true) + end + + it 'sends a notification email' do + expect_next_instance_of(NotificationService) do |service| + expect(service).to receive(:group_scheduled_for_deletion).with(group) + end + + result + end + end + + context 'when notification feature flag is disabled' do + before do + stub_feature_flags(group_deletion_notification_email: false) + allow(group).to receive(:adjourned_deletion?).and_return(true) + end + + it 'does not send a notification email' do + expect(NotificationService).not_to receive(:new) + + result + end + end + + context 'when notification feature flag is enabled for specific group' do + before do + stub_feature_flags(group_deletion_notification_email: group) + allow(group).to receive(:adjourned_deletion?).and_return(true) + end + + it 'sends a notification email' do + expect_next_instance_of(NotificationService) do |service| + expect(service).to receive(:group_scheduled_for_deletion).with(group) + end + + result + end + end + + context 'when adjourned deletion is disabled' do + before do + stub_feature_flags(group_deletion_notification_email: true) + allow(group).to receive(:adjourned_deletion?).and_return(false) + end + + it 'does not send a notification email' do + expect(NotificationService).not_to receive(:new) + + result + end + end + context 'when marking for deletion fails' do before do expect_next_instance_of(GroupDeletionSchedule) do |group_deletion_schedule| @@ -54,8 +103,8 @@ RSpec.describe Groups::MarkForDeletionService, feature_category: :groups_and_pro expect(result).to eq({ status: :error, message: 'error message' }) end - it 'does not call send_group_deletion_notification' do - expect(service).not_to receive(:send_group_deletion_notification) + it 'does not send notification' do + expect(NotificationService).not_to receive(:new) result end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4c3f52bf25c..b741f6d29e3 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -4621,6 +4621,64 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end + describe 'group scheduled for deletion' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + + context 'when group emails are disabled' do + before do + allow(group).to receive(:emails_disabled?).and_return(true) + end + + it 'does not send any emails' do + expect(Notify).not_to receive(:group_scheduled_for_deletion) + + subject.group_scheduled_for_deletion(group) + end + end + + context 'when group emails are enabled' do + before do + allow(group).to receive(:emails_disabled?).and_return(false) + end + + context 'when user is owner' do + it 'sends email' do + group.add_owner(user) + + expect(Notify).to receive(:group_scheduled_for_deletion).with(user.id, group.id).and_call_original + + subject.group_scheduled_for_deletion(group) + end + + context 'when owner is blocked' do + it 'does not send email' do + group.add_owner(user) + user.block! + + expect(Notify).not_to receive(:group_scheduled_for_deletion) + + subject.group_scheduled_for_deletion(group) + end + end + end + + context 'when group has multiple owners' do + let_it_be(:another_user) { create(:user) } + + it 'sends email to all owners' do + group.add_owner(user) + group.add_owner(another_user) + + expect(Notify).to receive(:group_scheduled_for_deletion).with(user.id, group.id).and_call_original + expect(Notify).to receive(:group_scheduled_for_deletion).with(another_user.id, group.id).and_call_original + + subject.group_scheduled_for_deletion(group) + end + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/mark_for_deletion_service_spec.rb b/spec/services/projects/mark_for_deletion_service_spec.rb index 82b733a8ff9..582fef253f7 100644 --- a/spec/services/projects/mark_for_deletion_service_spec.rb +++ b/spec/services/projects/mark_for_deletion_service_spec.rb @@ -75,8 +75,6 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p end it 'does not send notification email' do - stub_feature_flags(project_deletion_notification_email: true) - expect(NotificationService).not_to receive(:new) result @@ -112,35 +110,6 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p describe '#send_project_deletion_notification' do context 'when all conditions are met' do before do - stub_feature_flags(project_deletion_notification_email: true) - allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true) - end - - it 'sends a notification email' do - expect_next_instance_of(NotificationService) do |service| - expect(service).to receive(:project_scheduled_for_deletion).with(project) - end - - execute_send_project_deletion_notification - end - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(project_deletion_notification_email: false) - allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true) - end - - it 'does not send a notification email' do - expect(NotificationService).not_to receive(:new) - - execute_send_project_deletion_notification - end - end - - context 'when feature flag is enabled for specific project' do - before do - stub_feature_flags(project_deletion_notification_email: project) allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: true) end @@ -155,7 +124,6 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p context 'when adjourned deletion is disabled' do before do - stub_feature_flags(project_deletion_notification_email: true) allow(project).to receive_messages(adjourned_deletion?: false, marked_for_deletion?: true) end @@ -168,7 +136,6 @@ RSpec.describe Projects::MarkForDeletionService, feature_category: :groups_and_p context 'when project is not marked for deletion' do before do - stub_feature_flags(project_deletion_notification_email: true) allow(project).to receive_messages(adjourned_deletion?: true, marked_for_deletion?: false) end