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:
-rw-r--r--app/assets/javascripts/reports/codequality_report/constants.js14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js123
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb27
-rw-r--r--app/controllers/jira_connect/events_controller.rb30
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/user.rb16
-rw-r--r--app/views/admin/groups/_form.html.haml9
-rw-r--r--app/views/dashboard/todos/index.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/projects/_deletion_failed.html.haml11
-rw-r--r--app/views/projects/_new_project_fields.html.haml3
-rw-r--r--app/views/shared/_two_factor_auth_recovery_settings_check.html.haml2
-rw-r--r--config/feature_flags/development/jira_connect_installation_update.yml (renamed from config/feature_flags/development/restrict_special_characters_in_project_path.yml)10
-rw-r--r--config/feature_flags/development/route_hll_to_snowplow.yml8
-rw-r--r--doc/ci/jobs/index.md3
-rw-r--r--doc/development/performance.md37
-rw-r--r--locale/gitlab.pot22
-rw-r--r--spec/controllers/concerns/product_analytics_tracking_spec.rb171
-rw-r--r--spec/controllers/jira_connect/events_controller_spec.rb54
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js145
-rw-r--r--spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js87
-rw-r--r--spec/models/project_spec.rb32
26 files changed, 744 insertions, 109 deletions
diff --git a/app/assets/javascripts/reports/codequality_report/constants.js b/app/assets/javascripts/reports/codequality_report/constants.js
index 502977e714c..0c472b24471 100644
--- a/app/assets/javascripts/reports/codequality_report/constants.js
+++ b/app/assets/javascripts/reports/codequality_report/constants.js
@@ -15,3 +15,17 @@ export const SEVERITY_ICONS = {
blocker: 'severity-critical',
unknown: 'severity-unknown',
};
+
+// This is the icons mapping for the code Quality Merge-Request Widget Extension
+// once the refactor_mr_widgets_extensions flag is activated the above SEVERITY_ICONS
+// need be removed and this variable needs to be rename to SEVERITY_ICONS
+// Rollout Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341759
+
+export const SEVERITY_ICONS_EXTENSION = {
+ info: 'severityInfo',
+ minor: 'severityLow',
+ major: 'severityMedium',
+ critical: 'severityHigh',
+ blocker: 'severityCritical',
+ unknown: 'severityUnknown',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 1621b6831a2..7435f578852 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -82,17 +82,8 @@ export default {
return this.mr.shouldBeRebased;
},
- sourceBranchProtected() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.stateData.sourceBranchProtected;
- }
-
- return this.mr.sourceBranchProtected;
- },
showResolveButton() {
- return (
- this.mr.conflictResolutionPath && this.canPushToSourceBranch && !this.sourceBranchProtected
- );
+ return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
new file mode 100644
index 00000000000..d32db50874c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -0,0 +1,123 @@
+import { n__, s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+import { SEVERITY_ICONS_EXTENSION } from '~/reports/codequality_report/constants';
+import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+export default {
+ name: 'WidgetCodeQuality',
+ props: ['codeQuality', 'blobPath'],
+ i18n: {
+ label: s__('ciReport|Code Quality'),
+ loading: s__('ciReport|Code Quality test metrics results are being parsed'),
+ error: s__('ciReport|Code Quality failed loading results'),
+ },
+ expandEvent: 'i_testing_code_quality_widget_total',
+ computed: {
+ summary() {
+ const { newErrors, resolvedErrors, errorSummary } = this.collapsedData;
+ if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
+ const improvements = sprintf(
+ n__(
+ '%{strongOpen}%{errors}%{strongClose} point',
+ '%{strongOpen}%{errors}%{strongClose} points',
+ resolvedErrors.length,
+ ),
+ {
+ errors: resolvedErrors.length,
+ strongOpen: '<strong>',
+ strongClose: '</strong>',
+ },
+ false,
+ );
+
+ const degradations = sprintf(
+ n__(
+ '%{strongOpen}%{errors}%{strongClose} point',
+ '%{strongOpen}%{errors}%{strongClose} points',
+ newErrors.length,
+ ),
+ { errors: newErrors.length, strongOpen: '<strong>', strongClose: '</strong>' },
+ false,
+ );
+ return sprintf(
+ s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`),
+ );
+ } else if (errorSummary.resolved >= 1) {
+ const improvements = n__('%d point', '%d points', resolvedErrors.length);
+ return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`));
+ } else if (errorSummary.errored >= 1) {
+ const degradations = n__('%d point', '%d points', newErrors.length);
+ return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`));
+ }
+ return s__(`ciReport|No changes to Code Quality.`);
+ },
+ statusIcon() {
+ if (this.collapsedData.errorSummary?.errored >= 1) {
+ return EXTENSION_ICONS.warning;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => {
+ return {
+ resolvedErrors: parseCodeclimateMetrics(
+ values[0].resolved_errors,
+ this.blobPath.head_path,
+ ),
+ newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path),
+ existingErrors: parseCodeclimateMetrics(
+ values[0].existing_errors,
+ this.blobPath.head_path,
+ ),
+ errorSummary: values[0].summary,
+ };
+ });
+ },
+ fetchFullData() {
+ const fullData = [];
+
+ this.collapsedData.newErrors.map((e) => {
+ return fullData.push({
+ text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
+ subtext: sprintf(
+ s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`),
+ {
+ open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`,
+ close_link: '</a>',
+ },
+ false,
+ ),
+ icon: {
+ name: SEVERITY_ICONS_EXTENSION[e.severity],
+ },
+ });
+ });
+
+ this.collapsedData.resolvedErrors.map((e) => {
+ return fullData.push({
+ text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
+ subtext: sprintf(
+ s__(`ciReport|in %{open_link}${e.file_path}:${e.line}%{close_link}`),
+ {
+ open_link: `<a class="gl-text-decoration-underline" href="${e.urlPath}">`,
+ close_link: '</a>',
+ },
+ false,
+ ),
+ icon: {
+ name: SEVERITY_ICONS_EXTENSION[e.severity],
+ },
+ });
+ });
+
+ return Promise.resolve(fullData);
+ },
+ fetchReport(endpoint) {
+ return axios.get(endpoint).then((res) => res.data);
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index cd4d9398899..965746e79fb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -46,6 +46,7 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
+import codeQualityExtension from './extensions/code_quality';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -241,6 +242,11 @@ export default {
this.registerTerraformPlans();
}
},
+ shouldRenderCodeQuality(newVal) {
+ if (newVal) {
+ this.registerCodeQualityExtension();
+ }
+ },
shouldShowAccessibilityReport(newVal) {
if (newVal) {
this.registerAccessibilityExtension();
@@ -491,6 +497,11 @@ export default {
registerExtension(accessibilityExtension);
}
},
+ registerCodeQualityExtension() {
+ if (this.shouldRenderCodeQuality && this.shouldShowExtension) {
+ registerExtension(codeQualityExtension);
+ }
+ },
},
};
</script>
@@ -546,7 +557,7 @@ export default {
</div>
<extensions-container :mr="mr" />
<grouped-codequality-reports-app
- v-if="shouldRenderCodeQuality"
+ v-if="shouldRenderCodeQuality && !shouldShowExtension"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 994e0c23b44..eb07609d5d6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -32,9 +32,15 @@ export default class MergeRequestStore {
this.setPaths(data);
this.setData(data);
+ this.initCodeQualityReport(data);
this.setGitpodData(data);
}
+ initCodeQualityReport(data) {
+ this.blobPath = data.blob_path;
+ this.codeQuality = data.codequality_reports_path;
+ }
+
setData(data, isRebased) {
this.initApprovals();
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
new file mode 100644
index 00000000000..03296d6b233
--- /dev/null
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ProductAnalyticsTracking
+ include Gitlab::Tracking::Helpers
+ include RedisTracking
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def track_event(*controller_actions, name:, conditions: nil, destinations: [:redis_hll], &block)
+ custom_conditions = [:trackable_html_request?, *conditions]
+
+ after_action only: controller_actions, if: custom_conditions do
+ route_events_to(destinations, name, &block)
+ end
+ end
+ end
+
+ private
+
+ def route_events_to(destinations, name, &block)
+ track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll)
+
+ if destinations.include?(:snowplow) && Feature.enabled?(:route_hll_to_snowplow, tracking_namespace_source, default_enabled: :yaml)
+ Gitlab::Tracking.event(self.class.to_s, name, namespace: tracking_namespace_source, user: current_user)
+ end
+ end
+end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index 1ea0a92662b..327192857f6 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -7,11 +7,13 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
before_action :verify_asymmetric_atlassian_jwt!
def installed
- return head :ok if current_jira_installation
+ unless Feature.enabled?(:jira_connect_installation_update, default_enabled: :yaml)
+ return head :ok if current_jira_installation
+ end
- installation = JiraConnectInstallation.new(event_params)
+ success = current_jira_installation ? update_installation : create_installation
- if installation.save
+ if success
head :ok
else
head :unprocessable_entity
@@ -28,8 +30,24 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
private
- def event_params
- params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore)
+ def create_installation
+ JiraConnectInstallation.new(create_params).save
+ end
+
+ def update_installation
+ current_jira_installation.update(update_params)
+ end
+
+ def create_params
+ transformed_params.permit(:client_key, :shared_secret, :base_url)
+ end
+
+ def update_params
+ transformed_params.permit(:shared_secret, :base_url)
+ end
+
+ def transformed_params
+ @transformed_params ||= params.transform_keys(&:underscore)
end
def verify_asymmetric_atlassian_jwt!
@@ -43,7 +61,7 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
def jwt_verification_claims
{
aud: jira_connect_base_url(protocol: 'https'),
- iss: event_params[:client_key],
+ iss: transformed_params[:client_key],
qsh: Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
}
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 3ca719101d2..155ebe88d33 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -498,7 +498,10 @@ class Project < ApplicationRecord
presence: true,
project_path: true,
length: { maximum: 255 }
- validate :container_registry_project_path_validation
+ validates :path,
+ format: { with: Gitlab::Regex.oci_repository_path_regex,
+ message: Gitlab::Regex.oci_repository_path_regex_message },
+ if: :path_changed?
validates :project_feature, presence: true
@@ -889,14 +892,6 @@ class Project < ApplicationRecord
super
end
- def container_registry_project_path_validation
- if Feature.enabled?(:restrict_special_characters_in_project_path, self, default_enabled: :yaml) &&
- path_changed? &&
- !path.match?(Gitlab::Regex.oci_repository_path_regex)
- errors.add(:path, Gitlab::Regex.oci_repository_path_regex_message)
- end
- end
-
def parent_loaded?
association(:namespace).loaded?
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 2905db7ce6c..b3bdc2c1c42 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -291,21 +291,7 @@ class User < ApplicationRecord
end
end
after_commit(on: :update) do
- if previous_changes.key?('email')
- # Add the old primary email to Emails if not added already - this should be removed
- # after the background migration for MR https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70872/ has completed,
- # as the primary email is now added to Emails upon confirmation
- # Issue to remove that: https://gitlab.com/gitlab-org/gitlab/-/issues/344134
- previous_confirmed_at = previous_changes.key?('confirmed_at') ? previous_changes['confirmed_at'][0] : confirmed_at
- previous_email = previous_changes[:email][0]
- if previous_confirmed_at && !emails.exists?(email: previous_email)
- # rubocop: disable CodeReuse/ServiceClass
- Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
- # rubocop: enable CodeReuse/ServiceClass
- end
-
- update_invalid_gpg_signatures
- end
+ update_invalid_gpg_signatures if previous_changes.key?('email')
end
after_initialize :set_projects_limit
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 91a018121c0..0c3ce1f3fa4 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -27,12 +27,9 @@
- if @group.new_record?
.form-group.row
.offset-sm-2.col-sm-10
- .gl-alert.gl-alert-
- .gl-alert-container
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content
- .gl-alert-body
- = render 'shared/group_tips'
+ = render 'shared/global_alert', dismissible: false do
+ .gl-alert-body
+ = render 'shared/group_tips'
.form-actions
= f.submit _('Create group'), class: "gl-button btn btn-confirm"
= link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel"
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index cd177db3ed0..1d711f366c4 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -2,6 +2,7 @@
- page_title _("To-Do List")
- header_title _("To-Do List"), dashboard_todos_path
+= render_two_factor_auth_recovery_settings_check
= render_dashboard_ultimate_trial(current_user)
- add_page_specific_style 'page_bundles/todos'
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 133083f1103..a656b61dc8f 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -14,7 +14,6 @@
= dispensable_render "layouts/nav/classification_level_banner"
= yield :flash_message
= dispensable_render "shared/service_ping_consent"
- = render_two_factor_auth_recovery_settings_check
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
= dispensable_render_if_exists "layouts/header/seats_count_alert"
= dispensable_render_if_exists "shared/namespace_storage_limit_alert"
diff --git a/app/views/projects/_deletion_failed.html.haml b/app/views/projects/_deletion_failed.html.haml
index 21c799f5bb6..b713b805009 100644
--- a/app/views/projects/_deletion_failed.html.haml
+++ b/app/views/projects/_deletion_failed.html.haml
@@ -1,10 +1,7 @@
- project = local_assigns.fetch(:project)
- return unless project.delete_error.present?
-.project-deletion-failed-message.gl-alert.gl-alert-warning
- .gl-alert-container
- = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content
- .gl-alert-body
- This project was scheduled for deletion, but failed with the following message:
- = project.delete_error
+= render 'shared/global_alert', variant: :warning, dismissible: false, alert_class: 'project-deletion-failed-message' do
+ .gl-alert-body
+ This project was scheduled for deletion, but failed with the following message:
+ = project.delete_error
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 2d0a4ae8605..1fb045544aa 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -35,8 +35,7 @@
- link_start_group_path = '<a href="%{path}">' % { path: new_group_path }
- project_tip = s_('ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}') % { link_start: link_start_group_path, link_end: '</a>' }
= project_tip.html_safe
-.gl-alert.gl-alert-success.gl-mb-4.gl-display-none.js-user-readme-repo
- = sprite_icon('check-circle', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+= render 'shared/global_alert', alert_class: "gl-mb-4 gl-display-none js-user-readme-repo", dismissible: false, variant: :success do
.gl-alert-body
- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/profile/index', anchor: 'add-details-to-your-profile-with-a-readme') }
= html_escape(_('%{project_path} is a project that you can use to add a README to your GitLab profile. Create a public project and initialize the repository with a README to get started. %{help_link_start}Learn more.%{help_link_end}')) % { project_path: "<strong>#{current_user.username} / #{current_user.username}</strong>".html_safe, help_link_start: help_link_start, help_link_end: '</a>'.html_safe }
diff --git a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
index e7239661313..f21acd26ada 100644
--- a/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
+++ b/app/views/shared/_two_factor_auth_recovery_settings_check.html.haml
@@ -1,6 +1,6 @@
= render 'shared/global_alert',
variant: :warning,
- alert_class: 'js-recovery-settings-callout',
+ alert_class: 'js-recovery-settings-callout gl-mt-5',
alert_data: { feature_id: Users::CalloutsHelper::TWO_FACTOR_AUTH_RECOVERY_SETTINGS_CHECK, dismiss_endpoint: callouts_path, defer_links: 'true' },
close_button_data: { testid: 'close-account-recovery-regular-check-callout' } do
.gl-alert-body
diff --git a/config/feature_flags/development/restrict_special_characters_in_project_path.yml b/config/feature_flags/development/jira_connect_installation_update.yml
index d2eff146605..a92a7dafc14 100644
--- a/config/feature_flags/development/restrict_special_characters_in_project_path.yml
+++ b/config/feature_flags/development/jira_connect_installation_update.yml
@@ -1,8 +1,8 @@
---
-name: restrict_special_characters_in_project_path
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80055
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353054
-milestone: '14.8'
+name: jira_connect_installation_update
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83038
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356083
+milestone: '14.9'
type: development
-group: group::workspace
+group: group::integrations
default_enabled: false
diff --git a/config/feature_flags/development/route_hll_to_snowplow.yml b/config/feature_flags/development/route_hll_to_snowplow.yml
new file mode 100644
index 00000000000..3c8f7826a0a
--- /dev/null
+++ b/config/feature_flags/development/route_hll_to_snowplow.yml
@@ -0,0 +1,8 @@
+---
+name: route_hll_to_snowplow
+introduced_by_url: https://gitlab.com/gitlab-org/product-intelligence/-/issues/498
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354442
+milestone: '14.9'
+type: development
+group: group::product intelligence
+default_enabled: false
diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md
index 39e14d0d20a..4fa251eb3cf 100644
--- a/doc/ci/jobs/index.md
+++ b/doc/ci/jobs/index.md
@@ -103,6 +103,9 @@ Job names must be 255 characters or less. [Introduced](https://gitlab.com/gitlab
in GitLab 14.5, [with a feature flag](../../administration/feature_flags.md) named `ci_validate_job_length`.
Enabled by default. To disable it, ask an administrator to [disable the feature flag](../../administration/feature_flags.md).
+Use unique names for your jobs. If multiple jobs have the same name,
+only one is added to the pipeline, and it's difficult to predict which one is chosen.
+
## Group jobs in a pipeline
If you have many similar jobs, your [pipeline graph](../pipelines/index.md#visualize-pipelines) becomes long and hard
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 55453e31d3d..1e3e0570206 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -7,7 +7,38 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Performance Guidelines
This document describes various guidelines to follow to ensure good and
-consistent performance of GitLab.
+consistent performance of GitLab. Refer to the [Index](#performance-documentation) section below to navigate to Performance-related pages.
+
+## Performance Documentation
+
+- General:
+ - [Solving performance issues](#workflow)
+ - [Handbook performance page](https://about.gitlab.com/handbook/engineering/performance/)
+ - [Merge request performance guidelines](../development/merge_request_performance_guidelines.md)
+- Backend:
+ - [Tooling](#tooling)
+ - Database:
+ - [Query performance guidelines](../development/query_performance.md)
+ - [Pagination performance guidelines](../development/database/pagination_performance_guidelines.md)
+ - [Keyset pagination performance](../development/database/keyset_pagination.md#performance)
+ - [Troubleshooting import/export performance issues](../development/import_export.md#troubleshooting-performance-issues)
+ - [Pipelines performance in the `gitlab` project](../development/pipelines.md#performance)
+- Frontend:
+ - [Performance guidelines](../development/fe_guide/performance.md)
+ - [Performance dashboards and monitoring guidelines](../development/new_fe_guide/development/performance.md)
+ - [Browser performance testing guidelines](../user/project/merge_requests/browser_performance_testing.md)
+ - [`gdk measure` and `gdk measure-workflow`](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/gdk_commands.md#measure-performance)
+- QA:
+ - [Load performance testing](../user/project/merge_requests/load_performance_testing.md)
+ - [GitLab Performance Tool project](https://gitlab.com/gitlab-org/quality/performance)
+ - [Review apps performance metrics](../development/testing_guide/review_apps.md#performance-metrics)
+- Monitoring & Overview:
+ - [GitLab performance monitoring](../administration/monitoring/performance/index.md)
+ - [Development department performance indicators](https://about.gitlab.com/handbook/engineering/development/performance-indicators/)
+ - [Service measurement](../development/service_measurement.md)
+- Self-managed administration and customer-focused:
+ - [File system performance benchmarking](../administration/operations/filesystem_benchmarking.md)
+ - [Sidekiq performance troubleshooting](../administration/troubleshooting/sidekiq.md)
## Workflow
@@ -156,7 +187,7 @@ With the [Performance bar](../administration/monitoring/performance/performance_
you have the option to profile a request using Stackprof and immediately output the results to a
[Speedscope flamegraph](profiling.md#speedscope-flamegraphs).
-### RSpec profiling with Stackprof
+### RSpec profiling with Stackprof
To create a profile from a spec, identify (or create) a spec that
exercises the troublesome code path, then run it using the `bin/rspec-stackprof`
@@ -307,7 +338,7 @@ ProjectPolicy#access_allowed_to? (/Users/royzwambag/work/gitlab-development-kit/
code:
| 793 | def access_allowed_to?(feature)
141 (0.2%) | 794 | return false unless project.project_feature
- | 795 |
+ | 795 |
8 (0.0%) | 796 | case project.project_feature.access_level(feature)
| 797 | when ProjectFeature::DISABLED
| 798 | false
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e86ca43d657..4ff3918efb6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -350,6 +350,11 @@ msgid_plural "%d personal projects will be removed and cannot be restored."
msgstr[0] ""
msgstr[1] ""
+msgid "%d point"
+msgid_plural "%d points"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d previously merged commit"
msgid_plural "%d previously merged commits"
msgstr[0] ""
@@ -981,6 +986,11 @@ msgstr ""
msgid "%{start} to %{end}"
msgstr ""
+msgid "%{strongOpen}%{errors}%{strongClose} point"
+msgid_plural "%{strongOpen}%{errors}%{strongClose} points"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{strongOpen}Warning:%{strongClose} SAML group links can cause GitLab to automatically remove members from groups."
msgstr ""
@@ -43509,6 +43519,15 @@ msgstr ""
msgid "ciReport|Cluster Image Scanning"
msgstr ""
+msgid "ciReport|Code Quality"
+msgstr ""
+
+msgid "ciReport|Code Quality failed loading results"
+msgstr ""
+
+msgid "ciReport|Code Quality test metrics results are being parsed"
+msgstr ""
+
msgid "ciReport|Code quality degraded"
msgstr ""
@@ -43616,6 +43635,9 @@ msgstr ""
msgid "ciReport|New"
msgstr ""
+msgid "ciReport|No changes to Code Quality."
+msgstr ""
+
msgid "ciReport|No changes to code quality"
msgstr ""
diff --git a/spec/controllers/concerns/product_analytics_tracking_spec.rb b/spec/controllers/concerns/product_analytics_tracking_spec.rb
new file mode 100644
index 00000000000..250cc3cf2cf
--- /dev/null
+++ b/spec/controllers/concerns/product_analytics_tracking_spec.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe ProductAnalyticsTracking, :snowplow do
+ include TrackingHelpers
+ include SnowplowHelpers
+
+ let(:user) { create(:user) }
+ let!(:group) { create(:group) }
+
+ before do
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ end
+
+ controller(ApplicationController) do
+ include ProductAnalyticsTracking
+
+ skip_before_action :authenticate_user!, only: :show
+ track_event(:index, :show, name: 'g_analytics_valuestream', destinations: [:redis_hll, :snowplow],
+ conditions: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id }
+
+ def index
+ render html: 'index'
+ end
+
+ def new
+ render html: 'new'
+ end
+
+ def show
+ render html: 'show'
+ end
+
+ def get_custom_id
+ 'some_custom_id'
+ end
+
+ private
+
+ def tracking_namespace_source
+ Group.first
+ end
+
+ def custom_condition_one?
+ true
+ end
+
+ def custom_condition_two?
+ true
+ end
+ end
+
+ def expect_tracking(user: self.user)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to have_received(:track_event)
+ .with('g_analytics_valuestream', values: instance_of(String))
+
+ expect_snowplow_event(
+ category: anything,
+ action: 'g_analytics_valuestream',
+ namespace: group,
+ user: user
+ )
+ end
+
+ def expect_no_tracking
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ expect_no_snowplow_event
+ end
+
+ context 'when user is logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'tracks the event' do
+ get :index
+
+ expect_tracking
+ end
+
+ context 'when FF is disabled' do
+ before do
+ stub_feature_flags(route_hll_to_snowplow: false)
+ end
+
+ it 'doesnt track snowplow event' do
+ get :index
+
+ expect_no_snowplow_event
+ end
+ end
+
+ it 'tracks the event if DNT is not enabled' do
+ stub_do_not_track('0')
+
+ get :index
+
+ expect_tracking
+ end
+
+ it 'does not track the event if DNT is enabled' do
+ stub_do_not_track('1')
+
+ get :index
+
+ expect_no_tracking
+ end
+
+ it 'does not track the event if the format is not HTML' do
+ get :index, format: :json
+
+ expect_no_tracking
+ end
+
+ it 'does not track the event if a custom condition returns false' do
+ allow(controller).to receive(:custom_condition_two?).and_return(false)
+
+ get :index
+
+ expect_no_tracking
+ end
+
+ it 'does not track the event for untracked actions' do
+ get :new
+
+ expect_no_tracking
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:visitor_id) { SecureRandom.uuid }
+
+ it 'tracks the event when there is a visitor id' do
+ cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
+
+ get :show, params: { id: 1 }
+
+ expect_tracking(user: nil)
+ end
+ end
+
+ context 'when user is not logged in and there is no visitor_id' do
+ it 'does not track the event' do
+ get :index
+
+ expect_no_tracking
+ end
+
+ it 'tracks the event when there is custom id' do
+ get :show, params: { id: 1 }
+
+ expect_tracking(user: nil)
+ end
+
+ it 'does not track the HLL event when there is no custom id' do
+ allow(controller).to receive(:get_custom_id).and_return(nil)
+
+ get :show, params: { id: 2 }
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+ expect_snowplow_event(
+ category: anything,
+ action: 'g_analytics_valuestream',
+ namespace: group,
+ user: nil
+ )
+ end
+ end
+end
diff --git a/spec/controllers/jira_connect/events_controller_spec.rb b/spec/controllers/jira_connect/events_controller_spec.rb
index 2a70a2ea683..2129b24b2fb 100644
--- a/spec/controllers/jira_connect/events_controller_spec.rb
+++ b/spec/controllers/jira_connect/events_controller_spec.rb
@@ -43,14 +43,15 @@ RSpec.describe JiraConnect::EventsController do
end
describe '#installed' do
- let(:client_key) { '1234' }
- let(:shared_secret) { 'secret' }
+ let_it_be(:client_key) { '1234' }
+ let_it_be(:shared_secret) { 'secret' }
+ let_it_be(:base_url) { 'https://test.atlassian.net' }
let(:params) do
{
clientKey: client_key,
sharedSecret: shared_secret,
- baseUrl: 'https://test.atlassian.net'
+ baseUrl: base_url
}
end
@@ -77,11 +78,11 @@ RSpec.describe JiraConnect::EventsController do
expect(installation.base_url).to eq('https://test.atlassian.net')
end
- context 'when it is a version update and shared_secret is not sent' do
+ context 'when the shared_secret param is missing' do
let(:params) do
{
clientKey: client_key,
- baseUrl: 'https://test.atlassian.net'
+ baseUrl: base_url
}
end
@@ -90,13 +91,48 @@ RSpec.describe JiraConnect::EventsController do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
+ end
+
+ context 'when an installation already exists' do
+ let_it_be(:installation) { create(:jira_connect_installation, base_url: base_url, client_key: client_key, shared_secret: shared_secret) }
+
+ it 'validates the JWT token in authorization header and returns 200 without creating a new installation', :aggregate_failures do
+ expect { subject }.not_to change { JiraConnectInstallation.count }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when parameters include a new shared secret and base_url' do
+ let(:shared_secret) { 'new_secret' }
+ let(:base_url) { 'https://new_test.atlassian.net' }
- context 'and an installation exists' do
- let!(:installation) { create(:jira_connect_installation, client_key: client_key, shared_secret: shared_secret) }
+ it 'updates the installation', :aggregate_failures do
+ subject
- it 'validates the JWT token in authorization header and returns 200 without creating a new installation' do
- expect { subject }.not_to change { JiraConnectInstallation.count }
expect(response).to have_gitlab_http_status(:ok)
+ expect(installation.reload).to have_attributes(
+ shared_secret: shared_secret,
+ base_url: base_url
+ )
+ end
+
+ context 'when the `jira_connect_installation_update` feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_connect_installation_update: false)
+ end
+
+ it 'does not update the installation', :aggregate_failures do
+ expect { subject }.not_to change { installation.reload.attributes }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'when the new base_url is invalid' do
+ let(:base_url) { 'invalid' }
+
+ it 'renders 422', :aggregate_failures do
+ expect { subject }.not_to change { installation.reload.base_url }
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
end
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index bdc38eb0b22..7a92484695c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -224,8 +224,8 @@ describe('MRWidgetConflicts', () => {
});
});
- it('should not allow you to resolve the conflicts', () => {
- expect(findResolveButton().exists()).toBe(false);
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js
new file mode 100644
index 00000000000..9a72e4a086b
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/code_quality/index_spec.js
@@ -0,0 +1,145 @@
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { trimText } from 'helpers/text_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
+import httpStatusCodes from '~/lib/utils/http_status';
+import {
+ codeQualityResponseNewErrors,
+ codeQualityResponseResolvedErrors,
+ codeQualityResponseResolvedAndNewErrors,
+ codeQualityResponseNoErrors,
+} from './mock_data';
+
+describe('Code Quality extension', () => {
+ let wrapper;
+ let mock;
+
+ registerExtension(codeQualityExtension);
+
+ const endpoint = '/root/repo/-/merge_requests/4/accessibility_reports.json';
+
+ const mockApi = (statusCode, data) => {
+ mock.onGet(endpoint).reply(statusCode, data);
+ };
+
+ const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
+ const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ codeQuality: endpoint,
+ blobPath: {
+ head_path: 'example/path',
+ base_path: 'example/path',
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ it('displays loading text', () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+
+ createComponent();
+
+ expect(wrapper.text()).toBe('Code Quality test metrics results are being parsed');
+ });
+
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+
+ createComponent();
+
+ await waitForPromises();
+ expect(wrapper.text()).toBe('Code Quality failed loading results');
+ });
+
+ it('displays quality degradation', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Code Quality degraded on 2 points.');
+ });
+
+ it('displays quality improvement', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseResolvedErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Code Quality improved on 2 points.');
+ });
+
+ it('displays quality improvement and degradation', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Code Quality improved on 1 point and degraded on 1 point.');
+ });
+
+ it('displays no detected errors', async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseNoErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('No changes to Code Quality.');
+ });
+ });
+
+ describe('expanded data', () => {
+ beforeEach(async () => {
+ mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
+
+ createComponent();
+
+ await waitForPromises();
+
+ findToggleCollapsedButton().trigger('click');
+
+ await waitForPromises();
+ });
+
+ it('displays all report list items in viewport', async () => {
+ expect(findAllExtensionListItems()).toHaveLength(2);
+ });
+
+ it('displays report list item formatted', () => {
+ const text = {
+ newError: trimText(findAllExtensionListItems().at(0).text().replace(/\s+/g, ' ').trim()),
+ resolvedError: findAllExtensionListItems().at(1).text().replace(/\s+/g, ' ').trim(),
+ };
+
+ expect(text.newError).toContain(
+ "Minor - Parsing error: 'return' outside of function in index.js:12",
+ );
+ expect(text.resolvedError).toContain(
+ "Minor - Parsing error: 'return' outside of function in index.js:12",
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js
new file mode 100644
index 00000000000..f5ad0ce7377
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extentions/code_quality/mock_data.js
@@ -0,0 +1,87 @@
+export const codeQualityResponseNewErrors = {
+ status: 'failed',
+ new_errors: [
+ {
+ description: "Parsing error: 'return' outside of function",
+ severity: 'minor',
+ file_path: 'index.js',
+ line: 12,
+ },
+ {
+ description: 'TODO found',
+ severity: 'minor',
+ file_path: '.gitlab-ci.yml',
+ line: 73,
+ },
+ ],
+ resolved_errors: [],
+ existing_errors: [],
+ summary: {
+ total: 2,
+ resolved: 0,
+ errored: 2,
+ },
+};
+
+export const codeQualityResponseResolvedErrors = {
+ status: 'failed',
+ new_errors: [],
+ resolved_errors: [
+ {
+ description: "Parsing error: 'return' outside of function",
+ severity: 'minor',
+ file_path: 'index.js',
+ line: 12,
+ },
+ {
+ description: 'TODO found',
+ severity: 'minor',
+ file_path: '.gitlab-ci.yml',
+ line: 73,
+ },
+ ],
+ existing_errors: [],
+ summary: {
+ total: 2,
+ resolved: 2,
+ errored: 0,
+ },
+};
+
+export const codeQualityResponseResolvedAndNewErrors = {
+ status: 'failed',
+ new_errors: [
+ {
+ description: "Parsing error: 'return' outside of function",
+ severity: 'minor',
+ file_path: 'index.js',
+ line: 12,
+ },
+ ],
+ resolved_errors: [
+ {
+ description: "Parsing error: 'return' outside of function",
+ severity: 'minor',
+ file_path: 'index.js',
+ line: 12,
+ },
+ ],
+ existing_errors: [],
+ summary: {
+ total: 2,
+ resolved: 1,
+ errored: 1,
+ },
+};
+
+export const codeQualityResponseNoErrors = {
+ status: 'failed',
+ new_errors: [],
+ resolved_errors: [],
+ existing_errors: [],
+ summary: {
+ total: 0,
+ resolved: 0,
+ errored: 0,
+ },
+};
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 21d12ce2856..fc7ac35ed41 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -631,38 +631,6 @@ RSpec.describe Project, factory_default: :keep do
expect(project).not_to be_valid
end
-
- context 'restrict_special_characters_in_project_path feature flag is disabled' do
- before do
- stub_feature_flags(restrict_special_characters_in_project_path: false)
- end
-
- it "allows a path ending in '#{special_character}'" do
- project = build(:project, path: "foo#{special_character}")
-
- expect(project).to be_valid
- end
- end
- end
-
- context 'restrict_special_characters_in_project_path feature flag is disabled' do
- before do
- stub_feature_flags(restrict_special_characters_in_project_path: false)
- end
-
- %w[. _].each do |special_character|
- it "allows a path starting with '#{special_character}'" do
- project = build(:project, path: "#{special_character}foo")
-
- expect(project).to be_valid
- end
- end
-
- it "rejects a path starting with '-'" do
- project = build(:project, path: "-foo")
-
- expect(project).not_to be_valid
- end
end
end
end