diff options
Diffstat (limited to 'app')
13 files changed, 211 insertions, 47 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js index cd5cfb6837c..23f14bea4e1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js @@ -10,6 +10,8 @@ export const i18n = { label: s__('Reports|Test summary'), loading: s__('Reports|Test summary results are loading'), error: s__('Reports|Test summary failed to load results'), + newHeader: s__('Reports|New'), + fixedHeader: s__('Reports|Fixed'), fullReport: s__('Reports|Full report'), noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`), @@ -36,4 +38,32 @@ export const i18n = { sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }), headReportParsingError: s__('Reports|Head report parsing error:'), baseReportParsingError: s__('Reports|Base report parsing error:'), + + recentFailureSummary: (recentlyFailed, failed) => { + if (failed < 2) { + return sprintf( + s__( + 'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days', + ), + { recentlyFailed, failed }, + ); + } + return sprintf( + n__( + 'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days', + 'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days', + recentlyFailed, + ), + { recentlyFailed, failed }, + ); + }, + recentFailureCount: (recentFailures) => + sprintf( + n__( + 'Reports|Failed %{count} time in %{base_branch} in the last 14 days', + 'Reports|Failed %{count} times in %{base_branch} in the last 14 days', + recentFailures.count, + ), + recentFailures, + ), }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js index 65d9257903f..577b2cbfc5c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js @@ -1,7 +1,13 @@ import { uniqueId } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { EXTENSION_ICONS } from '../../constants'; -import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils'; +import { + summaryTextBuilder, + reportTextBuilder, + reportSubTextBuilder, + countRecentlyFailedTests, + recentFailuresTextBuilder, +} from './utils'; import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants'; export default { @@ -18,7 +24,10 @@ export default { if (data.hasSuiteError) { return this.$options.i18n.error; } - return summaryTextBuilder(this.$options.i18n.label, data.summary); + return { + subject: summaryTextBuilder(this.$options.i18n.label, data.summary), + meta: recentFailuresTextBuilder(data.summary), + }; }, statusIcon(data) { if (data.parsingInProgress) { @@ -50,6 +59,10 @@ export default { hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS), parsingInProgress: status === 204, ...data, + summary: { + recentlyFailed: countRecentlyFailedTests(data.suites), + ...data.summary, + }, }, }; }); @@ -66,17 +79,66 @@ export default { } return EXTENSION_ICONS.success; }, - prepareReports() { - return this.collapsedData.suites.map((suite) => { + testHeader(test, sectionHeader, index) { + const headers = []; + if (index === 0) { + headers.push(sectionHeader); + } + if (test.recent_failures?.count && test.recent_failures?.base_branch) { + headers.push(i18n.recentFailureCount(test.recent_failures)); + } + return headers; + }, + mapTestAsChild({ iconName, sectionHeader }) { + return (test, index) => { return { - id: uniqueId('suite-'), - text: reportTextBuilder(suite), - subtext: reportSubTextBuilder(suite), - icon: { - name: this.suiteIcon(suite), - }, + id: uniqueId('test-'), + header: this.testHeader(test, sectionHeader, index), + icon: { name: iconName }, + text: test.name, }; - }); + }; + }, + prepareReports() { + return this.collapsedData.suites + .map((suite) => { + return { + ...suite, + summary: { + recentlyFailed: countRecentlyFailedTests(suite), + ...suite.summary, + }, + }; + }) + .map((suite) => { + return { + id: uniqueId('suite-'), + text: reportTextBuilder(suite), + subtext: reportSubTextBuilder(suite), + icon: { + name: this.suiteIcon(suite), + }, + children: [ + ...[...suite.new_failures, ...suite.new_errors].map( + this.mapTestAsChild({ + sectionHeader: i18n.newHeader, + iconName: EXTENSION_ICONS.failed, + }), + ), + ...[...suite.existing_failures, ...suite.existing_errors].map( + this.mapTestAsChild({ + iconName: EXTENSION_ICONS.failed, + }), + ), + ...[...suite.resolved_failures, ...suite.resolved_errors].map( + this.mapTestAsChild({ + sectionHeader: i18n.fixedHeader, + iconName: EXTENSION_ICONS.success, + }), + ), + ], + }; + }); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js index a74ed20362f..9e4b0ac581c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js @@ -43,13 +43,42 @@ export const reportTextBuilder = ({ name = '', summary = {}, status }) => { return i18n.summaryText(name, resultsString); }; -export const reportSubTextBuilder = ({ suite_errors }) => { - const errors = []; - if (suite_errors?.head) { - errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`); - } - if (suite_errors?.base) { - errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`); +export const recentFailuresTextBuilder = (summary = {}) => { + const { failed, recentlyFailed } = summary; + if (!failed || !recentlyFailed) return ''; + + return i18n.recentFailureSummary(recentlyFailed, failed); +}; + +export const reportSubTextBuilder = ({ suite_errors, summary }) => { + if (suite_errors?.head || suite_errors?.base) { + const errors = []; + if (suite_errors?.head) { + errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`); + } + if (suite_errors?.base) { + errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`); + } + return errors.join('<br />'); } - return errors.join('<br />'); + return recentFailuresTextBuilder(summary); +}; + +export const countRecentlyFailedTests = (subject) => { + // handle either a single report or an array of reports + const reports = !subject.length ? [subject] : subject; + + return reports + .map((report) => { + return ( + [report.new_failures, report.existing_failures, report.resolved_failures] + // only count tests which have failed more than once + .map( + (failureArray) => + failureArray.filter((failure) => failure.recent_failures?.count > 1).length, + ) + .reduce((total, count) => total + count, 0) + ); + }) + .reduce((total, count) => total + count, 0); }; 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 4b3ad288768..04f71e2b185 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 @@ -195,6 +195,9 @@ export default { shouldRenderTestReport() { return Boolean(this.mr?.testResultsPath); }, + shouldRenderRefactoredTestReport() { + return window.gon?.features?.refactorMrWidgetTestSummary; + }, mergeError() { let { mergeError } = this.mr; @@ -512,7 +515,7 @@ export default { } }, registerTestReportExtension() { - if (this.shouldRenderTestReport && this.shouldShowExtension) { + if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) { registerExtension(testReportExtension); } }, @@ -588,7 +591,7 @@ export default { /> <grouped-test-reports-app - v-if="mr.testResultsPath && !shouldShowExtension" + v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport" class="js-reports-container" :endpoint="mr.testResultsPath" :head-blob-path="mr.headBlobPath" diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 03bb132fe47..47f82471937 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml) push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml) + push_frontend_feature_flag(:refactor_mr_widget_test_summary, project, default_enabled: :yaml) push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml) push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb index d0bdaaae5f8..10d603ef5d3 100644 --- a/app/helpers/lazy_image_tag_helper.rb +++ b/app/helpers/lazy_image_tag_helper.rb @@ -8,8 +8,11 @@ module LazyImageTagHelper end # Override the default ActionView `image_tag` helper to support lazy-loading + # accept :auto_dark boolean to enable automatic dark variant of the image + # (see: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2698) + # accept :dark_variant path to be used as a source when dark mode is enabled def image_tag(source, options = {}) - source = options[:dark_variant] if options[:dark_variant] && user_application_dark_mode? + source, options = prepare_dark_variant(source, options) options = options.symbolize_keys unless options.delete(:lazy) == false @@ -29,4 +32,25 @@ module LazyImageTagHelper # Required for Banzai::Filter::ImageLazyLoadFilter module_function :placeholder_image # rubocop: disable Style/AccessModifierDeclarations + + private + + def prepare_dark_variant(source, options) + dark_variant = options.delete(:dark_variant) + auto_dark = options.delete(:auto_dark) + + if dark_variant && auto_dark + raise ArgumentError, "dark_variant and auto_dark are mutually exclusive" + end + + if (auto_dark || dark_variant) && user_application_dark_mode? + if auto_dark + options[:class] = 'gl-dark-invert-keep-hue' + elsif dark_variant + source = dark_variant + end + end + + [source, options] + end end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 78bd520d5d5..92f0b978788 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -43,7 +43,8 @@ class ContainerRepository < ApplicationRecord migration_canceled: 4, not_found: 5, native_import: 6, - migration_forced_canceled: 7 + migration_forced_canceled: 7, + migration_canceled_by_registry: 8 } delegate :client, :gitlab_api_client, to: :registry @@ -214,7 +215,7 @@ class ContainerRepository < ApplicationRecord container_repository.migration_skipped_at = Time.zone.now end - before_transition any => %i[import_done import_aborted] do |container_repository| + before_transition any => %i[import_done import_aborted import_skipped] do |container_repository| container_repository.run_after_commit do ::ContainerRegistry::Migration::EnqueuerWorker.perform_async end @@ -328,7 +329,7 @@ class ContainerRepository < ApplicationRecord when 'import_canceled', 'pre_import_canceled' return if import_skipped? - skip_import(reason: :migration_canceled) + skip_import(reason: :migration_canceled_by_registry) when 'import_complete' finish_import when 'import_failed' @@ -376,6 +377,10 @@ class ContainerRepository < ApplicationRecord migration_retries_count >= ContainerRegistry::Migration.max_retries end + def nearing_or_exceeded_retry_limit? + migration_retries_count >= ContainerRegistry::Migration.max_retries - 1 + end + def last_import_step_done_at [migration_pre_import_done_at, migration_import_done_at, migration_aborted_at, migration_skipped_at].compact.max end diff --git a/app/models/key.rb b/app/models/key.rb index 42ea0f29171..07d5b1eea3a 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -24,15 +24,9 @@ class Key < ApplicationRecord length: { maximum: 5000 }, format: { with: /\A(#{Gitlab::SSHPublicKey.supported_algorithms.join('|')})/ } - validates :fingerprint, - uniqueness: true, - presence: { message: 'cannot be generated' }, - unless: -> { Gitlab::FIPS.enabled? } - validates :fingerprint_sha256, uniqueness: true, - presence: { message: 'cannot be generated' }, - if: -> { Gitlab::FIPS.enabled? } + presence: { message: 'cannot be generated' } validate :key_meets_restrictions diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 47f4b9c6898..30d7093b8a6 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -83,8 +83,10 @@ module QuickActions args.map! { _1.gsub(/\\_/, '_') } usernames = (args - ['me']).map { _1.delete_prefix('@') } found = User.by_username(usernames).to_a.select { can?(:read_user, _1) } - found_names = found.map(&:username).to_set - missing = args.reject { |arg| arg == 'me' || found_names.include?(arg.delete_prefix('@')) }.map { "'#{_1}'" } + found_names = found.map(&:username).map(&:downcase).to_set + missing = args.reject do |arg| + arg == 'me' || found_names.include?(arg.downcase.delete_prefix('@')) + end.map { "'#{_1}'" } failed_parse(format(_("Failed to find users for %{missing}"), missing: missing.to_sentence)) if missing.present? diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb index c8733bc2f11..ed6eb071f32 100644 --- a/app/services/service_ping/submit_service.rb +++ b/app/services/service_ping/submit_service.rb @@ -6,6 +6,7 @@ module ServicePing STAGING_BASE_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org' USAGE_DATA_PATH = 'usage_data' ERROR_PATH = 'usage_ping_errors' + METADATA_PATH = 'usage_ping_metadata' SubmissionError = Class.new(StandardError) @@ -31,7 +32,7 @@ module ServicePing message: e.message, elapsed: (Time.current - start).round(1) } - submit_payload({ error: error_payload }, url: error_url) + submit_payload({ error: error_payload }, path: ERROR_PATH) usage_data = Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values) response = submit_usage_data_payload(usage_data) @@ -48,21 +49,30 @@ module ServicePing raw_usage_data.update_version_metadata!(usage_data_id: version_usage_data_id) DevopsReportService.new(response).execute end - end - def url - URI.join(base_url, USAGE_DATA_PATH) - end + return unless Feature.enabled?(:measure_service_ping_metric_collection, default_enabled: :yaml) - def error_url - URI.join(base_url, ERROR_PATH) + submit_payload({ metadata: { metrics: metrics_collection_time(usage_data) } }, path: METADATA_PATH) end private - def submit_payload(payload, url: self.url) + def metrics_collection_time(payload, parents = []) + return [] unless payload.is_a?(Hash) + + payload.flat_map do |key, metric_value| + key_path = parents.dup.append(key) + if metric_value.respond_to?(:duration) + { name: key_path.join('.'), time_elapsed: metric_value.duration } + else + metrics_collection_time(metric_value, key_path) + end + end + end + + def submit_payload(payload, path: USAGE_DATA_PATH) Gitlab::HTTP.post( - url, + URI.join(base_url, path), body: payload.to_json, allow_local_requests: true, headers: { 'Content-type' => 'application/json' } diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 1a2f770cd59..6ecc050fa25 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -34,7 +34,7 @@ = render 'groups/settings/ip_restriction_registration_features_cta', f: f = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group - - if Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml) + - if @group.licensed_feature_available?(:group_wikis) && Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml) = render_if_exists 'groups/settings/wiki', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index d0c4fb2432c..d199c8e71a2 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -10,7 +10,7 @@ .row.empty-state.merge-requests .col-12 .svg-content - = image_tag 'illustrations/merge_requests.svg' + = image_tag 'illustrations/merge_requests.svg', { auto_dark: true } .col-12 .text-content - if has_filter_bar_param? diff --git a/app/workers/container_registry/migration/guard_worker.rb b/app/workers/container_registry/migration/guard_worker.rb index bab6b8c2a72..e5b3bd908c1 100644 --- a/app/workers/container_registry/migration/guard_worker.rb +++ b/app/workers/container_registry/migration/guard_worker.rb @@ -93,7 +93,7 @@ module ContainerRegistry end def long_running_migration_threshold - @threshold ||= 30.minutes.ago + @threshold ||= 10.minutes.ago end def cancel_long_running_migration(repository) @@ -101,7 +101,11 @@ module ContainerRegistry case result[:status] when :ok - repository.skip_import(reason: :migration_canceled) + if repository.nearing_or_exceeded_retry_limit? + repository.skip_import(reason: :migration_canceled) + else + repository.abort_import + end when :bad_request repository.reconcile_import_status(result[:state]) do repository.abort_import |