Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 12:09:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 12:09:20 +0300
commit3d8459c18b7a20d9142359bb9334b467e774eb36 (patch)
tree109d881c19a27cdae131bf5e82f1216401d9df2b
parente0a415ccb7a7e59c7a6c16841bdd1668d2ef0be5 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile.lock10
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue136
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue18
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js10
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/graphql/types/commit_type.rb9
-rw-r--r--app/models/merge_request/metrics.rb3
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml6
-rw-r--r--db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb9
-rw-r--r--db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb11
-rw-r--r--db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb13
-rw-r--r--db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb16
-rw-r--r--db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb15
-rw-r--r--db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb19
-rw-r--r--db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb19
-rw-r--r--db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb29
-rw-r--r--db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb19
-rw-r--r--db/schema_migrations/202307140844151
-rw-r--r--db/schema_migrations/202307140959461
-rw-r--r--db/schema_migrations/202307181456131
-rw-r--r--db/schema_migrations/202307190832021
-rw-r--r--db/schema_migrations/202307261040221
-rw-r--r--db/schema_migrations/202307261045471
-rw-r--r--db/schema_migrations/202307261046161
-rw-r--r--db/schema_migrations/202307310903191
-rw-r--r--db/schema_migrations/202307311303511
-rw-r--r--db/structure.sql16
-rw-r--r--doc/administration/external_users.md2
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/user/permissions.md2
-rw-r--r--lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb95
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/support/matchers/have_matcher.rb1
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_spec.js94
-rw-r--r--spec/frontend/projects/settings_service_desk/components/mock_data.js9
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js47
-rw-r--r--spec/graphql/types/commit_type_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb113
-rw-r--r--spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb44
-rw-r--r--spec/support/helpers/migrations_helpers/project_statistics_helper.rb37
-rw-r--r--spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb106
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb1
45 files changed, 924 insertions, 17 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 334b819fa1f..a274df8450d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,15 +13,15 @@ PATH
json (~> 2.6.3)
PATH
- remote: gems/error_tracking_open_api
+ remote: gems/csv_builder
specs:
- error_tracking_open_api (1.0.0)
- typhoeus (~> 1.0, >= 1.0.1)
+ csv_builder (0.1.0)
PATH
- remote: gems/csv_builder
+ remote: gems/error_tracking_open_api
specs:
- csv_builder (0.1.0)
+ error_tracking_open_api (1.0.0)
+ typhoeus (~> 1.0, >= 1.0.1)
PATH
remote: gems/gitlab-rspec
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
new file mode 100644
index 00000000000..f90633c6e03
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlSprintf, GlLink, GlCard } from '@gitlab/ui';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
+import axios from '~/lib/utils/axios_utils';
+import {
+ FEEDBACK_ISSUE_URL,
+ I18N_LOADING_LABEL,
+ I18N_CARD_TITLE,
+ I18N_GENERIC_ERROR,
+ I18N_FEEDBACK_PARAGRAPH,
+} from '../custom_email_constants';
+
+export default {
+ components: {
+ BetaBadge,
+ GlAlert,
+ GlLoadingIcon,
+ GlSprintf,
+ GlLink,
+ GlCard,
+ },
+ FEEDBACK_ISSUE_URL,
+ I18N_LOADING_LABEL,
+ I18N_CARD_TITLE,
+ I18N_FEEDBACK_PARAGRAPH,
+ props: {
+ incomingEmail: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ customEmailEndpoint: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ customEmail: null,
+ enabled: false,
+ verificationState: null,
+ verificationError: null,
+ smtpAddress: null,
+ errorMessage: null,
+ alertMessage: null,
+ };
+ },
+ mounted() {
+ this.getCustomEmailDetails();
+ },
+ methods: {
+ dismissAlert() {
+ this.alertMessage = null;
+ },
+ getCustomEmailDetails() {
+ axios
+ .get(this.customEmailEndpoint)
+ .then(({ data }) => {
+ this.updateData(data);
+ })
+ .catch(this.handleRequestError)
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ handleRequestError() {
+ this.alertMessage = I18N_GENERIC_ERROR;
+ },
+ updateData(data) {
+ this.customEmail = data.custom_email;
+ this.enabled = data.custom_email_enabled;
+ this.verificationState = data.custom_email_verification_state;
+ this.verificationError = data.custom_email_verification_error;
+ this.smtpAddress = data.custom_email_smtp_address;
+ this.errorMessage = data.error_message;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row gl-mt-7">
+ <div class="col-md-9">
+ <gl-card>
+ <template #header>
+ <div class="gl-display-flex align-items-center justify-content-between">
+ <h5 class="gl-my-0">{{ $options.I18N_CARD_TITLE }}</h5>
+ <beta-badge />
+ </div>
+ </template>
+
+ <template #default>
+ <template v-if="loading">
+ <div class="gl-p-3 gl-text-center">
+ <gl-loading-icon
+ :label="$options.I18N_LOADING_LABEL"
+ size="md"
+ color="dark"
+ variant="spinner"
+ :inline="false"
+ />
+ {{ $options.I18N_LOADING_LABEL }}
+ </div>
+ </template>
+
+ <gl-alert
+ v-if="alertMessage"
+ variant="warning"
+ class="gl-mt-n5 gl-mx-n5"
+ @dismiss="dismissAlert"
+ >
+ {{ alertMessage }}
+ </gl-alert>
+ </template>
+
+ <template #footer>
+ <span>
+ <gl-sprintf :message="$options.I18N_FEEDBACK_PARAGRAPH">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.FEEDBACK_ISSUE_URL"
+ data-testid="feedback-link"
+ target="_blank"
+ class="gl-text-blue-600 font-size-inherit"
+ >{{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-card>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index ae28694f5d2..3af4ea8af65 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -4,8 +4,11 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ServiceDeskSetting from './service_desk_setting.vue';
+const CustomEmail = () => import('./custom_email.vue');
+
export default {
serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk.html', {
anchor: 'use-an-additional-service-desk-alias-email',
@@ -15,10 +18,12 @@ export default {
GlSprintf,
GlLink,
ServiceDeskSetting,
+ CustomEmail,
},
directives: {
SafeHtml,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
initialIsEnabled: {
default: false,
@@ -56,6 +61,9 @@ export default {
publicProject: {
default: false,
},
+ customEmailEndpoint: {
+ default: '',
+ },
},
data() {
return {
@@ -68,6 +76,11 @@ export default {
updatedServiceDeskEmail: this.serviceDeskEmail,
};
},
+ computed: {
+ showCustomEmail() {
+ return this.glFeatures.serviceDeskCustomEmail && this.isEnabled && this.isIssueTrackerEnabled;
+ },
+ },
methods: {
onEnableToggled(isChecked) {
this.isEnabled = isChecked;
@@ -179,5 +192,10 @@ export default {
@save="onSaveTemplate"
@toggle="onEnableToggled"
/>
+ <custom-email
+ v-if="showCustomEmail"
+ :incoming-email="incomingEmail"
+ :custom-email-endpoint="customEmailEndpoint"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
new file mode 100644
index 00000000000..9770a1f4df9
--- /dev/null
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -0,0 +1,10 @@
+import { s__, __ } from '~/locale';
+
+export const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/416637';
+
+export const I18N_LOADING_LABEL = __('Loading');
+export const I18N_CARD_TITLE = s__('ServiceDesk|Configure a custom email address');
+export const I18N_FEEDBACK_PARAGRAPH = s__(
+ 'ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}',
+);
+export const I18N_GENERIC_ERROR = __('An error occurred. Please try again.');
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index 0f4c747a7b6..dd9585734db 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -22,6 +22,7 @@ export default () => {
selectedFileTemplateProjectId,
templates,
publicProject,
+ customEmailEndpoint,
} = el.dataset;
return new Vue({
@@ -39,6 +40,7 @@ export default () => {
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
publicProject: parseBoolean(publicProject),
+ customEmailEndpoint,
},
render: (createElement) => createElement(ServiceDeskRoot),
});
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index ad484b09d4d..9f83e955f4c 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -34,6 +34,9 @@ module Types
field :authored_date, type: Types::TimeType, null: true,
description: 'Timestamp of when the commit was authored.'
+ field :committed_date, type: Types::TimeType, null: true,
+ description: 'Timestamp of when the commit was committed.'
+
field :web_url, type: GraphQL::Types::String, null: false,
description: 'Web URL of the commit.'
@@ -55,6 +58,12 @@ module Types
field :author_name, type: GraphQL::Types::String, null: true,
description: 'Commit authors name.'
+ field :committer_email, type: GraphQL::Types::String, null: true,
+ description: "Email of the committer."
+
+ field :committer_name, type: GraphQL::Types::String, null: true,
+ description: "Name of the committer."
+
# models/commit lazy loads the author by email
field :author, type: Types::UserType, null: true,
description: 'Author of the commit.'
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index a13cb353c7b..3c592c0008f 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
- include IgnorableColumns
include DatabaseEventTracking
belongs_to :merge_request, inverse_of: :metrics
@@ -17,8 +16,6 @@ class MergeRequest::Metrics < ApplicationRecord
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
scope :by_target_project, ->(project) { where(target_project_id: project) }
- ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
-
class << self
def time_to_merge_expression
Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))')
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index 0a83efdb3b8..b6e54e7395a 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -19,6 +19,7 @@
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
templates: available_service_desk_templates_for(@project),
- public_project: "#{@project.public?}" } }
+ public_project: "#{@project.public?}",
+ custom_email_endpoint: project_service_desk_custom_email_path(@project) } }
- elsif show_callout?('promote_service_desk_dismissed')
= render 'shared/promotions/promote_servicedesk'
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8d2219a0ef5..d278986266c 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -469,8 +469,6 @@
- 5
- - process_commit
- 3
-- - product_analytics_initialize_analytics
- - 1
- - product_analytics_initialize_snowplow_product_analytics
- 1
- - product_analytics_post_push
diff --git a/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml b/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml
new file mode 100644
index 00000000000..c2dda4a2923
--- /dev/null
+++ b/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml
@@ -0,0 +1,6 @@
+---
+migration_job_name: BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob
+description: Refreshes ProjectStatistics to remove pipeline_artifacts_size from the total storage_size
+feature_category: consumables_cost_management
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126053
+milestone: 16.3
diff --git a/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb b/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb
new file mode 100644
index 00000000000..631ca9ec550
--- /dev/null
+++ b/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIsUniqueToProjectAuthorizations < Gitlab::Database::Migration[2.1]
+ enable_lock_retries!
+
+ def change
+ add_column :project_authorizations, :is_unique, :boolean, null: true
+ end
+end
diff --git a/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb b/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb
new file mode 100644
index 00000000000..4ef493092a0
--- /dev/null
+++ b/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddNameToGoogleCloudLoggingConfiguration < Gitlab::Database::Migration[2.1]
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # text limit is added in a 20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb migration
+ def change
+ add_column :audit_events_google_cloud_logging_configurations, :name, :text
+ end
+
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb b/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb
new file mode 100644
index 00000000000..4a13bfda89e
--- /dev/null
+++ b/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToGoogleCloudLoggingConfigurationName < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :audit_events_google_cloud_logging_configurations, :name, 72
+ end
+
+ def down
+ remove_text_limit :audit_events_google_cloud_logging_configurations, :name
+ end
+end
diff --git a/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb b/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb
new file mode 100644
index 00000000000..258d4583aa1
--- /dev/null
+++ b/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddIndexToGoogleCloudLoggingConfiguration < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'uniq_google_cloud_logging_configuration_namespace_id_and_name'
+
+ def up
+ add_concurrent_index :audit_events_google_cloud_logging_configurations, [:namespace_id, :name], unique: true,
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :audit_events_google_cloud_logging_configurations, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb b/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb
new file mode 100644
index 00000000000..b1bd58d679a
--- /dev/null
+++ b/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RemoveInitializeAnalyticsWorkerJobInstances < Gitlab::Database::Migration[2.1]
+ DEPRECATED_JOB_CLASSES = %w[InitializeAnalyticsWorker]
+
+ disable_ddl_transaction!
+
+ def up
+ sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
+ end
+
+ def down
+ # This migration removes any instances of deprecated workers and cannot be undone.
+ end
+end
diff --git a/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb b/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb
new file mode 100644
index 00000000000..d4d51131dda
--- /dev/null
+++ b/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ScheduleUniqueIndexProjectAuthorizationsOnUniqueProjectUser < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_unique_project_authorizations_on_unique_project_user'
+
+ def up
+ prepare_async_index :project_authorizations,
+ %i[project_id user_id],
+ unique: true,
+ where: "is_unique",
+ name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index :project_authorizations,
+ %i[project_id user_id],
+ name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb b/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb
new file mode 100644
index 00000000000..17e1e9e81f8
--- /dev/null
+++ b/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTempIndexForProjectStatisticsPipelineArtifactsSizeMigration < Gitlab::Database::Migration[2.1]
+ INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE = 'tmp_index_project_statistics_pipeline_artifacts_size'
+
+ disable_ddl_transaction!
+
+ def up
+ # Temporary index is to be used to trigger a refresh of project_statistics with
+ # pipeline_artifacts_size != 0
+ add_concurrent_index :project_statistics, [:project_id],
+ name: INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE,
+ where: "pipeline_artifacts_size != 0"
+ end
+
+ def down
+ remove_concurrent_index_by_name :project_statistics, INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE
+ end
+end
diff --git a/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb b/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb
new file mode 100644
index 00000000000..135d3596960
--- /dev/null
+++ b/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSize < Gitlab::Database::Migration[2.1]
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ MIGRATION = "BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob"
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 500
+ SUB_BATCH_SIZE = 100
+
+ def up
+ return unless Gitlab.dev_or_test_env? || Gitlab.org_or_com?
+
+ queue_batched_background_migration(
+ MIGRATION,
+ :project_statistics,
+ :project_id,
+ job_interval: DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE
+ )
+ end
+
+ def down
+ return unless Gitlab.dev_or_test_env? || Gitlab.org_or_com?
+
+ delete_batched_background_migration(MIGRATION, :project_statistics, :project_id, [])
+ end
+end
diff --git a/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb b/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb
new file mode 100644
index 00000000000..5e06170a506
--- /dev/null
+++ b/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddNotesNamespaceIdForeignKey < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_notes_on_namespace_id'
+
+ def up
+ add_concurrent_index :notes, :namespace_id, name: INDEX_NAME
+ add_concurrent_foreign_key :notes, :namespaces,
+ column: :namespace_id,
+ on_delete: :cascade
+ end
+
+ def down
+ remove_foreign_key_if_exists :notes, column: :namespace_id
+ remove_concurrent_index_by_name :notes, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230714084415 b/db/schema_migrations/20230714084415
new file mode 100644
index 00000000000..4cfc303bbc1
--- /dev/null
+++ b/db/schema_migrations/20230714084415
@@ -0,0 +1 @@
+0feaa606eef20192e84dfac7bc9285ca7c7baa56892fba99867c43b6b486dc33 \ No newline at end of file
diff --git a/db/schema_migrations/20230714095946 b/db/schema_migrations/20230714095946
new file mode 100644
index 00000000000..26efcf8bed1
--- /dev/null
+++ b/db/schema_migrations/20230714095946
@@ -0,0 +1 @@
+4cd83a7c498b698c7fa2b7adade3ac96005ff3408f92138416efa9c3a13ab05b \ No newline at end of file
diff --git a/db/schema_migrations/20230718145613 b/db/schema_migrations/20230718145613
new file mode 100644
index 00000000000..9202326eeed
--- /dev/null
+++ b/db/schema_migrations/20230718145613
@@ -0,0 +1 @@
+dc5291ab650b378e4bad5a395715a03535e2b217507ab36281dbf43b907fd9e1 \ No newline at end of file
diff --git a/db/schema_migrations/20230719083202 b/db/schema_migrations/20230719083202
new file mode 100644
index 00000000000..03f8d98fa23
--- /dev/null
+++ b/db/schema_migrations/20230719083202
@@ -0,0 +1 @@
+a604208ecfe76fe6ba380b804d81018e00a084146c4b29418ce4d447cb030c86 \ No newline at end of file
diff --git a/db/schema_migrations/20230726104022 b/db/schema_migrations/20230726104022
new file mode 100644
index 00000000000..ca48184b4a5
--- /dev/null
+++ b/db/schema_migrations/20230726104022
@@ -0,0 +1 @@
+d11b25fc925acdd86fe8ba25347a45dac314cf1963dbdaa6da58f63ade92cd2c \ No newline at end of file
diff --git a/db/schema_migrations/20230726104547 b/db/schema_migrations/20230726104547
new file mode 100644
index 00000000000..7657a19078b
--- /dev/null
+++ b/db/schema_migrations/20230726104547
@@ -0,0 +1 @@
+d880367d09bb1e563ddc61727b9bf852782853b78b822039bcf59fde4a714cc3 \ No newline at end of file
diff --git a/db/schema_migrations/20230726104616 b/db/schema_migrations/20230726104616
new file mode 100644
index 00000000000..dde68062a7c
--- /dev/null
+++ b/db/schema_migrations/20230726104616
@@ -0,0 +1 @@
+15bfb68b92824b905681fedae219caa92ddd8976d6178497a76e2714db872a08 \ No newline at end of file
diff --git a/db/schema_migrations/20230731090319 b/db/schema_migrations/20230731090319
new file mode 100644
index 00000000000..6849ff0f158
--- /dev/null
+++ b/db/schema_migrations/20230731090319
@@ -0,0 +1 @@
+d3ec607d6efbc8f890648bad5ccaf84360ce0ba2417c0dfcb9b91eca08767cf4 \ No newline at end of file
diff --git a/db/schema_migrations/20230731130351 b/db/schema_migrations/20230731130351
new file mode 100644
index 00000000000..b35240ee857
--- /dev/null
+++ b/db/schema_migrations/20230731130351
@@ -0,0 +1 @@
+a68e6320db6c7d370aa72ccd0a262f989d9a2c1dd3b4c49fdae9dcb80fa04f59 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f88bb440002..2e66ac6efa3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12240,9 +12240,11 @@ CREATE TABLE audit_events_google_cloud_logging_configurations (
log_id_name text DEFAULT 'audit_events'::text,
encrypted_private_key bytea NOT NULL,
encrypted_private_key_iv bytea NOT NULL,
+ name text,
CONSTRAINT check_0ef835c61e CHECK ((char_length(client_email) <= 254)),
CONSTRAINT check_55783c7c19 CHECK ((char_length(google_project_id_name) <= 30)),
- CONSTRAINT check_898a76b005 CHECK ((char_length(log_id_name) <= 511))
+ CONSTRAINT check_898a76b005 CHECK ((char_length(log_id_name) <= 511)),
+ CONSTRAINT check_cdf6883cd6 CHECK ((char_length(name) <= 72))
);
CREATE SEQUENCE audit_events_google_cloud_logging_configurations_id_seq
@@ -20983,7 +20985,8 @@ ALTER SEQUENCE project_aliases_id_seq OWNED BY project_aliases.id;
CREATE TABLE project_authorizations (
user_id integer NOT NULL,
project_id integer NOT NULL,
- access_level integer NOT NULL
+ access_level integer NOT NULL,
+ is_unique boolean
);
CREATE TABLE project_auto_devops (
@@ -32383,6 +32386,8 @@ CREATE INDEX index_notes_on_id_where_internal ON notes USING btree (id) WHERE (i
CREATE INDEX index_notes_on_line_code ON notes USING btree (line_code);
+CREATE INDEX index_notes_on_namespace_id ON notes USING btree (namespace_id);
+
CREATE INDEX index_notes_on_noteable_id_and_noteable_type_and_system ON notes USING btree (noteable_id, noteable_type, system);
CREATE INDEX index_notes_on_project_id_and_id_and_system_false ON notes USING btree (project_id, id) WHERE (NOT system);
@@ -34007,12 +34012,16 @@ CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING
CREATE INDEX tmp_index_project_statistics_cont_registry_size ON project_statistics USING btree (project_id) WHERE (container_registry_size = 0);
+CREATE INDEX tmp_index_project_statistics_pipeline_artifacts_size ON project_statistics USING btree (project_id) WHERE (pipeline_artifacts_size <> 0);
+
CREATE INDEX tmp_index_vulnerability_dismissal_info ON vulnerabilities USING btree (id) WHERE ((state = 2) AND ((dismissed_at IS NULL) OR (dismissed_by_id IS NULL)));
CREATE INDEX tmp_index_vulnerability_overlong_title_html ON vulnerabilities USING btree (id) WHERE (length(title_html) > 800);
CREATE UNIQUE INDEX u_project_compliance_standards_adherence_for_reporting ON project_compliance_standards_adherence USING btree (project_id, check_name, standard);
+CREATE UNIQUE INDEX uniq_google_cloud_logging_configuration_namespace_id_and_name ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, name);
+
CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14);
CREATE UNIQUE INDEX uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, user_id);
@@ -36230,6 +36239,9 @@ ALTER TABLE ONLY environments
ALTER TABLE ONLY vulnerabilities
ADD CONSTRAINT fk_76bc5f5455 FOREIGN KEY (resolved_by_id) REFERENCES users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY notes
+ ADD CONSTRAINT fk_76db6d50c6 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY oauth_openid_requests
ADD CONSTRAINT fk_77114b3b09 FOREIGN KEY (access_grant_id) REFERENCES oauth_access_grants(id) ON DELETE CASCADE;
diff --git a/doc/administration/external_users.md b/doc/administration/external_users.md
index f8ca379d10c..9be49fab95f 100644
--- a/doc/administration/external_users.md
+++ b/doc/administration/external_users.md
@@ -34,7 +34,7 @@ always take into account the
as well as the permission level of the user.
NOTE:
-External users still count towards a license seat.
+External users still count towards a license seat, unless the user has the [Guest role](../subscriptions/self_managed/index.md#free-guest-users) in the Ultimate tier.
An administrator can flag a user as external by either of the following methods:
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index ae0538104cf..5b4d8fc1389 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -13954,6 +13954,9 @@ Code Quality report for a pipeline.
| <a id="commitauthorgravatar"></a>`authorGravatar` | [`String`](#string) | Commit authors gravatar. |
| <a id="commitauthorname"></a>`authorName` | [`String`](#string) | Commit authors name. |
| <a id="commitauthoreddate"></a>`authoredDate` | [`Time`](#time) | Timestamp of when the commit was authored. |
+| <a id="commitcommitteddate"></a>`committedDate` | [`Time`](#time) | Timestamp of when the commit was committed. |
+| <a id="commitcommitteremail"></a>`committerEmail` | [`String`](#string) | Email of the committer. |
+| <a id="commitcommittername"></a>`committerName` | [`String`](#string) | Name of the committer. |
| <a id="commitdescription"></a>`description` | [`String`](#string) | Description of the commit message. |
| <a id="commitdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="commitdiffs"></a>`diffs` | [`[Diff!]`](#diff) | Diffs contained within the commit. This field can only be resolved for 10 diffs in any single request. |
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index ddb87f1e569..6e7dcb7e5fd 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -524,7 +524,7 @@ You can see the required minimal access levels and abilities requirements in the
To associate a custom role with an existing group member, a group member with
the Owner role:
-1. Invites a user to the root group or any subgroup or project in the root
+1. Invites a user as a direct member to the root group or any subgroup or project in the root
group's hierarchy as a Guest. At this point, this Guest user cannot see any
code on the projects in the group or subgroup.
1. Optional. If the Owner does not know the `ID` of the Guest user receiving a custom
diff --git a/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb b/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb
new file mode 100644
index 00000000000..88d0f27282a
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # rubocop:disable Style/Documentation
+ class BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob < Gitlab::BackgroundMigration::BatchedMigrationJob # rubocop:disable Layout/LineLength
+ class Project < ::ApplicationRecord
+ self.table_name = 'projects'
+
+ has_one :statistics, class_name: '::Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob::ProjectStatistics' # rubocop:disable Layout/LineLength
+ end
+
+ class ProjectStatistics < ::ApplicationRecord
+ include ::EachBatch
+
+ self.table_name = 'project_statistics'
+
+ belongs_to :project, class_name: '::Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob::Project' # rubocop:disable Layout/LineLength
+
+ def update_storage_size(storage_size_components)
+ new_storage_size = storage_size_components.sum { |component| method(component).call }
+
+ # Only update storage_size if storage_size needs updating
+ return unless storage_size != new_storage_size
+
+ self.storage_size = new_storage_size
+ save!
+
+ ::Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
+ log_with_data('Scheduled Namespaces::ScheduleAggregationWorker')
+ end
+
+ def wiki_size
+ super.to_i
+ end
+
+ def snippets_size
+ super.to_i
+ end
+
+ private
+
+ def log_with_data(log_line)
+ log_info(
+ log_line,
+ project_id: project.id,
+ pipeline_artifacts_size: pipeline_artifacts_size,
+ storage_size: storage_size,
+ namespace_id: project.namespace_id
+ )
+ end
+
+ def log_info(message, **extra)
+ ::Gitlab::BackgroundMigration::Logger.info(
+ migrator: 'BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob',
+ message: message,
+ **extra
+ )
+ end
+ end
+
+ scope_to ->(relation) {
+ relation.where.not(pipeline_artifacts_size: 0)
+ }
+ operation_name :update_storage_size
+ feature_category :consumables_cost_management
+
+ def perform
+ each_sub_batch do |sub_batch|
+ ProjectStatistics.merge(sub_batch).each do |statistics|
+ statistics.update_storage_size(storage_size_components)
+ end
+ end
+ end
+
+ private
+
+ # Overridden in EE
+ def storage_size_components
+ [
+ :repository_size,
+ :wiki_size,
+ :lfs_objects_size,
+ :build_artifacts_size,
+ :packages_size,
+ :snippets_size,
+ :uploads_size
+ ]
+ end
+ end
+ # rubocop:enable Style/Documentation
+ end
+end
+
+Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob.prepend_mod
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9bf0700b3b1..565e5c22561 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -43081,6 +43081,9 @@ msgstr ""
msgid "ServiceDesk|Cannot update custom email"
msgstr ""
+msgid "ServiceDesk|Configure a custom email address"
+msgstr ""
+
msgid "ServiceDesk|Custom email address could not be verified."
msgstr ""
@@ -43108,6 +43111,9 @@ msgstr ""
msgid "ServiceDesk|Parameters missing"
msgstr ""
+msgid "ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}"
+msgstr ""
+
msgid "ServiceDesk|Service Desk is not enabled"
msgstr ""
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 882924c003b..8092b69d377 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -77,6 +77,11 @@ module QA
has_element?('delete-issue-button')
end
+ def has_no_delete_issue_button?
+ open_actions_dropdown
+ has_no_element?('delete-issue-button')
+ end
+
def delete_issue
has_delete_issue_button?
diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb
index c77b585ada8..6fa88ef205b 100644
--- a/qa/qa/support/matchers/have_matcher.rb
+++ b/qa/qa/support/matchers/have_matcher.rb
@@ -30,6 +30,7 @@ module QA
alert_with_title
incident
framework
+ delete_issue_button
].each do |predicate|
RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs|
match do |page_object|
diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js
new file mode 100644
index 00000000000..f167d2e9d6e
--- /dev/null
+++ b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js
@@ -0,0 +1,94 @@
+import { nextTick } from 'vue';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
+import CustomEmail from '~/projects/settings_service_desk/components/custom_email.vue';
+import {
+ FEEDBACK_ISSUE_URL,
+ I18N_GENERIC_ERROR,
+} from '~/projects/settings_service_desk/custom_email_constants';
+import { MOCK_CUSTOM_EMAIL_EMPTY } from './mock_data';
+
+describe('CustomEmail', () => {
+ let axiosMock;
+ let wrapper;
+
+ const defaultProps = {
+ incomingEmail: 'incoming@example.com',
+ customEmailEndpoint: '/flightjs/Flight/-/service_desk/custom_email',
+ };
+
+ const createWrapper = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(CustomEmail, {
+ propsData: { ...defaultProps, ...props },
+ }),
+ );
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ it('displays link to feedback issue', () => {
+ createWrapper();
+
+ expect(findFeedbackLink().attributes('href')).toEqual(FEEDBACK_ISSUE_URL);
+ });
+
+ describe('when initial resource loading returns no configured custom email', () => {
+ beforeEach(() => {
+ axiosMock
+ .onGet(defaultProps.customEmailEndpoint)
+ .reply(HTTP_STATUS_OK, MOCK_CUSTOM_EMAIL_EMPTY);
+
+ createWrapper();
+ });
+
+ it('displays loading icon while fetching data', async () => {
+ // while loading
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+ // loading completed
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when initial resource loading returns 404', () => {
+ beforeEach(async () => {
+ axiosMock.onGet(defaultProps.customEmailEndpoint).reply(HTTP_STATUS_NOT_FOUND);
+
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('displays error alert with correct text', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(I18N_GENERIC_ERROR);
+ });
+
+ it('dismissing the alert removes it', async () => {
+ expect(findAlert().exists()).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js
index 934778ff601..ea88a6cfccd 100644
--- a/spec/frontend/projects/settings_service_desk/components/mock_data.js
+++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js
@@ -6,3 +6,12 @@ export const TEMPLATES = [
{ name: 'Security release', project_id: 1 },
],
];
+
+export const MOCK_CUSTOM_EMAIL_EMPTY = {
+ custom_email: null,
+ custom_email_enabled: false,
+ custom_email_verification_state: null,
+ custom_email_verification_error: null,
+ custom_email_smtp_address: null,
+ error_message: null,
+};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index b84d1c9c0aa..60f0efd9195 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
+import CustomEmail from '~/projects/settings_service_desk/components/custom_email.vue';
describe('ServiceDeskRoot', () => {
let axiosMock;
@@ -25,6 +26,10 @@ describe('ServiceDeskRoot', () => {
selectedFileTemplateProjectId: 42,
templates: ['Bug', 'Documentation'],
publicProject: false,
+ customEmailEndpoint: '/gitlab-org/gitlab-test/-/service_desk/custom_email',
+ glFeatures: {
+ serviceDeskCustomEmail: true,
+ },
};
const getAlertText = () => wrapper.findComponent(GlAlert).text();
@@ -186,4 +191,46 @@ describe('ServiceDeskRoot', () => {
});
});
});
+
+ describe('CustomEmail component', () => {
+ it('is rendered', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.findComponent(CustomEmail).exists()).toBe(true);
+ expect(wrapper.findComponent(CustomEmail).props()).toEqual({
+ incomingEmail: provideData.initialIncomingEmail,
+ customEmailEndpoint: provideData.customEmailEndpoint,
+ });
+ });
+
+ describe('when Service Desk is disabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ initialIsEnabled: false });
+ });
+
+ it('is not rendered', () => {
+ expect(wrapper.findComponent(CustomEmail).exists()).toBe(false);
+ });
+ });
+
+ describe('when issue tracker is disabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ isIssueTrackerEnabled: false });
+ });
+
+ it('is not rendered', () => {
+ expect(wrapper.findComponent(CustomEmail).exists()).toBe(false);
+ });
+ });
+
+ describe('when feature flag service_desk_custom_email is disabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ glFeatures: { serviceDeskCustomEmail: false } });
+ });
+
+ it('is not rendered', () => {
+ expect(wrapper.findComponent(CustomEmail).exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 3912b0905e3..6af5ea04dd2 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
expect(described_class).to have_graphql_fields(
:id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message, :title_html, :authored_date,
:author_name, :author_email, :author_gravatar, :author, :diffs, :web_url, :web_path,
- :pipelines, :signature_html, :signature
+ :pipelines, :signature_html, :signature, :committer_name, :committer_email, :committed_date
)
end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb
new file mode 100644
index 00000000000..c85636f4998
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob,
+ schema: 20230719083202,
+ feature_category: :consumables_cost_management do
+ include MigrationHelpers::ProjectStatisticsHelper
+
+ include_context 'when backfilling project statistics'
+
+ let(:default_pipeline_artifacts_size) { 5 }
+ let(:default_stats) do
+ {
+ repository_size: 1,
+ wiki_size: 1,
+ lfs_objects_size: 1,
+ build_artifacts_size: 1,
+ packages_size: 1,
+ snippets_size: 1,
+ uploads_size: 1,
+ pipeline_artifacts_size: default_pipeline_artifacts_size,
+ storage_size: default_storage_size
+ }
+ end
+
+ describe '#filter_batch' do
+ it 'filters out project_statistics with no artifacts size' do
+ project_statistics = generate_records(default_projects, project_statistics_table, default_stats)
+ project_statistics_table.create!(
+ project_id: proj5.id,
+ namespace_id: proj5.namespace_id,
+ repository_size: 1,
+ wiki_size: 1,
+ lfs_objects_size: 1,
+ build_artifacts_size: 1,
+ packages_size: 1,
+ snippets_size: 1,
+ pipeline_artifacts_size: 0,
+ uploads_size: 1,
+ storage_size: 7
+ )
+
+ expected = project_statistics.map(&:id)
+ actual = migration.filter_batch(project_statistics_table).pluck(:id)
+
+ expect(actual).to match_array(expected)
+ end
+ end
+
+ describe '#perform' do
+ subject(:perform_migration) { migration.perform }
+
+ context 'when project_statistics backfill runs' do
+ before do
+ generate_records(default_projects, project_statistics_table, default_stats)
+ end
+
+ context 'when storage_size includes pipeline_artifacts_size' do
+ it 'removes pipeline_artifacts_size from storage_size' do
+ allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ expect(project_statistics_table.pluck(:storage_size).uniq).to match_array([default_storage_size])
+
+ perform_migration
+
+ expect(project_statistics_table.pluck(:storage_size).uniq).to match_array(
+ [default_storage_size - default_pipeline_artifacts_size]
+ )
+ expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(4).times
+ end
+ end
+
+ context 'when storage_size does not include default_pipeline_artifacts_size' do
+ it 'does not update the record' do
+ allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ proj_stat = project_statistics_table.last
+ expect(proj_stat.storage_size).to eq(default_storage_size)
+ proj_stat.storage_size = default_storage_size - default_pipeline_artifacts_size
+ proj_stat.save!
+
+ perform_migration
+
+ expect(project_statistics_table.pluck(:storage_size).uniq).to match_array(
+ [default_storage_size - default_pipeline_artifacts_size]
+ )
+ expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(3).times
+ end
+ end
+ end
+
+ it 'coerces a null wiki_size to 0' do
+ project_statistics = create_project_stats(projects, namespaces, default_stats, { wiki_size: nil })
+ allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ migration = create_migration(end_id: project_statistics.project_id)
+
+ migration.perform
+
+ project_statistics.reload
+ expect(project_statistics.storage_size).to eq(6)
+ end
+
+ it 'coerces a null snippets_size to 0' do
+ project_statistics = create_project_stats(projects, namespaces, default_stats, { snippets_size: nil })
+ allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ migration = create_migration(end_id: project_statistics.project_id)
+
+ migration.perform
+
+ project_statistics.reload
+ expect(project_statistics.storage_size).to eq(6)
+ end
+ end
+end
diff --git a/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb b/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb
new file mode 100644
index 00000000000..c3183a5da1b
--- /dev/null
+++ b/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSize, feature_category: :consumables_cost_management do
+ let!(:batched_migration) { described_class::MIGRATION }
+
+ it 'does not schedule background jobs when Gitlab.org_or_com? is false' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ allow(Gitlab).to receive(:org_or_com?).and_return(false)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+ end
+ end
+
+ it 'schedules a new batched migration' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ allow(Gitlab).to receive(:org_or_com?).and_return(true)
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :project_statistics,
+ column_name: :project_id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ }
+ end
+ end
+end
diff --git a/spec/support/helpers/migrations_helpers/project_statistics_helper.rb b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb
new file mode 100644
index 00000000000..4e7d83a38ac
--- /dev/null
+++ b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module MigrationHelpers
+ module ProjectStatisticsHelper
+ def generate_records(projects, table, values = {})
+ projects.map do |proj|
+ table.create!(
+ values.merge({
+ project_id: proj.id,
+ namespace_id: proj.namespace_id
+ })
+ )
+ end
+ end
+
+ def create_migration(end_id:)
+ described_class.new(start_id: 1, end_id: end_id,
+ batch_table: 'project_statistics', batch_column: 'project_id',
+ sub_batch_size: 1_000, pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ def create_project_stats(project_table, namespace, default_stats, override_stats = {})
+ stats = default_stats.merge(override_stats)
+
+ group = namespace.create!(name: 'group_a', path: 'group-a', type: 'Group')
+ project_namespace = namespace.create!(name: 'project_a', path: 'project_a', type: 'Project', parent_id: group.id)
+ proj = project_table.create!(name: 'project_a', path: 'project-a', namespace_id: group.id,
+ project_namespace_id: project_namespace.id)
+ project_statistics_table.create!(
+ project_id: proj.id,
+ namespace_id: group.id,
+ **stats
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb
new file mode 100644
index 00000000000..1b835e1392d
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when backfilling project statistics' do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:project_statistics_table) { table(:project_statistics) }
+ let!(:projects) { table(:projects) }
+ let!(:count_of_columns) { ProjectStatistics::STORAGE_SIZE_COMPONENTS.count }
+ let(:default_storage_size) { 12 }
+
+ let!(:root_group) do
+ namespaces.create!(name: 'root-group', path: 'root-group', type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [new_group.id])
+ end
+ end
+
+ let!(:group) do
+ namespaces.create!(name: 'group', path: 'group', parent_id: root_group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, new_group.id])
+ end
+ end
+
+ let!(:sub_group) do
+ namespaces.create!(name: 'subgroup', path: 'subgroup', parent_id: group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, group.id, new_group.id])
+ end
+ end
+
+ let!(:namespace1) do
+ namespaces.create!(
+ name: 'namespace1', type: 'Group', path: 'space1'
+ )
+ end
+
+ let!(:proj_namespace1) do
+ namespaces.create!(
+ name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id
+ )
+ end
+
+ let!(:proj_namespace2) do
+ namespaces.create!(
+ name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace1.id
+ )
+ end
+
+ let!(:proj_namespace3) do
+ namespaces.create!(
+ name: 'proj3', path: 'proj3', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj_namespace4) do
+ namespaces.create!(
+ name: 'proj4', path: 'proj4', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj_namespace5) do
+ namespaces.create!(
+ name: 'proj5', path: 'proj5', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj1) do
+ projects.create!(
+ name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id
+ )
+ end
+
+ let!(:proj2) do
+ projects.create!(
+ name: 'proj2', path: 'proj2', namespace_id: namespace1.id, project_namespace_id: proj_namespace2.id
+ )
+ end
+
+ let!(:proj3) do
+ projects.create!(
+ name: 'proj3', path: 'proj3', namespace_id: sub_group.id, project_namespace_id: proj_namespace3.id
+ )
+ end
+
+ let!(:proj4) do
+ projects.create!(
+ name: 'proj4', path: 'proj4', namespace_id: sub_group.id, project_namespace_id: proj_namespace4.id
+ )
+ end
+
+ let!(:proj5) do
+ projects.create!(
+ name: 'proj5', path: 'proj5', namespace_id: sub_group.id, project_namespace_id: proj_namespace5.id
+ )
+ end
+
+ let(:migration) do
+ described_class.new(start_id: 1, end_id: proj4.id,
+ batch_table: 'project_statistics', batch_column: 'project_id',
+ sub_batch_size: 1_000, pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ let(:default_projects) do
+ [
+ proj1, proj2, proj3, proj4
+ ]
+ end
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 38959b6d764..3cd030e678d 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -398,7 +398,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'PipelineProcessWorker' => 3,
'PostReceive' => 3,
'ProcessCommitWorker' => 3,
- 'ProductAnalytics::InitializeAnalyticsWorker' => 3,
'ProductAnalytics::InitializeSnowplowProductAnalyticsWorker' => 1,
'ProjectCacheWorker' => 3,
'ProjectDestroyWorker' => 3,